feat: 更新依赖项,添加 OpenCode 支持,重构代理和路由逻辑,新增 AGENTS 文档

This commit is contained in:
2026-01-20 02:46:29 +08:00
parent 26b4ffa3a2
commit 9f20e149a0
16 changed files with 531 additions and 32 deletions

View File

@@ -0,0 +1 @@
export * from "../../agent/plugin";

204
assistant/AGENTS.md Normal file
View File

@@ -0,0 +1,204 @@
# AGENTS.md
## 构建、代码检查和测试命令
### 核心命令
```bash
# 开发
bun run dev # 运行主入口 (src/run.ts)
bun run dev:server # 启动服务器(热重载)
bun run dev:cnb # 使用自定义配置目录启动服务器
bun run dev:share # 测试远程应用
# 构建
bun run build # 完整生产构建(先清理 dist
bun run build:lib # 构建库文件
bun run postbuild:lib # 生成 TypeScript 类型定义
# 包管理
pnpm install # 安装依赖(使用 pnpm v10.28.0
```
### 环境要求
- **Node.js**: >=22.0.0
- **运行时**: Bun开发环境/ Node.js生产环境
- **包管理器**: pnpm@10.28.0(强制要求)
### 环境变量
- `ASSISTANT_CONFIG_DIR`: 覆盖助手配置文件目录路径
- `BUN_PATH`: 覆盖 Bun 可执行文件路径
---
## 代码风格规范
### TypeScript
- **严格模式**: 通过扩展配置启用(`@kevisual/types/json/backend.json`
- **模块格式**: 仅 ESM`package.json` 中的 `"type": "module"`
- **目标**: NodeNext 模块解析
- **路径别名**: 使用 `@/*` 作为本地导入别名(如 `import { Foo } from '@/module/foo'`
### 导入规则
```typescript
// Node.js 内置模块必须使用 node: 协议
import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
// 外部包(不使用路径别名)
import chalk from 'chalk';
import { program } from 'commander';
// 本地模块(使用 @ 别名)
import { AssistantInit } from '@/services/init/index.ts';
// 类型导入
import type { AssistantConfig } from '@/module/assistant/index.ts';
```
### 命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 类 | PascalCase | `AssistantInit`, `AssistantApp`, `AssistantQuery` |
| 接口 | PascalCase | `ProxyInfo`, `AssistantInitOptions` |
| 类型 | PascalCase | `AssistantConfigData` |
| 函数 | camelCase | `getBunPath`, `checkFileExists`, `parseHomeArg` |
| 变量 | camelCase | `configDir`, `assistantConfig`, `isPortAvailable` |
| 私有字段 | `#` 前缀 + camelCase | `#query`, `#config` |
| 常量 | UPPER_SNAKE_CASE 或 camelCase | `commonPaths`, `randomId` |
| 文件名 | kebab-case | `proxy-page-index.ts`, `query-login.ts` |
| 目录名 | kebab-case | `query-login`, `hot-api` |
### 代码模式
#### 错误处理
```typescript
// 同步操作包装
try {
const result = fs.readFileSync(path);
return result;
} catch (error) {
console.error('读取文件错误:', error.message);
continue; // 或 return/throw
}
// 异步操作包装
try {
const result = await someAsyncOperation();
return result;
} catch (error) {
console.error('操作失败:', error.message);
process.exit(1); // 或 throw/return undefined
}
```
#### 上下文/依赖注入
使用 `@kevisual/use-config` 进行上下文管理:
```typescript
export const assistantConfig = useContextKey<AssistantInit>('assistantConfig', () => {
return new AssistantInit({
path: configDir,
init: isInit,
});
});
```
#### 路由模式
使用 `@kevisual/router` 的 App 和 SimpleRouter
```typescript
app.route({
path: 'router',
key: 'list',
description: '获取路由列表',
}).define(async (ctx) => {
ctx.body = { list };
}).addTo(app);
```
### JSDoc 文档
为公共 API、复杂函数和类型添加文档
```typescript
/**
* 助手初始化类
* @class AssistantInit
*/
export class AssistantInit extends AssistantConfig {}
/**
* @param {string} p - 要解析的路径
* @returns {string} 解析后的绝对路径
*/
export const w = (p: string) => path.join(__dirname, p);
```
### 文件组织
- **路由**: `src/routes/``src/routes-simple/`
- **模块**: `src/module/`(功能模块)
- **服务**: `src/services/`(公共服务)
- **命令**: `src/command/`CLI 命令)
- **查询**: `src/query/`API/查询逻辑)
- **入口文件**:
- `src/index.ts` - 主 CLI 入口
- `src/server.ts` - 服务器入口
- `src/run.ts` - 开发运行器
- `src/run-server.ts` - 服务器运行器
### 控制台输出
使用 `chalk` 实现彩色输出:
```typescript
console.log(chalk.blue('助手路径不存在,正在创建...'));
console.log(chalk.yellow('助手路径已存在'), chalk.green(assistantConfig.configDir));
console.error(chalk.red('启动服务器错误:'), error.message);
```
### 系统检测
```typescript
const isWindows = process.platform === 'win32';
const bunExecutableName = isWindows ? 'bun.exe' : 'bun';
```
### 端口处理
```typescript
import getPort, { portNumbers } from 'get-port';
const port = await getPort({ port: 51515 });
const availablePort = await getPort({ port: portNumbers(51515, 52000) });
```
### 关键依赖
- `@kevisual/router` - 应用路由
- `@kevisual/query` - API 查询
- `@kevisual/use-config` - 配置/上下文
- `commander` - CLI 参数解析
- `chalk` - 终端颜色
- `ws` - WebSocket通过 `@kevisual/ws`
- `pm2` - 进程管理(生产环境)
### 重要说明
1. 开发时始终使用 `bun`,但构建输出以 Node.js 为目标
2. 打包时外部包必须在 `bun.config.mjs``external` 数组中声明
3. 库构建排除 `pm2`
4.`ENVISION_*` 为前缀的环境变量在构建时可用
5. 项目在业务逻辑中广泛使用中文注释
6. 私有类成员应使用 `#` 前缀语法
7. 操作前始终使用 `checkFileExists` 检查文件是否存在

