优化noco-agent功能和修复相关问题

- 修复Core类中的类型定义和拼写错误
- 添加tableId参数支持到NocoLifeService
- 优化认证逻辑,支持环境变量配置
- 增强配置功能,返回当前配置信息
- 改进任务完成功能,支持批量操作
- 添加记录创建和更新功能
- 更新依赖包版本
- 修复导出类型定义

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-10 17:46:45 +08:00
parent df385f00ed
commit 22fac88d01
10 changed files with 238 additions and 90 deletions

View File

@@ -11,7 +11,6 @@ export type CoreOptions<T = {}> = {
baseId?: string
} & T
type CoreItem = ColumnItem
export class Core {
nocoApi: NocoApi;
baseId?: string;
@@ -76,19 +75,19 @@ export class Core {
}
};
}
getItem(id: number): Promise<ReponseData<CoreItem>> {
getItem(id: number): Promise<ReponseData<ColumnItem>> {
return this.nocoApi.record.read(id);
}
getList(params: any): Promise<ReponseData<{ list: CoreItem[] }>> {
getList(params: any): Promise<ReponseData<{ list: ColumnItem[] }>> {
return this.nocoApi.record.list({
...params,
});
}
updateItem(data: Partial<CoreItem>) {
updateItem(data: Partial<ColumnItem>) {
return this.nocoApi.record.update(data);
}
creatItem(data: Partial<CoreItem>) {
createItem(data: Partial<ColumnItem>) {
return this.nocoApi.record.create(data);
}
}

View File

@@ -1,5 +1,5 @@
import { NocoApi } from "@kevisual/noco";
import { columns, ColumnItem } from "./common/base-table.ts";
import { ColumnItem, columns, } from "./common/base-table.ts";
import { Life } from "../noco/life/index.ts";
import { Control } from "../noco/control/index.ts";
import { Core } from "./common/core.ts";
@@ -8,12 +8,12 @@ import { NocoWehookPayload } from "./callback/index.ts";
export {
NocoApi,
columns,
ColumnItem,
Control,
Life,
Core,
}
export type {
NocoWehookPayload
NocoWehookPayload,
ColumnItem
}

View File

@@ -28,6 +28,8 @@
"packageManager": "pnpm@10.24.0",
"type": "module",
"dependencies": {
"@kevisual/app": "^0.0.1",
"@kevisual/context": "^0.0.4",
"@kevisual/local-proxy": "^0.0.8",
"@kevisual/noco-auto": "../",
"@kevisual/query": "^0.0.31",

View File

@@ -14,7 +14,9 @@ if (!hasAuth) {
id: 'auth'
}).define(async (ctx) => {
// 这里可以添加实际的认证逻辑
ctx.query.token = process.env.TOKEN || ' ';
if (!ctx.query.token) {
ctx.query.token = process.env.KEVISUAL_API_TOKEN || ' ';
console.log('本地测试认证通过,设置 token');
}
}).addTo(app);
}

View File

@@ -13,11 +13,16 @@ app.route({
const token = ctx.query.token || '';
const question = ctx.query.question || '';
let data = ctx.query.data || {};
const nocoLifeService = new NocoLifeService({ token });
const config = await nocoLifeService.getLifeConfig()
if (question) {
const ai: BaseChat = useContextKey('ai');
const originConfig = JSON.stringify(config);
await ai.chat([
{ role: 'system', content: `你是一个多维表格配置助理,你的任务是帮助用户解析多维表格的配置信息。用户会提供配置信息,你需要从中提取出 baseURL, token, baseId, tableId 等字段,并以 JSON 格式返回。如果某个字段缺失,可以不返回该字段。请确保返回的 JSON 格式正确且易于解析。` },
{ role: 'user', content: question }
{ role: 'user', content: question },
{ role: 'user', content: `当前已有的多维表格配置信息是: ${originConfig}` }
])
let msg = AIUtils.extractJsonFromMarkdown(ai.responseText || '');
if (msg == null) {
@@ -25,12 +30,9 @@ app.route({
}
data = msg;
}
if (!data?.baseURL || !data?.token || !data?.baseId) {
ctx.throw(400, '缺少参数 baseURL, token, baseId, tableId');
}
const nocoLifeService = new NocoLifeService({ token });
const config = await nocoLifeService.getLifeConfig()
if (data.baseURL) {
config.baseURL = data.baseURL;
}
@@ -48,7 +50,7 @@ app.route({
if (res.code !== 200) {
ctx.throw(500, '保存配置失败');
}
ctx.body = { content: '配置更新成功' };
ctx.body = { content: '配置更新成功,当前配置是: ' + (JSON.stringify(config)), data: config };
}).addTo(app);

View File

@@ -16,6 +16,7 @@ app.route({
ctx.throw(400, '缺少参数 question');
}
const token = ctx.query.token || '';
const tableId = ctx.query.tableId || '';
const slicedQuestion = question.slice(0, 10);
if (slicedQuestion.startsWith('配置多维表格')) {
const res = await ctx.call({
@@ -27,11 +28,12 @@ app.route({
ctx.body = res.body;
return;
}
const nocoLifeService = new NocoLifeService({ token });
const nocoLifeService = new NocoLifeService({ token, tableId });
await nocoLifeService.initConfig()
const routes = ctx.app.getList().filter(r => r.path.startsWith('noco-life') && r.key !== 'chat');
const v = `${routes.map((r, index) => `${index + 1}工具名称: ${r.id}\n描述: ${r.description}\n`).join('\n')}\n\n当用户询问时如果拥有工具请返回 JSON 数据不存在工具则返回分析判断数据JSON数据类型是{id,payload}外面的id是工具的id。如果工具有参数在 payload 当中默认不需要参数如果工具内部需要id在payload当中。`
const ai: BaseChat = useContextKey('ai');
const slicedQuestion2 = question.slice(0, 1000);
const answer = await ai.chat([
{ role: 'system', content: `你是一个多维表格助理,你的任务是帮助用户操作和查询多维表格的数据。你可以使用以下工具来完成任务:\n\n${v}` },
{ role: 'user', content: question }
@@ -64,7 +66,8 @@ app.route({
middleware: ['auth']
}).define(async (ctx) => {
const token = ctx.query.token || '';
const nocoLifeService = new NocoLifeService({ token });
const tableId = ctx.query.tableId || '';
const nocoLifeService = new NocoLifeService({ token, tableId });
await nocoLifeService.initConfig()
const life = nocoLifeService.life;
@@ -100,22 +103,34 @@ app.route({
app.route({
path: 'noco-life',
key: 'done',
description: `完成某件事情然后判断下一次运行时间。参数是id数据类型是number。`,
description: `完成某件事情然后判断下一次运行时间。参数是id数据类型是number。如果多个存在则是ids的number数组`,
middleware: ['auth']
}).define(async (ctx) => {
const id = ctx.query.id;
if (!id) {
const ids = ctx.query.ids || [];
if (!id || ids.length === 0) {
ctx.throw(400, '缺少参数 id');
}
console.log('id', id);
if (ids.length === 0 && id) {
ids.push(Number(id));
}
console.log('id', id, ids);
const token = ctx.query.token || '';
const nocoLifeService = new NocoLifeService({ token });
const tableId = ctx.query.tableId || '';
const nocoLifeService = new NocoLifeService({ token, tableId });
await nocoLifeService.initConfig()
const messages = [];
const changeItem = async (id: number) => {
const life = nocoLifeService.life;
// 获取记录详情
const recordRes = await life.getItem(id);
if (recordRes.code !== 200) {
ctx.throw(500, '获取记录详情失败');
// ctx.throw(500, '获取记录详情失败');
messages.push({
id,
content: `获取记录 ${id} 详情失败`,
});
return;
}
const record = recordRes.data;
@@ -125,7 +140,12 @@ app.route({
const startDate = dayjs(startTime).startOf('day');
if (startDate.isAfter(today)) {
ctx.throw(400, '还没到今天呢,到时候再做吧');
// ctx.throw(400, '还没到今天呢,到时候再做吧');
messages.push({
id,
content: `记录 ${id} 的启动时间是 ${dayjs(startTime).format('YYYY-MM-DD HH:mm:ss')},还没到今天呢,到时候再做吧`,
});
return;
}
// 计算下一次运行时间
// 1. 知道当前时间
@@ -168,25 +188,97 @@ ${prompt ? `这是我给你的提示词,帮你更好地理解我的需求:${
nextTime = time.toISOString();
}
} else {
ctx.throw(500, 'AI 返回的时间格式无效,无法格式化');
messages.push({
id,
content: `记录 ${id} 的任务 "${record['标题']}"AI 返回的时间格式无效,无法格式化,返回内容是:${ai.responseText}`,
});
return;
}
} catch (e) {
ctx.throw(500, 'AI 返回结果解析失败');
messages.push({
id,
content: `记录 ${id} 的任务 "${record['标题']}"AI 返回结果解析失败,返回内容是:${ai.responseText}`,
});
return;
}
const update = await life.updateItem({ Id: id, '启动时间': nextTime });
if (update.code !== 200) {
ctx.throw(500, '更新记录失败');
messages.push({
id,
content: `记录 ${id} 的任务 "${record['标题']}",更新记录失败`,
});
return;
}
ctx.body = {
const msg = {
id,
nextTime,
showCNTime: dayjs(nextTime).format('YYYY-MM-DD HH:mm:ss'),
content: `任务 "${record['标题']}" 已标记为完成。下一次运行时间是 ${dayjs(nextTime).format('YYYY-MM-DD HH:mm:ss')}`
};
messages.push(msg);
}
for (const _id of ids) {
await changeItem(Number(_id));
}
ctx.body = {
content: messages.map(m => m.content).join('\n'),
list: messages
};
}).addTo(app);
app.route({
path: 'noco-life',
key: 'record',
description: `创建或者更新一条新的记录,参数是 question 和 id 如果id存在则更新记录否则创建新的记录`,
middleware: ['auth']
}).define(async (ctx) => {
const { id, question } = ctx.query;
let summary = '空'
const token = ctx.query.token || '';
const tableId = ctx.query.tableId || '';
const nocoLifeService = new NocoLifeService({ token, tableId });
await nocoLifeService.initConfig()
const life = nocoLifeService.life;
const ai = useContextKey('ai');
let record = null;
if (id) {
record = await life.getItem(id);
if (record.code !== 200) {
// 获取记录失败
} else {
summary = record.data['总结'] || ''
}
}
const prompt = `对当前的内容进行总结要求简洁扼要200字以内。如果内容已经很简洁则不需要修改。当前内容是${question}\n历史总结内容是${summary}`;
await ai.chat([
{ role: 'system', content: `你是一个总结专家,擅长将冗长的信息进行提炼和总结。` },
{ role: 'user', content: prompt }
])
const newSummary = ai.responseText?.trim() || '';
if (record) {
// 更新记录
const updateRes = await life.updateItem({ Id: id, '总结': newSummary });
if (updateRes.code !== 200) {
ctx.throw(500, '更新记录失败');
}
ctx.body = {
id: id,
content: `已更新记录 ${id} 的总结内容为:${newSummary}`
}
} else {
// 创建记录
const createRes = await life.createItem({ '标题': question.slice(0, 50), '总结': newSummary, '任务': '运行中', '启动时间': new Date().toISOString() });
if (createRes.code !== 200) {
ctx.throw(500, '创建记录失败');
}
ctx.body = {
id: createRes.data.Id,
content: `已创建新的记录ID 是 ${createRes.data.Id}\n内容是${newSummary}`
}
}
}).addTo(app);
app.route({
path: 'noco-life',
key: 'how-to-use',

View File

@@ -4,6 +4,10 @@ import { Query } from "@kevisual/query/query";
import { CustomError } from '@kevisual/router'
type NocoLifeServiceOpts = {
token: string;
/**
* 不使用默认的视图配置,使用当前的表
*/
tableId?: string;
}
export type NocoLifeConfig = {
@@ -17,9 +21,11 @@ export class NocoLifeService {
nocoApi: NocoApi;
life: Life;
queryConfig: QueryConfig;
tableId: string;
constructor(opts: NocoLifeServiceOpts) {
this.token = opts.token;
const tableId = opts.tableId;
this.tableId = tableId || '';
this.initEnv();
}
initEnv() {
@@ -31,7 +37,7 @@ export class NocoLifeService {
async getLifeConfig(): Promise<NocoLifeConfig> {
const res = await this.queryConfig.getByKey('life.json', { token: this.token });
if (res.code !== 200) {
return {} as NocoLifeConfig;
return { 'baseId': '', baseURL: '', token: '', tableId: '' } as NocoLifeConfig;
}
return res.data?.data as NocoLifeConfig;
}
@@ -42,13 +48,17 @@ export class NocoLifeService {
}, { token: this.token });
return res;
}
async createLife(data: NocoLifeConfig) {
createLife(data: NocoLifeConfig) {
if (!data.tableId && this.tableId) {
data.tableId = this.tableId;
}
const nocoApi = new NocoApi({
baseURL: data.baseURL || '',
token: data.token || '',
table: data.tableId || '',
});
const life = new Life({ nocoApi, baseId: data.baseId });
this.life = life;
return life;
}
/**
@@ -71,24 +81,23 @@ export class NocoLifeService {
this.nocoApi = nocoApi;
const life = new Life({ nocoApi, baseId: lifeConfig.baseId });
const tableId = lifeConfig.tableId || '';
let tableId = this.tableId || lifeConfig.tableId || '';
if (!tableId) {
const newTable = await life.createTable()
if (newTable.code !== 200) {
throw new CustomError(500, `创建默认表失败: ${newTable.message}`);
}
lifeConfig.tableId = newTable.data?.id;
tableId = newTable.data?.id;
// 保存 tableId 到配置中
const res = await this.queryConfig.updateConfig({
key: 'life.json',
data: lifeConfig,
data: { ...lifeConfig, tableId },
}, { token: this.token });
if (res.code === 200) {
console.log('默认表创建成功,配置已更新');
}
}
life.tableId = lifeConfig.tableId || '';
nocoApi.record.table = life.tableId;
life.tableId = tableId || '';
this.life = life;
return lifeConfig;
}

View File

@@ -1,4 +1,4 @@
import { app, sleep } from './common';
import { app, sleep, token } from './common.ts';
const res = await app.call({
@@ -8,8 +8,13 @@ const res = await app.call({
// question: '今天我需要做什么事情?',
// question: '任务5 完成了,帮我判断下一次运行时间应该是什么时候?',
// question: '任务59 完成了',
question: '我的多维表格配置'
}
// question: '我的多维表格配置'
// question: '记录一下,今天洗了澡',
// question: '编辑任务123进行补充, 对opencode进行描述介绍。',
// question: '编辑任务94对内容注释',
question: '任务59和124完成了',
token: token,
},
})
console.log('res', res.code, res.body, res.message);

View File

@@ -6,13 +6,15 @@ import { BailianProvider } from '@kevisual/ai';
import dotenv from 'dotenv';
dotenv.config();
console.log('process.env.BAILIAN_API_KEY', process.env.BAILIAN_API_KEY);
const token = process.env.KEVISUAL_API_TOKEN || '';
const ai = useContextKey('ai', () => {
return new BailianProvider({
apiKey: process.env.BAILIAN_API_KEY || '',
model: 'qwen-turbo'
model: 'qwen-plus'
});
});
export {
app,
ai,
token,
}

35
pnpm-lock.yaml generated
View File

@@ -30,6 +30,12 @@ importers:
backend:
dependencies:
'@kevisual/app':
specifier: ^0.0.1
version: 0.0.1(dotenv@17.2.3)
'@kevisual/context':
specifier: ^0.0.4
version: 0.0.4
'@kevisual/local-proxy':
specifier: ^0.0.8
version: 0.0.8
@@ -670,6 +676,12 @@ packages:
'@kevisual/ai@0.0.16':
resolution: {integrity: sha512-K5KYm+dwHCnB61BhVFh9UcWiOS/FeS29ijvgwE/cQR8RonfPtX/oI7WhAu0jCGGSxTI6cel2LjrpU4JoVzWgnA==}
'@kevisual/ai@0.0.19':
resolution: {integrity: sha512-AFc8m6OcHZNxCb88bvzhvwWTZ4EVYyPupBzPUsLKLpdNBvsqm9TRboKCM2brJj2cqHnm+H+RbAk9AcGJkYhRCA==}
'@kevisual/app@0.0.1':
resolution: {integrity: sha512-PEx8P3l0iNSqrz9Ib9kVCYfqNMX6/LfNu+cEafmY6ECP1cV5Vmv+TH2fuasMosKjtbH2fAdDi97sbd29tdEK+g==}
'@kevisual/cache@0.0.3':
resolution: {integrity: sha512-BWEck69KYL96/ywjYVkML974RHjDJTj2ITQND1zFPR+hlBV1H1p55QZgSYRJCObg3EAV1S9Zic/fR2T4pfe8yg==}
@@ -2472,6 +2484,9 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
@@ -3820,6 +3835,24 @@ snapshots:
'@kevisual/permission': 0.0.3
'@kevisual/query': 0.0.30
'@kevisual/ai@0.0.19':
dependencies:
'@kevisual/logger': 0.0.4
'@kevisual/permission': 0.0.3
'@kevisual/query': 0.0.31
'@kevisual/app@0.0.1(dotenv@17.2.3)':
dependencies:
'@kevisual/ai': 0.0.19
'@kevisual/context': 0.0.4
'@kevisual/query': 0.0.31
'@kevisual/router': 0.0.36
'@kevisual/use-config': 1.0.21(dotenv@17.2.3)
mitt: 3.0.1
transitivePeerDependencies:
- dotenv
- supports-color
'@kevisual/cache@0.0.3':
dependencies:
idb-keyval: 6.2.2
@@ -6207,6 +6240,8 @@ snapshots:
minipass@7.1.2: {}
mitt@3.0.1: {}
mrmime@2.0.1: {}
ms@2.1.3: {}