From 4e5f94c839f650c119ead17375282396ffb36bd3 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Mon, 8 Dec 2025 16:41:04 +0800 Subject: [PATCH] init --- .gitignore | 5 ++ mod.ts | 1 + package.json | 28 +++++++++ readme.md | 2 + src/app.ts | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ test/common.ts | 33 ++++++++++ 6 files changed, 230 insertions(+) create mode 100644 .gitignore create mode 100644 mod.ts create mode 100644 package.json create mode 100644 readme.md create mode 100644 src/app.ts create mode 100644 test/common.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dab2e6e --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules + +.env + +pnpm-lock.yaml \ No newline at end of file diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..42751b7 --- /dev/null +++ b/mod.ts @@ -0,0 +1 @@ +export { App } from './src/app.ts'; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..495371c --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "@kevisual/app", + "version": "0.0.1", + "description": "", + "main": "index.js", + "scripts": { + "dev": "bun" + }, + "files": [ + "src" + ], + "publishConfig": { + "access": "public" + }, + "keywords": [], + "author": "abearxiong (https://www.xiongxiao.me)", + "license": "MIT", + "packageManager": "pnpm@10.14.0", + "type": "module", + "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", + "mitt": "^3.0.1" + } +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..a34fef7 --- /dev/null +++ b/readme.md @@ -0,0 +1,2 @@ +# 应用层应用 + diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..afa6edc --- /dev/null +++ b/src/app.ts @@ -0,0 +1,161 @@ +import { QueryRouterServer } from '@kevisual/router' +import { use } from '@kevisual/context' +import { Query } from '@kevisual/query' +import { Kevisual, BailianChat } from '@kevisual/ai' +import mitt from 'mitt'; +const isBrowser = typeof window !== 'undefined' +type AppOptions = { + router?: QueryRouterServer + query?: Query + queryOptions?: { url?: string } + token?: string + initAI?: boolean +} +export class App { + #router: QueryRouterServer + use = use; + query: Query; + #token = ''; + ai!: Kevisual; + loading = false; + emitter = mitt(); + constructor(opts?: AppOptions) { + this.#router = opts?.router || new QueryRouterServer() + const queryOptions = opts?.queryOptions || {} + this.query = opts?.query || new Query({ url: queryOptions.url || 'https://kevisual.cn/api/router' }) + const initAI = opts?.initAI ?? true; + if (opts?.token) { + this.#token = opts.token + if (initAI) { + this.loading = true; + this.initAI().finally(() => { + this.loading = false; + }); + } + } + + } + async initAI() { + try { + const config = await this.getConfig('ai.json'); + if (config.token) { + this.ai = new Kevisual(config); + } + } catch (e) { } + this.emitter.emit('ai-inited'); + } + async loadAI() { + if (!this.ai) { + const that = this; + return new Promise(resolve => { + const listen = () => { + resolve(that.ai); + this.emitter.off('ai-inited', listen); + } + this.emitter.on('ai-inited', listen); + }); + } + return this.ai; + } + get token() { + if (isBrowser && !this.#token) { + this.#token = localStorage.getItem('token') || '' + return this.#token; + } + return this.#token; + } + set token(value: string) { + this.#token = value; + } + get router() { + return this.#router; + } + set router(value: QueryRouterServer) { + this.#router = value; + } + async getConfig(key: string) { + if (isBrowser) { + const config = sessionStorage.getItem(`config_${key}`) + if (config) { + return Promise.resolve(JSON.parse(config)) + } + } + const res = await this.query.post({ + path: 'config', + key: 'get', + token: this.token, + data: { key } + }) + if (res.code !== 200) { + throw new Error(res.message || '获取配置失败') + } + const data = res.data || {} + const config = data.data || {} + if (isBrowser) { + sessionStorage.setItem(`config_${key}`, JSON.stringify(config)) + } + return config; + } + async chat(message: string) { + const routes = this.router.getList(); + let v = ''; + + if (routes.length > 0) { + const toolsList = routes.map((r, index) => + `${index + 1}. 工具名称: ${r.id}\n 描述: ${r.description}` + ).join('\n\n'); + + v = `你是一个 AI 助手,你可以使用以下工具来帮助用户完成任务: + +${toolsList} + +## 回复规则 +1. 如果用户的请求可以使用上述工具完成,请返回 JSON 格式数据 +2. 如果没有合适的工具,请直接分析并回答用户问题 + +## JSON 数据格式 +\`\`\`json +{ + "id": "工具的id", + "payload": { + // 工具所需的参数(如果需要) + // 例如: "id": "xxx", "name": "xxx" + } +} +\`\`\` + +注意: +- payload 中包含工具执行所需的所有参数 +- 如果工具不需要参数,payload 可以为空对象 {} +- 确保返回的 id 与上述工具列表中的工具名称完全匹配` + } + await this.ai.chat([ + { + role: 'system', + content: v + }, + { + role: 'user', + content: message + } + ]) + const json = this.ai.utils.extractJsonFromMarkdown(this.ai.responseText); + if (json.id) { + const callRes = await this.router.call(json) + const data = { + code: callRes.code, + data: callRes.body, + message: callRes.message + } + return Promise.resolve(data); + } + return Promise.resolve({ + code: 200, + data: { + id: 'ai_response', + description: 'AI 直接回复', + response: this.ai.responseText + } + }) + } +} \ No newline at end of file diff --git a/test/common.ts b/test/common.ts new file mode 100644 index 0000000..9d076ab --- /dev/null +++ b/test/common.ts @@ -0,0 +1,33 @@ +import { App } from '../src/app.ts'; +import { useConfig } from '@kevisual/use-config'; + +export const config = useConfig(); +export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +const time = Date.now(); +const app = new App({ token: config.KEVISUAL_TOKEN || '' }); + + +console.log('time to init app', Date.now() - time); + +await app.loadAI(); +console.log('app ai inited', Date.now() - time); +const router = app.router; +router.route({ + description: '获取天气,返回天气情况', +}).define(async ctx => { + ctx.body = '今天天气晴朗,气温25度,适合出行。'; +}).addTo(router) + +router.route({ + description: '获取新闻,返回最新新闻', +}).define(async (ctx) => { + ctx.body = '今天的头条新闻是:科技公司发布了最新的智能手机。'; +}).addTo(router) + +// await sleep(1000); + +const run = await app.chat('今天的新闻'); +console.log('run', run); + +console.log('all done', Date.now() - time); \ No newline at end of file