33
assistant/agent/call.ts Normal file
View File

@@ -0,0 +1,33 @@
import { createSkill } from '@kevisual/router'
import { app } from './index.ts'
import { tool } from '@opencode-ai/plugin/tool'
// "调用 path: router key: list"
app.route({
path: 'call',
key: '',
description: '调用',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'call-app',
title: '调用app应用',
summary: '调用router的应用, 参数path, key, payload',
args: {
path: tool.schema.string().describe('应用路径,例如 cnb'),
key: tool.schema.string().optional().describe('应用key例如 list-repos'),
payload: tool.schema.object({}).optional().describe('调用参数'),
}
})
},
}).define(async (ctx) => {
const { path, key = '' } = ctx.query;
if (!path) {
ctx.throw('路径path不能为空');
}
const res = await ctx.run({ path, key, payload: ctx.query.payload || {} }, {
...ctx
});
ctx.forward(res);
}).addTo(app)

3
assistant/agent/index.ts Normal file
View File

@@ -0,0 +1,3 @@
import { app } from '../src/main.ts'
export { app }

64
assistant/agent/plugin.ts Normal file
View File

@@ -0,0 +1,64 @@
import { tool } from "@opencode-ai/plugin/tool"
import { type Plugin } from "@opencode-ai/plugin"
import { app } from './index.ts';
import { Skill } from "@kevisual/router";
import './call.ts';
import './step.ts';
const routes = app.router.routes.filter(r => {
const metadata = r.metadata as Skill
if (metadata && metadata.tags && metadata.tags.includes('opencode')) {
return !!metadata.skill
}
return false
})
// opencode run "查看系统信息"
export const AgentPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
'tool': {
...routes.reduce((acc, route) => {
const metadata = route.metadata as Skill
acc[metadata.skill!] = {
name: metadata.title || metadata.skill,
description: metadata.summary || '',
args: metadata.args || {},
async execute(args: Record<string, any>) {
console.log(`Executing skill ${metadata.skill} with args:`, args);
await client.app.log({
body: {
service: 'cnb',
level: 'info',
message: `Executing skill ${metadata.skill} with args: ${JSON.stringify(args)}`
}
});
const res = await app.run({
path: route.path,
key: route.key,
payload: args
},
// @ts-ignore
{ appId: app.appId! });
if (res.code === 200) {
if (res.data?.content) {
return res.data.content;
}
const str = JSON.stringify(res.data || res, null, 2);
if (str.length > 5000) {
return str.slice(0, 5000) + '... (truncated)';
}
return str;
}
return `Error: ${res?.message || '无法获取结果'}`;
}
}
return acc;
}, {} as Record<string, any>)
},
'tool.execute.before': async (opts) => {
// console.log('CnbPlugin: tool.execute.before', opts.tool);
// delete toolSkills['cnb-login-verify']
}
}
}

