init
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
.env
|
||||||
|
|
||||||
|
pnpm-lock.yaml
|
||||||
28
package.json
Normal file
28
package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
161
src/app.ts
Normal file
161
src/app.ts
Normal 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
33
test/common.ts
Normal 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);
|
||||||
Reference in New Issue
Block a user