diff --git a/agents/noco/common/core.ts b/agents/noco/common/core.ts index 049949b..652c5e2 100644 --- a/agents/noco/common/core.ts +++ b/agents/noco/common/core.ts @@ -11,7 +11,6 @@ export type CoreOptions = { baseId?: string } & T -type CoreItem = ColumnItem export class Core { nocoApi: NocoApi; baseId?: string; @@ -76,19 +75,19 @@ export class Core { } }; } - getItem(id: number): Promise> { + getItem(id: number): Promise> { return this.nocoApi.record.read(id); } - getList(params: any): Promise> { + getList(params: any): Promise> { return this.nocoApi.record.list({ ...params, }); } - updateItem(data: Partial) { + updateItem(data: Partial) { return this.nocoApi.record.update(data); } - creatItem(data: Partial) { + createItem(data: Partial) { return this.nocoApi.record.create(data); } } \ No newline at end of file diff --git a/agents/noco/index.ts b/agents/noco/index.ts index 05d9631..efdf370 100644 --- a/agents/noco/index.ts +++ b/agents/noco/index.ts @@ -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 } \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 00a2135..6583b16 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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", diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 29a815c..609392c 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -14,7 +14,9 @@ if (!hasAuth) { id: 'auth' }).define(async (ctx) => { // 这里可以添加实际的认证逻辑 - ctx.query.token = process.env.TOKEN || ' '; - console.log('本地测试认证通过,设置 token'); + if (!ctx.query.token) { + ctx.query.token = process.env.KEVISUAL_API_TOKEN || ' '; + console.log('本地测试认证通过,设置 token'); + } }).addTo(app); } \ No newline at end of file diff --git a/backend/src/routes/noco/config.ts b/backend/src/routes/noco/config.ts index 354925c..1a9569f 100644 --- a/backend/src/routes/noco/config.ts +++ b/backend/src/routes/noco/config.ts @@ -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); diff --git a/backend/src/routes/noco/noco-life.ts b/backend/src/routes/noco/noco-life.ts index 63cda1d..7405160 100644 --- a/backend/src/routes/noco/noco-life.ts +++ b/backend/src/routes/noco/noco-life.ts @@ -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,49 +103,66 @@ 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 life = nocoLifeService.life; - // 获取记录详情 - const recordRes = await life.getItem(id); - if (recordRes.code !== 200) { - ctx.throw(500, '获取记录详情失败'); - } - const record = recordRes.data; + const messages = []; + const changeItem = async (id: number) => { + const life = nocoLifeService.life; + // 获取记录详情 + const recordRes = await life.getItem(id); + if (recordRes.code !== 200) { + // ctx.throw(500, '获取记录详情失败'); + messages.push({ + id, + content: `获取记录 ${id} 详情失败`, + }); + return; + } + const record = recordRes.data; - // 检查启动时间是否大于今天 - const startTime = record['启动时间']; - const today = dayjs().startOf('day'); - const startDate = dayjs(startTime).startOf('day'); + // 检查启动时间是否大于今天 + const startTime = record['启动时间']; + const today = dayjs().startOf('day'); + const startDate = dayjs(startTime).startOf('day'); - if (startDate.isAfter(today)) { - ctx.throw(400, '还没到今天呢,到时候再做吧'); - } - // 计算下一次运行时间 - // 1. 知道当前时间 - // 2. 知道任务类型,如果是每日,则加一天;如果是每周,则加七天;如果是每月,则加一个月,如果是每年农历,需要转为新的,如果是其他,需要智能判断 - // 3. 更新记录 - const strTime = (time: string) => { - return dayjs(time).format('YYYY-MM-DD HH:mm:ss'); - } - const currentTime = strTime(new Date().toISOString()); - const isLuar = record['类型']?.includes?.('农历'); - let summay = record['总结'] || '无'; - if (summay.length > 200) { - summay = summay.substring(0, 200) + '...'; - } - const prompt = record['提示词'] || ''; - const type = record['类型'] || ''; - const content = `上一次执行的时间是${strTime(startTime)},当前时间是${currentTime},请帮我计算下一次的运行时间,如果时间不存在,默认在8点启动。 + if (startDate.isAfter(today)) { + // ctx.throw(400, '还没到今天呢,到时候再做吧'); + messages.push({ + id, + content: `记录 ${id} 的启动时间是 ${dayjs(startTime).format('YYYY-MM-DD HH:mm:ss')},还没到今天呢,到时候再做吧`, + }); + return; + } + // 计算下一次运行时间 + // 1. 知道当前时间 + // 2. 知道任务类型,如果是每日,则加一天;如果是每周,则加七天;如果是每月,则加一个月,如果是每年农历,需要转为新的,如果是其他,需要智能判断 + // 3. 更新记录 + const strTime = (time: string) => { + return dayjs(time).format('YYYY-MM-DD HH:mm:ss'); + } + const currentTime = strTime(new Date().toISOString()); + const isLuar = record['类型']?.includes?.('农历'); + let summay = record['总结'] || '无'; + if (summay.length > 200) { + summay = summay.substring(0, 200) + '...'; + } + const prompt = record['提示词'] || ''; + const type = record['类型'] || ''; + const content = `上一次执行的时间是${strTime(startTime)},当前时间是${currentTime},请帮我计算下一次的运行时间,如果时间不存在,默认在8点启动。 ${prompt ? `这是我给你的提示词,帮你更好地理解我的需求:${prompt}` : ''} 相关资料是 @@ -150,43 +170,115 @@ ${prompt ? `这是我给你的提示词,帮你更好地理解我的需求:${ 总结:${summay} 类型: ${type} ` - const ai = useContextKey('ai'); - await ai.chat([ - { role: 'system', content: `你是一个时间计算专家,擅长根据任务类型和时间计算下一次运行时间。只返回我对应的日期的结果,格式是:YYYY-MM-DD HH:mm:ss。` }, - { role: 'user', content } - ]) - let nextTime = ai.responseText?.trim(); - try { - // 判断返回的时间是否可以格式化 - if (nextTime && dayjs(nextTime).isValid()) { - const time = dayjs(nextTime); - if (isLuar) { - const festival = createLunarDate({ year: time.year(), month: time.month() + 1, day: time.date() }); - const { date } = toGregorian(festival); - nextTime = dayjs(date).toISOString(); + const ai = useContextKey('ai'); + await ai.chat([ + { role: 'system', content: `你是一个时间计算专家,擅长根据任务类型和时间计算下一次运行时间。只返回我对应的日期的结果,格式是:YYYY-MM-DD HH:mm:ss。` }, + { role: 'user', content } + ]) + let nextTime = ai.responseText?.trim(); + try { + // 判断返回的时间是否可以格式化 + if (nextTime && dayjs(nextTime).isValid()) { + const time = dayjs(nextTime); + if (isLuar) { + const festival = createLunarDate({ year: time.year(), month: time.month() + 1, day: time.date() }); + const { date } = toGregorian(festival); + nextTime = dayjs(date).toISOString(); + } else { + nextTime = time.toISOString(); + } } else { - nextTime = time.toISOString(); + messages.push({ + id, + content: `记录 ${id} 的任务 "${record['标题']}",AI 返回的时间格式无效,无法格式化,返回内容是:${ai.responseText}`, + }); + return; } - } else { - ctx.throw(500, 'AI 返回的时间格式无效,无法格式化'); + } catch (e) { + messages.push({ + id, + content: `记录 ${id} 的任务 "${record['标题']}",AI 返回结果解析失败,返回内容是:${ai.responseText}`, + }); + return; } - } catch (e) { - ctx.throw(500, 'AI 返回结果解析失败'); + const update = await life.updateItem({ Id: id, '启动时间': nextTime }); + if (update.code !== 200) { + messages.push({ + id, + content: `记录 ${id} 的任务 "${record['标题']}",更新记录失败`, + }); + return; + } + 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); } - const update = await life.updateItem({ Id: id, '启动时间': nextTime }); - if (update.code !== 200) { - ctx.throw(500, '更新记录失败'); + + for (const _id of ids) { + await changeItem(Number(_id)); } ctx.body = { - id, - nextTime, - showCNTime: dayjs(nextTime).format('YYYY-MM-DD HH:mm:ss'), - content: `任务 "${record['标题']}" 已标记为完成。下一次运行时间是 ${dayjs(nextTime).format('YYYY-MM-DD HH:mm:ss')}` + 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', diff --git a/backend/src/routes/noco/services/life.ts b/backend/src/routes/noco/services/life.ts index 060cf96..50d012c 100644 --- a/backend/src/routes/noco/services/life.ts +++ b/backend/src/routes/noco/services/life.ts @@ -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 { 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; } diff --git a/backend/test/chat.ts b/backend/test/chat.ts index 7f8dabc..be58560 100644 --- a/backend/test/chat.ts +++ b/backend/test/chat.ts @@ -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); \ No newline at end of file diff --git a/backend/test/common.ts b/backend/test/common.ts index 011d539..21fb95e 100644 --- a/backend/test/common.ts +++ b/backend/test/common.ts @@ -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, } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aff7469..70a8766 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {}