import { app } from '@/app.ts' import { NocoLifeService } from './services/life.ts'; import { useContextKey } from '@kevisual/context'; import { BaseChat } from '@kevisual/ai'; import { AIUtils } from '@kevisual/ai'; import { createLunarDate, toGregorian } from 'lunar'; import dayjs from 'dayjs'; app.route({ path: 'noco-life', key: 'chat', description: `多维表格聊天接口, 对自己的多维表格的数据进行操作,参数是 question, `, middleware: ['auth'] }).define(async (ctx) => { const question = ctx.query.question || ''; if (!question) { 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({ path: 'noco-life', key: 'config-update', token: token, payload: { question } }) ctx.body = res.body; return; } 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 } ]) let msg = AIUtils.extractJsonFromMarkdown(ai.responseText || ''); if (msg == null) { ctx.throw(500, 'AI 返回结果解析失败'); } console.log('msg', msg); const route = routes.find(r => r.id === msg.id || r.key === msg.id); console.log('route============', route.id, route.path, route.key); const res = await ctx.call({ ...msg, token: token }); if (res.code !== 200) { console.log('调用工具失败', res.message); ctx.throw(500, res.message || '调用工具失败'); } console.log('con=============', res?.data); console.log('res', res.code, res.body?.content); ctx.body = res.body; }).addTo(app); app.route({ path: 'noco-life', key: 'today', description: `获取今天需要做的事情列表`, middleware: ['auth'] }).define(async (ctx) => { const token = ctx.query.token || ''; const tableId = ctx.query.tableId || ''; const nocoLifeService = new NocoLifeService({ token, tableId }); await nocoLifeService.initConfig() const life = nocoLifeService.life; const tomorrow = dayjs().add(1, 'day').startOf('day').toISOString(); const tomorrowDate = dayjs(tomorrow).format('YYYY-MM-DD'); const res = await life.getList({ fields: ['Id', '标题', '总结', '启动时间', '标签', '任务'], where: `(任务,eq,运行中)~and(启动时间,lt,exactDate,${tomorrowDate})`, // where: "(任务,eq,运行中)~and(启动时间,le,today)", // where: "(任务,eq,运行中)~and(启动时间,le,daysAgo,-1)", sort: '启动时间', }); console.log('today res', res.data?.list?.map(i => i['标题'])); if (res.code === 200) { const list = res.data.list || [] ctx.body = { list, content: list.map(item => { return `任务[${item['Id']}]: ${item['标题']}。\n启动时间: ${dayjs(item['启动时间']).format('YYYY-MM-DD HH:mm:ss')}。标签: ${item['标签'] || '无'} \n总结: ${item['总结'] || '无'}`; }).join('\n') }; if (list.length === 0) { ctx.body = { list, content: '今天没有需要做的事情了,休息一下吧' } } return; } ctx.throw(500, '获取记录列表失败'); }).addTo(app); app.route({ path: 'noco-life', key: 'done', description: `完成某件事情,然后判断下一次运行时间。参数是id,数据类型是number。如果多个存在,则是ids的number数组`, middleware: ['auth'] }).define(async (ctx) => { const id = ctx.query.id; const ids = ctx.query.ids || []; if (!id && ids.length === 0) { ctx.throw(400, '缺少参数 id'); } if (ids.length === 0 && id) { ids.push(Number(id)); } console.log('id', id, ids); const token = ctx.query.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, '获取记录详情失败'); messages.push({ id, content: `获取记录 ${id} 详情失败`, }); return; } const record = recordRes.data; // 检查启动时间是否大于今天 const startTime = record['启动时间']; const today = dayjs().startOf('day'); const startDate = dayjs(startTime).startOf('day'); 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}` : ''} 相关资料是 任务:${record['标题']} 总结:${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(); } else { nextTime = time.toISOString(); } } else { messages.push({ id, content: `记录 ${id} 的任务 "${record['标题']}",AI 返回的时间格式无效,无法格式化,返回内容是:${ai.responseText}`, }); return; } } catch (e) { messages.push({ id, content: `记录 ${id} 的任务 "${record['标题']}",AI 返回结果解析失败,返回内容是:${ai.responseText}`, }); return; } 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); } 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', description: `多维表格使用指南,如何配置和使用多维表格`, middleware: ['auth'] }).define(async (ctx) => { const message = `多维表格使用指南: 1. 发送 "配置多维表格" 来设置和更新多维表格的配置。 2. 配置包含的内容是 baseURL, baseId, token, tableId 其中 tableId是可选的,如果不配置会自动创建一个新的多维表格。 ` ctx.body = { content: message } }).addTo(app);