Compare commits

..

9 Commits

Author SHA1 Message Date
719d7e76e1 更新问题描述,注释掉旧问题并添加新问题 2025-12-16 23:45:39 +08:00
9cd1de87dd update 2025-12-12 16:41:31 +08:00
32db5ec306 update 2025-12-12 11:57:12 +08:00
63e853641b 修复当只传入id参数时的判断逻辑错误 2025-12-12 11:56:09 +08:00
22fac88d01 优化noco-agent功能和修复相关问题
- 修复Core类中的类型定义和拼写错误
- 添加tableId参数支持到NocoLifeService
- 优化认证逻辑,支持环境变量配置
- 增强配置功能,返回当前配置信息
- 改进任务完成功能,支持批量操作
- 添加记录创建和更新功能
- 更新依赖包版本
- 修复导出类型定义

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-10 17:46:45 +08:00
df385f00ed update 2025-12-07 12:12:48 +08:00
0b0482a217 fix 2025-12-06 18:58:31 +08:00
ac0e980f34 temp 2025-12-06 18:56:08 +08:00
c02ecc9d85 update version 2025-12-04 14:23:58 +08:00
25 changed files with 1407 additions and 690 deletions

View File

@@ -0,0 +1,21 @@
export type BaseNocoItem<T = {}> = {
Id: number,
CreatedAt: string
UpdatedAt: string
} & T;
export type NocoWehookPayload<NocoItem = {}> = {
/** 请求id */
id: string;
type: "records.after.trigger";
/**
* 多维表base id
**/
base_id: string;
version: "v3",
data: {
table_id: string;
table_name: string;
rows: BaseNocoItem<NocoItem>[];
}
}

View File

