Files
noco-auto/backend/src/routes/noco/noco-life.ts

295 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);