27
assistant/agent/step.ts Normal file
View File

@@ -0,0 +1,27 @@
import { createSkill } from '@kevisual/router'
import { app } from './index.ts'
import { tool } from '@opencode-ai/plugin/tool'
// "调用 path: test key: step"
app.route({
path: 'test',
key: 'step',
description: '测试步骤调用',
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'test-step',
title: '获取系统信息',
summary: '根据步骤获取系统信息',
args: {
}
})
},
}).define(async (ctx) => {
ctx.body = {
context: `执行步骤
1. 调用 path: client key: system 获取系统信息
2. 调用 path: client key: time 获取当前时间
3. 返回结果`,
};
}).addTo(app)

View File

@@ -10,7 +10,7 @@
],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.28.0",
"packageManager": "pnpm@10.28.1",
"type": "module",
"files": [
"dist",
@@ -41,15 +41,16 @@
}
},
"devDependencies": {
"@kevisual/ai": "^0.0.20",
"@kevisual/ai": "^0.0.21",
"@kevisual/load": "^0.0.6",
"@kevisual/local-app-manager": "^0.1.32",
"@kevisual/logger": "^0.0.4",
"@kevisual/query": "0.0.35",
"@kevisual/query-login": "0.0.7",
"@kevisual/router": "^0.0.55",
"@kevisual/router": "^0.0.56",
"@kevisual/types": "^0.0.11",
"@kevisual/use-config": "^1.0.28",
"@opencode-ai/plugin": "^1.1.25",
"@types/bun": "^1.3.6",
"@types/lodash-es": "^4.17.12",
"@types/node": "^25.0.9",
@@ -80,7 +81,9 @@
"@kevisual/ha-api": "^0.0.6",
"@kevisual/oss": "^0.0.16",
"@kevisual/video-tools": "^0.0.13",
"eventemitter3": "^5.0.1",
"@opencode-ai/sdk": "^1.1.25",
"es-toolkit": "^1.44.0",
"eventemitter3": "^5.0.4",
"lowdb": "^7.0.1",
"lru-cache": "^11.2.4",
"pm2": "^6.0.14",

6
assistant/src/main.ts Normal file
View File

@@ -0,0 +1,6 @@
import { app, assistantConfig } from './app.ts';
import './routes/index.ts';
import './routes-simple/index.ts';
export { app, assistantConfig };

View File

@@ -9,8 +9,11 @@ import './user/index.ts';
// TODO: 移除
// import './hot-api/key-sender/index.ts';
import './opencode/index.ts';
import os from 'node:os';
import { authCache } from '@/module/cache/auth.ts';
import { createSkill } from '@kevisual/router';
const getTokenUser = async (token: string) => {
const query = assistantConfig.query
const res = await query.post({
@@ -101,6 +104,9 @@ app
description: '获取当前登录用户信息, 第一个登录的用户为管理员用户',
})
.define(async (ctx) => {
if (!ctx.query?.token && ctx.appId === app.appId) {
return;
}
const authResult = await checkAuth(ctx);
if (authResult.code !== 200) {
ctx.throw(authResult.code, authResult.message);
@@ -115,6 +121,9 @@ app
})
.define(async (ctx) => {
console.log('query', ctx.query);
if (!ctx.query?.token && ctx.appId === app.appId) {
return;
}
const authResult = await checkAuth(ctx, true);
if (authResult.code !== 200) {
ctx.throw(authResult.code, authResult.message);
@@ -152,6 +161,14 @@ app
path: 'client',
key: 'system',
description: '获取系统信息',
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'view-system-info',
title: '查看系统信息',
summary: '获取服务器操作系统平台、架构和版本信息',
})
}
})
.define(async (ctx) => {
const { platform, arch, release } = os;

View File

@@ -0,0 +1 @@
import './ls.ts'

View File

@@ -0,0 +1,40 @@
import { useKey } from "@kevisual/use-config";
import { app } from '@/app.ts'
import { createSkill } from "@kevisual/router";
import { opencodeManager } from './module/open.js'
app.route({
path: 'opencode',
key: 'create',
middleware: ['auth'],
description: '创建 OpenCode 客户端',
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'create-opencode-client',
title: '创建 OpenCode 客户端',
summary: '创建 OpenCode 客户端,如果存在则复用',
args: {
}
})
},
}).define(async (ctx) => {
const client = await opencodeManager.getClient();
ctx.body = { success: true, url: opencodeManager.url, message: 'OpenCode 客户端已就绪' };
}).addTo(app);
// 调用 path: opencode key: ls-projects
app.route({
path: 'opencode',
key: 'ls-projects'
}).define(async (ctx) => {
const client = await opencodeManager.getClient();
const projects = await client.project.list();
const currentProject = await client.project.current();
ctx.body = {
projects,
currentProject
};
}).addTo(app);