@@ -2,6 +2,7 @@ import { CreateColumnData } from "@kevisual/noco"
export const columns: CreateColumnData[] = [
{
title: 'Id',
// @ts-ignore
uidt: "ID",
pk: true,
pv: true,
@@ -10,7 +11,7 @@ export const columns: CreateColumnData[] = [
title: '标题',
uidt: 'SingleLineText',
description: '简单的标题',
description: '简单的标题, 最简单的介绍',
},
{
title: '标签',
@@ -116,3 +117,21 @@ export const columns: CreateColumnData[] = [
uidt: 'LongText',
description: '和AI交互时候简单的实时提示词',
}]
export type ColumnItem<T = {}> = {
'Id': number,
'CreatedAt': string,
'UpdatedAt': string,
"标题": string,
"标签"?: string,
"总结"?: string,
"描述"?: string,
"数据"?: string,
"链接"?: string,
"类型"?: string,
"启动时间"?: string,
"任务"?: "非任务" | "运行中" | "已停止" | "个人计划" | "已完成" | "AI自动化",
"任务结果"?: string,
"提示词"?: string,
} & T;

View File

@@ -1,6 +1,6 @@
import { NocoApi } from "@kevisual/noco";
import { columns } from "../common/base-table.ts";
import { ColumnItem } from "./base-table.ts";
type ReponseData<T = {}> = {
code: number,
message?: string,
@@ -11,26 +11,13 @@ export type CoreOptions<T = {}> = {
baseId?: string
} & T
type CoreItem = {
Id: number,
标题: string,
总结?: string,
启动时间?: string,
标签?: string,
任务?: string, // 运行中,已完成,未开始
链接?: string,
数据?: string,
类型?: string,
提示词?: string,
任务结果?: string,
}
export class Core {
nocoApi: NocoApi;
baseId?: string;
#baseId?: string;
key = 'core';
title = '默认表';
description = '默认表描述';
tableId?: string;
#tableId?: string;
constructor(opts: {
nocoApi: NocoApi,
baseId?: string,
@@ -40,6 +27,21 @@ export class Core {
this.baseId = opts.baseId;
this.tableId = opts.tableId;
}
get tableId() {
return this.#tableId;
}
set tableId(id: string | undefined) {
this.#tableId = id;
if (this.nocoApi.record && id) {
this.nocoApi.record.table = id;
}
}
get baseId() {
return this.#baseId;
}
set baseId(id: string | undefined) {
this.#baseId = id;
}
async createTable(opts?: { columns?: any[], title?: string, description?: string, baseId?: string }): Promise<ReponseData<{ id: string, title: string }>> {
const baseId = opts?.baseId ?? this.baseId!;
const title = opts?.title ?? this.title;
@@ -68,7 +70,7 @@ export class Core {
tableId = res?.data?.id;
}
this.tableId = tableId;
if(this.nocoApi.record) {
if (this.nocoApi.record) {
this.nocoApi.record.table = tableId;
}
return {
@@ -79,19 +81,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,4 +1,3 @@
import { NocoApi } from "@kevisual/noco";
import { Core } from "../common/index.ts";
export class Control extends Core {

View File

@@ -1,11 +1,19 @@
import { NocoApi } from "@kevisual/noco";
import { columns } 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";
import { NocoWehookPayload } from "./callback/index.ts";
export {
NocoApi,
columns,
Control,
Life
Life,
Core,
}
export type {
NocoWehookPayload,
ColumnItem
}

View File

@@ -1,4 +1,3 @@
import { NocoApi } from "@kevisual/noco";
import { Core } from "../common/index.ts";
export class Life extends Core {

View File

@@ -14,6 +14,7 @@
"scripts": {
"dev": "bun --watch src/main.ts ",
"build": "pnpm run clean && bun run bun.config.mjs",
"create": "bun run test/create-json.ts",
"clean": "rm -rf dist && rimraf pack-dist",
"prepub": "pnpm build",
"pub": "envision pack -p -u"
@@ -24,13 +25,15 @@
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.24.0",
"packageManager": "pnpm@10.25.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.29",
"@kevisual/router": "0.0.33",
"@kevisual/query": "^0.0.32",
"@kevisual/router": "0.0.37",
"@kevisual/use-config": "^1.0.21",
"archiver": "^7.0.1",
"dayjs": "^1.11.19",
@@ -39,11 +42,12 @@
"nanoid": "^5.1.6"
},
"devDependencies": {
"@kevisual/ai": "^0.0.15",
"@kevisual/ai": "^0.0.19",
"@kevisual/types": "^0.0.10",
"@types/archiver": "^7.0.0",
"@types/bun": "^1.3.3",
"@types/node": "^24.10.1",
"dotenv": "^17.2.3"
"@types/bun": "^1.3.4",
"@types/node": "^25.0.1",
"dotenv": "^17.2.3",
"fast-glob": "^3.3.3"
}
}

View File

@@ -0,0 +1,18 @@
[
{
"title": "应用脚本",
"content": "应用脚本"
},
{
"title": "智能家居HA",
"content": "请根据用户输入生成符合规范的快捷键 JSON 数据,格式如下:\n\n```json\n{ \"type\": \"ha\", \"ha\": { \"entity_id\": \"string\", \"task\": \"\" } }\n```\n"
},
{
"title": "快捷键",
"content": "请根据用户输入生成符合规范的快捷键 JSON 数据,格式如下:\n```json\n{\"type\":\"hotkeys\",\"hotkeys\":\"ctrlOrCommand+h\"}\n```\n### 规则说明:\n\n1. **数据类型固定为**`\"hotkeys\"`,字段名为 `hotkeys`,值为**标准快捷键字符串**,使用小写字母和 `+` 连接。\n1. **操作系统适配**:所有修饰符的 `Ctrl` 键必须替换为 `ctrlOrCommand`, 但是win默认为windows的按键alt键保留\n2. 如果用户没有提供指令但是说要复制则根据对应的情况生成一个快捷键比如ctrlOrCommand+c\n3. *优先级规则**\n - 如果用户**明确提供了快捷键指令**(如“按 Ctrl+H”、“设置快捷键为 Ctrl+Shift+A”则**优先解析并生成对应的快捷键**。\n - 如果用户**未明确提供快捷键**,但表达了**常见操作意图**(如“复制”、\"粘贴\"、ps快捷键等则根据标准自动映射\n\n比如生成一个ctrl+h的快捷键生成的json数据是\n```json\n{”type\":\"hotkeys\",\"hotkeys\":\"ctrlOrCommnd+h\"}\n```\n### 用户输入内容是\n"
},
{
"title": "文档",
"content": "对当前内容进行美化"
}
]

View File

@@ -0,0 +1 @@
应用脚本

View File

@@ -0,0 +1,18 @@
请根据用户输入生成符合规范的快捷键 JSON 数据,格式如下:
```json
{"type":"hotkeys","hotkeys":"ctrlOrCommand+h"}
```
### 规则说明:
1. **数据类型固定为**`"hotkeys"`,字段名为 `hotkeys`,值为**标准快捷键字符串**,使用小写字母和 `+` 连接。
1. **操作系统适配**:所有修饰符的 `Ctrl` 键必须替换为 `ctrlOrCommand`, 但是win默认为windows的按键alt键保留
2. 如果用户没有提供指令但是说要复制则根据对应的情况生成一个快捷键比如ctrlOrCommand+c
3. *优先级规则**
- 如果用户**明确提供了快捷键指令**(如“按 Ctrl+H”、“设置快捷键为 Ctrl+Shift+A”则**优先解析并生成对应的快捷键**。
- 如果用户**未明确提供快捷键**,但表达了**常见操作意图**(如“复制”、"粘贴"、ps快捷键等则根据标准自动映射
比如生成一个ctrl+h的快捷键生成的json数据是
```json
{”type":"hotkeys","hotkeys":"ctrlOrCommnd+h"}
```
### 用户输入内容是

View File

@@ -0,0 +1 @@
对当前内容进行美化

View File

@@ -0,0 +1,5 @@
请根据用户输入生成符合规范的快捷键 JSON 数据,格式如下:
```json
{ "type": "ha", "ha": { "entity_id": "string", "task": "" } }
```

View File

@@ -1,5 +1,5 @@
import { app } from './app.ts'
import './router/index.ts';
import './routes/index.ts';
import { HOME } from './config.ts';
import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts';

View File

@@ -2,7 +2,6 @@
// base
import { app } from '../app.ts';
import './noco/index.ts';
import './noco/config.ts';
// 添加认证中间件路由
const hasAuth = app.router.routes.some(r => r.id === 'auth');
@@ -15,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);
}

View File

@@ -0,0 +1,106 @@
import { app } from '@/app.ts'
import { NocoLifeService } from './services/life.ts';
import { useContextKey } from '@kevisual/context';
import { NocoWehookPayload, ColumnItem } from '@kevisual/noco-auto';
import { AIUtils, BaseChat } from '@kevisual/ai';
export const reportErrors = (errors: any[]) => {
// TODO
}
app.route({
path: 'noco-control',
key: 'generateData',
description: `多维表格自动生成数据接口, 根据用户需求,自动生成字段"数据"的内容`,
middleware: ['auth']
}).define(async (ctx) => {
const query = ctx.query as NocoWehookPayload<ColumnItem>;
const token = ctx.query.token || '';
const baseId = query?.base_id;
const tableId = query?.data?.table_id;
let question = ctx.query.question || '';
const lifeService = new NocoLifeService({ token });
const config = await lifeService.getLifeConfig();
console.log('rows', query.data.rows);
if (config.baseId !== baseId) {
ctx.throw(400, 'baseId 不匹配');
}
const life = await lifeService.createLife({ ...config, tableId });
const row = query.data.rows?.[0];
if (!row) {
ctx.throw(400, '没有数据行');
}
const columnKeys = Object.keys(row).filter(k => !['Id', 'CreatedAt', 'UpdatedAt'].includes(k));
if (columnKeys.length === 0) {
ctx.throw(400, '没有可用的字段');
}
let prompt = question || row['提示词'] || ''
const id = row['Id'];
if (!id) {
ctx.throw(400, '数据行没有 Id');
}
const title = row['标题'] || '';
const summary = row['总结'] || '';
const type = row['类型'] || '';
const _data = row['数据'] || '';
let systemPrompt = getPrompt({ type });
let other = `\n相关资料是
标题: ${title}
总结: ${summary}`
if (_data) {
other += `
已有数据: ${_data}`
}
if (title) {
systemPrompt += other
}
const ai: BaseChat = useContextKey('ai');
const answer = await ai.chat([
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt ? prompt : '请生成对应的数据' }
])
let msg = ai.responseText || '';
console.log('生成的数据内容:', msg);
const data = await AIUtils.extractJsonFromMarkdown(msg);
if (data == null) {
ctx.throw(500, 'AI 返回结果解析失败');
}
// 更新数据到多维表格
const itme = await life.updateItem({
Id: id,
['数据']: data,
});
console.log('更新后的数据行:', itme);
ctx.body = 'ok'
}).addTo(app)
const DATA_TYPES = ['快捷键', '应用脚本', '智能家居HA', '文档'];
type DataType = typeof DATA_TYPES[number];
const getPrompt = (opts?: { type: DataType }) => {
const type = opts?.type || '通用';
const data = [
{
"title": "应用脚本",
"content": "应用脚本"
},
{
"title": "智能家居HA",
"content": "请根据用户输入生成符合规范的快捷键 JSON 数据,格式如下:\n\n```json\n{ \"type\": \"ha\", \"ha\": { \"entity_id\": \"string\", \"task\": \"\" } }\n```\n"
},
{
"title": "快捷键",
"content": "请根据用户输入生成符合规范的快捷键 JSON 数据,格式如下:\n```json\n{\"type\":\"hotkeys\",\"hotkeys\":\"ctrlOrCommand+h\"}\n```\n### 规则说明:\n\n1. **数据类型固定为**`\"hotkeys\"`,字段名为 `hotkeys`,值为**标准快捷键字符串**,使用小写字母和 `+` 连接。\n1. **操作系统适配**:所有修饰符的 `Ctrl` 键必须替换为 `ctrlOrCommand`, 但是win默认为windows的按键alt键保留\n2. 如果用户没有提供指令但是说要复制则根据对应的情况生成一个快捷键比如ctrlOrCommand+c\n3. *优先级规则**\n - 如果用户**明确提供了快捷键指令**(如“按 Ctrl+H”、“设置快捷键为 Ctrl+Shift+A”则**优先解析并生成对应的快捷键**。\n - 如果用户**未明确提供快捷键**,但表达了**常见操作意图**(如“复制”、\"粘贴\"、ps快捷键等则根据标准自动映射\n\n比如生成一个ctrl+h的快捷键生成的json数据是\n```json\n{”type\":\"hotkeys\",\"hotkeys\":\"ctrlOrCommnd+h\"}\n```\n### 用户输入内容是\n"
},
{
"title": "文档",
"content": "对当前内容进行美化"
}
]
const item = data.find(d => d.title === type);
if (item) {
return item.content;
}
return '';
}

View File

@@ -3,6 +3,7 @@ import { NocoLifeService } from './services/life.ts';
import dayjs from 'dayjs';
import { useContextKey } from '@kevisual/context';
import { BaseChat, AIUtils } from '@kevisual/ai';
import { pick } from 'es-toolkit'
app.route({
path: 'noco-life',
key: 'config-update',
@@ -13,24 +14,29 @@ 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 pickData = pick(config, ['baseURL', 'token', 'baseId', 'tableId']);
const originConfig = JSON.stringify(pickData);
await ai.chat([
{ role: 'system', content: `你是一个多维表格配置助理,你的任务是帮助用户解析多维表格的配置信息。用户会提供配置信息,你需要从中提取出 baseURL, token, baseId, tableId 等字段,并以 JSON 格式返回。如果某个字段缺失,可以不返回该字段。请确保返回的 JSON 格式正确且易于解析。` },
{ role: 'user', content: question }
{
role: 'system', content: `你是一个多维表格配置助理,你的任务是帮助用户解析多维表格的配置信息。用户会提供配置信息,你需要从中提取出 baseURL, token, baseId, tableId 等字段,并以 JSON 格式返回。如果某个字段缺失,可以不返回该字段。请确保返回的 JSON 格式正确且易于解析。
返回的数据例子: { "baseURL": "https://example.com", "token": "abc123", "baseId": "base_01", "tableId": "table_01" }` },
{ role: 'user', content: `当前已有的多维表格配置信息是: ${originConfig}` },
{ role: 'user', content: question },
])
let msg = AIUtils.extractJsonFromMarkdown(ai.responseText || '');
if (msg == null) {
ctx.throw(500, 'AI 返回结果解析失败');
}
data = msg;
data = { ...data, ...config, ...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 +54,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

@@ -1,188 +1,4 @@
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);
import './noco-life.ts'
import './config.ts'
import './auto-generate-data.ts'

View File

@@ -0,0 +1,295 @@
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);

View File

@@ -1,10 +1,13 @@
import { Life, NocoApi } from '@kevisual/noco-auto';
import { initConfig } from "@kevisual/use-config";
import { QueryConfig } from "@/query/query-config/query-config.ts";
import { Query } from "@kevisual/query/query";
import { CustomError } from '@kevisual/router'
type NocoLifeServiceOpts = {
token: string;
/**
* 不使用默认的视图配置,使用当前的表
*/
tableId?: string;
}
export type NocoLifeConfig = {
@@ -18,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() {
@@ -32,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;
}
@@ -43,6 +48,19 @@ export class NocoLifeService {
}, { token: this.token });
return res;
}
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;
}
/**
* 需要从服务端获取自己保存的配置,包括 nocodb 地址apiKey 等
*/
@@ -63,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,14 @@ const res = await app.call({
// question: '今天我需要做什么事情?',
// question: '任务5 完成了,帮我判断下一次运行时间应该是什么时候?',
// question: '任务59 完成了',
question: '我的多维表格配置'
}
// question: '我的多维表格配置'
// question: '记录一下,今天洗了澡',
// question: '编辑任务123进行补充, 对opencode进行描述介绍。',
// question: '编辑任务94对内容注释',
// question: '任务59和124完成了',
question: '任务 126 完成',
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,
}

View File

@@ -0,0 +1,27 @@
import fs from 'node:fs';
import path from 'node:path';
import fastGlob from 'fast-glob';
// 匹配所有 markdown 文件
const mds = await fastGlob('../prompts/*.md', {
cwd: __dirname,
absolute: false,
});
// 生成 JSON 数据
const jsonData = mds.map((filePath) => {
const fileName = path.basename(filePath, '.md');
const content = fs.readFileSync(path.join(__dirname, filePath), 'utf-8');
return {
title: fileName,
content: content,
};
});
// 输出 JSON
const outputPath = path.join(__dirname, '../prompts/markdown-files.json');
fs.writeFileSync(outputPath, JSON.stringify(jsonData, null, 2), 'utf-8');
console.log(`已生成 JSON 文件: ${outputPath}`);
console.log(`共找到 ${jsonData.length} 个 markdown 文件`);

View File

@@ -19,29 +19,29 @@
"@astrojs/mdx": "^4.3.12",
"@astrojs/react": "^4.4.2",
"@astrojs/sitemap": "^3.6.0",
"@kevisual/query": "^0.0.29",
"@kevisual/query": "^0.0.31",
"@kevisual/query-login": "^0.0.7",
"@kevisual/registry": "^0.0.1",
"@radix-ui/react-slot": "^1.2.4",
"@tailwindcss/vite": "^4.1.17",
"@uiw/react-md-editor": "^4.0.8",
"antd": "^6.0.0",
"astro": "^5.16.0",
"@uiw/react-md-editor": "^4.0.11",
"antd": "^6.0.1",
"astro": "^5.16.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.19",
"es-toolkit": "^1.42.0",
"github-markdown-css": "^5.8.1",
"highlight.js": "^11.11.1",
"lucide-react": "^0.554.0",
"lucide-react": "^0.556.0",
"marked": "^17.0.1",
"marked-highlight": "^2.2.3",
"nanoid": "^5.1.6",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"react-toastify": "^11.0.5",
"tailwind-merge": "^3.4.0",
"zustand": "^5.0.8"
"zustand": "^5.0.9"
},
"publishConfig": {
"access": "public"
@@ -54,7 +54,7 @@
"tailwindcss": "^4.1.17",
"tw-animate-css": "^1.4.0"
},
"packageManager": "pnpm@10.23.0",
"packageManager": "pnpm@10.24.0",
"onlyBuiltDependencies": [
"@tailwindcss/oxide",
"esbuild",

View File

@@ -1,6 +1,6 @@
{
"name": "@kevisual/noco-auto",
"version": "0.0.1",
"version": "0.0.2",
"description": "",
"main": "mod.ts",
"scripts": {
@@ -20,8 +20,8 @@
"dependencies": {
"@kevisual/context": "^0.0.4",
"@kevisual/noco": "^0.0.8",
"@kevisual/query": "^0.0.29",
"@kevisual/router": "^0.0.33",
"@kevisual/query": "^0.0.31",
"@kevisual/router": "^0.0.36",
"@kevisual/use-config": "^1.0.21"
},
"publishConfig": {

1214
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff