This commit is contained in:
2025-12-06 18:56:08 +08:00
parent c02ecc9d85
commit ac0e980f34
21 changed files with 1158 additions and 633 deletions

View File

@@ -0,0 +1,188 @@
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 || '';
if (question.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 });
await nocoLifeService.initConfig()
const routes = ctx.queryRouter.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 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 nocoLifeService = new NocoLifeService({ token });
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['标题']}[${item['Id']}], 启动时间: ${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。`,
middleware: ['auth']
}).define(async (ctx) => {
const id = ctx.query.id;
if (!id) {
ctx.throw(400, '缺少参数 id');
}
console.log('id', id);
const token = ctx.query.token || '';
const nocoLifeService = new NocoLifeService({ token });
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 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点启动。
${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 {
ctx.throw(500, 'AI 返回的时间格式无效,无法格式化');
}
} catch (e) {
ctx.throw(500, 'AI 返回结果解析失败');
}
const update = await life.updateItem({ Id: id, '启动时间': nextTime });
if (update.code !== 200) {
ctx.throw(500, '更新记录失败');
}
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')}`
};
}).addTo(app);