View File

@@ -0,0 +1,56 @@
import { createOpencode, OpencodeClient } from "@opencode-ai/sdk";
export class OpencodeManager {
private static instance: OpencodeManager | null = null;
private client: OpencodeClient | null = null;
private server: { url: string; close(): void } | null = null;
private isInitializing: boolean = false;
public url: string = '';
private constructor() {}
static getInstance(): OpencodeManager {
if (!OpencodeManager.instance) {
OpencodeManager.instance = new OpencodeManager();
}
return OpencodeManager.instance;
}
async getClient(): Promise<OpencodeClient> {
// 如果已经有 client直接返回
if (this.client) {
return this.client;
}
// 如果正在初始化,等待初始化完成
if (this.isInitializing) {
await new Promise(resolve => setTimeout(resolve, 100));
return this.getClient();
}
// 开始初始化
this.isInitializing = true;
try {
const result = await createOpencode({
hostname: '0.0.0.0',
});
console.log('OpencodeManager: OpenCode 服务已启动', result.server.url);
this.url = result.server.url;
this.client = result.client;
this.server = result.server;
return this.client;
} finally {
this.isInitializing = false;
}
}
close(): void {
if (this.server) {
this.server.close();
this.server = null;
}
this.client = null;
}
}
export const opencodeManager = OpencodeManager.getInstance();

View File

@@ -111,6 +111,10 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
logger.debug('handle by router', { url: req.url });
return;
}
if (pathname.startsWith('/router') || pathname.startsWith('/opencode')) {
logger.debug('handle by router (opencode/router)', { url: req.url });
return;
}
// client, api, v1, serve 开头的拦截
const apiProxy = _assistantConfig?.api?.proxy || [];
const defaultApiProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn');

View File

@@ -14,5 +14,6 @@
},
"include": [
"src/**/*",
"agent/**/*"
],
}