This commit is contained in:
2025-12-08 16:41:04 +08:00
commit 4e5f94c839
6 changed files with 230 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
.env
pnpm-lock.yaml

1
mod.ts Normal file
View File

@@ -0,0 +1 @@
export { App } from './src/app.ts';

28
package.json Normal file
View File

@@ -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 <xiongxiao@xiongxiao.me> (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"
}
}

2
readme.md Normal file
View File

@@ -0,0 +1,2 @@
# 应用层应用

161
src/app.ts Normal file
View File

@@ -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<Kevisual>(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
}
})
}
}

33
test/common.ts Normal file
View File

@@ -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);