Compare commits
18 Commits
91d4fed474
...
ddfdb63598
| Author | SHA1 | Date | |
|---|---|---|---|
| ddfdb63598 | |||
| 18a6fd4cfe | |||
| 028a6ac726 | |||
| a911334459 | |||
| 3f899dbd5f | |||
| bb2b129343 | |||
|
|
2eeaf991b9 | ||
| ddba845ce7 | |||
| a4e04e7afa | |||
|
|
398c41a512 | ||
|
|
dbd044ec66 | ||
| 2b55c2bd03 | |||
| 89470346be | |||
| 9f20e149a0 | |||
| 26b4ffa3a2 | |||
| 43992d896f | |||
| 5e5f4f6543 | |||
| fee5076e16 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ pack-dist
|
|||||||
assistant-app
|
assistant-app
|
||||||
|
|
||||||
build
|
build
|
||||||
|
.pnpm-store
|
||||||
1
.opencode/plugin/agenat.ts
Normal file
1
.opencode/plugin/agenat.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { AgentPlugin } from "../../assistant/src/main.ts";
|
||||||
1
assistant/.opencode/plugin/agent.ts
Normal file
1
assistant/.opencode/plugin/agent.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { AgentPlugin } from "../../src/main.ts";
|
||||||
35
assistant/.opencode/skills/kill-opencode/SKILL.md
Normal file
35
assistant/.opencode/skills/kill-opencode/SKILL.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: kill-opencode
|
||||||
|
description: 自动查找并杀死所有opencode相关的进程,确保系统资源释放。
|
||||||
|
tags:
|
||||||
|
- opencode
|
||||||
|
- process-management
|
||||||
|
- automation
|
||||||
|
---
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# kill_opencode.sh - 自动杀死所有opencode进程
|
||||||
|
echo "正在查找opencode进程..."
|
||||||
|
ps aux | grep opencode | grep -v grep
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "正在杀死所有opencode进程..."
|
||||||
|
pkill -f opencode
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# 检查是否还有残留进程
|
||||||
|
remaining=$(ps aux | grep opencode | grep -v grep | wc -l)
|
||||||
|
if [ $remaining -gt 0 ]; then
|
||||||
|
echo "发现 $remaining 个顽固进程,使用强制杀死模式..."
|
||||||
|
pkill -9 -f opencode
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "opencode进程清理完成!"
|
||||||
|
else
|
||||||
|
echo "未找到opencode进程"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 验证是否已完全清理
|
||||||
|
echo "当前opencode进程状态:"
|
||||||
|
ps aux | grep opencode | grep -v grep || echo "没有运行中的opencode进程"
|
||||||
|
```
|
||||||
204
assistant/AGENTS.md
Normal file
204
assistant/AGENTS.md
Normal 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` 检查文件是否存在
|
||||||
@@ -40,6 +40,28 @@ await Bun.build({
|
|||||||
},
|
},
|
||||||
external,
|
external,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await Bun.build({
|
||||||
|
target: 'node',
|
||||||
|
format: 'esm',
|
||||||
|
entrypoints: [w('./src/main.ts')],
|
||||||
|
outdir: w('./dist'),
|
||||||
|
naming: {
|
||||||
|
entry: 'assistant-opencode.js',
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
ENVISION_VERSION: JSON.stringify(pkg.version),
|
||||||
|
},
|
||||||
|
external,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dts = 'dts -i src/main.ts -o assistant-opencode.d.ts';
|
||||||
|
Bun.spawnSync({
|
||||||
|
cmd: ['sh', '-c', dts],
|
||||||
|
cwd: __dirname,
|
||||||
|
stdout: 'inherit',
|
||||||
|
stderr: 'inherit',
|
||||||
|
});
|
||||||
// const copyDist = ['dist', 'bin'];
|
// const copyDist = ['dist', 'bin'];
|
||||||
const copyDist = ['dist'];
|
const copyDist = ['dist'];
|
||||||
export const copyFileToEnvision = async () => {
|
export const copyFileToEnvision = async () => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
],
|
],
|
||||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"packageManager": "pnpm@10.28.0",
|
"packageManager": "pnpm@10.28.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
@@ -41,15 +41,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kevisual/ai": "^0.0.20",
|
"@kevisual/ai": "^0.0.21",
|
||||||
|
"@kevisual/api": "^0.0.22",
|
||||||
"@kevisual/load": "^0.0.6",
|
"@kevisual/load": "^0.0.6",
|
||||||
"@kevisual/local-app-manager": "^0.1.32",
|
"@kevisual/local-app-manager": "^0.1.32",
|
||||||
"@kevisual/logger": "^0.0.4",
|
"@kevisual/logger": "^0.0.4",
|
||||||
"@kevisual/query": "0.0.35",
|
"@kevisual/query": "0.0.37",
|
||||||
"@kevisual/query-login": "0.0.7",
|
"@kevisual/query-login": "0.0.7",
|
||||||
"@kevisual/router": "^0.0.55",
|
"@kevisual/router": "^0.0.60",
|
||||||
"@kevisual/types": "^0.0.11",
|
"@kevisual/types": "^0.0.12",
|
||||||
"@kevisual/use-config": "^1.0.28",
|
"@kevisual/use-config": "^1.0.28",
|
||||||
|
"@opencode-ai/plugin": "^1.1.28",
|
||||||
"@types/bun": "^1.3.6",
|
"@types/bun": "^1.3.6",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^25.0.9",
|
"@types/node": "^25.0.9",
|
||||||
@@ -61,7 +63,7 @@
|
|||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.19",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"get-port": "^7.1.0",
|
"get-port": "^7.1.0",
|
||||||
"inquirer": "^13.2.0",
|
"inquirer": "^13.2.1",
|
||||||
"lodash-es": "^4.17.22",
|
"lodash-es": "^4.17.22",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"send": "^1.2.1",
|
"send": "^1.2.1",
|
||||||
@@ -76,11 +78,13 @@
|
|||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.971.0",
|
"@aws-sdk/client-s3": "^3.972.0",
|
||||||
"@kevisual/ha-api": "^0.0.6",
|
"@kevisual/ha-api": "^0.0.6",
|
||||||
"@kevisual/oss": "^0.0.16",
|
"@kevisual/oss": "^0.0.16",
|
||||||
"@kevisual/video-tools": "^0.0.13",
|
"@kevisual/video-tools": "^0.0.13",
|
||||||
"eventemitter3": "^5.0.1",
|
"@opencode-ai/sdk": "^1.1.28",
|
||||||
|
"es-toolkit": "^1.44.0",
|
||||||
|
"eventemitter3": "^5.0.4",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
"lru-cache": "^11.2.4",
|
"lru-cache": "^11.2.4",
|
||||||
"pm2": "^6.0.14",
|
"pm2": "^6.0.14",
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { App } from '@kevisual/router';
|
|||||||
import { SimpleRouter } from '@kevisual/router/simple'
|
import { SimpleRouter } from '@kevisual/router/simple'
|
||||||
// import { App } from '@kevisual/router/src/app.ts';
|
// import { App } from '@kevisual/router/src/app.ts';
|
||||||
// import { AssistantConfig } from '@/module/assistant/index.ts';
|
// import { AssistantConfig } from '@/module/assistant/index.ts';
|
||||||
|
|
||||||
import { AssistantInit, parseHomeArg } from '@/services/init/index.ts';
|
import { AssistantInit, parseHomeArg } from '@/services/init/index.ts';
|
||||||
import { configDir as HomeConfigDir } from '@/module/assistant/config/index.ts';
|
import { configDir as HomeConfigDir } from '@/module/assistant/config/index.ts';
|
||||||
import { useContextKey } from '@kevisual/use-config/context';
|
import { useContextKey } from '@kevisual/use-config/context';
|
||||||
import { AssistantQuery } from '@/module/assistant/query/index.ts';
|
import { AssistantQuery } from '@/module/assistant/query/index.ts';
|
||||||
|
|
||||||
const manualParse = parseHomeArg(HomeConfigDir);
|
const manualParse = parseHomeArg(HomeConfigDir);
|
||||||
const _configDir = manualParse.configDir;
|
const _configDir = manualParse.configDir;
|
||||||
export const configDir = AssistantInit.detectConfigDir(_configDir);
|
export const configDir = AssistantInit.detectConfigDir(_configDir);
|
||||||
@@ -14,6 +16,7 @@ export const assistantConfig = useContextKey<AssistantInit>('assistantConfig', (
|
|||||||
return new AssistantInit({
|
return new AssistantInit({
|
||||||
path: configDir,
|
path: configDir,
|
||||||
init: isInit,
|
init: isInit,
|
||||||
|
initWorkspace: manualParse.isOpencode ? false : true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,8 +31,6 @@ export const runtime = useContextKey('runtime', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const app: App = useContextKey<App>('app', () => {
|
export const app: App = useContextKey<App>('app', () => {
|
||||||
const init = isInit;
|
|
||||||
if (init) {
|
|
||||||
return new App({
|
return new App({
|
||||||
serverOptions: {
|
serverOptions: {
|
||||||
path: '/client/router',
|
path: '/client/router',
|
||||||
@@ -40,7 +41,6 @@ export const app: App = useContextKey<App>('app', () => {
|
|||||||
io: true
|
io: true
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const simpleRouter = useContextKey('simpleRouter', () => {
|
export const simpleRouter = useContextKey('simpleRouter', () => {
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import inquirer from 'inquirer';
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
type InitCommandOptions = {
|
type InitCommandOptions = {
|
||||||
path?: string;
|
workspace?: string;
|
||||||
};
|
};
|
||||||
const Init = new Command('init')
|
const Init = new Command('init')
|
||||||
.description('初始化一个助手客户端,生成配置文件。')
|
.description('初始化一个助手客户端,生成配置文件。')
|
||||||
.option('-p --path <path>', '助手路径,默认为执行命令的目录,如果助手路径不存在则创建。')
|
.option('-w --workspace <workspace>', '助手路径,默认为执行命令的目录,如果助手路径不存在则创建。')
|
||||||
.action((opts: InitCommandOptions) => {
|
.action((opts: InitCommandOptions) => {
|
||||||
// 如果path参数存在,检测path是否是相对路径,如果是相对路径,则转换为绝对路径
|
// 如果workspace参数存在,检测workspace是否是相对路径,如果是相对路径,则转换为绝对路径
|
||||||
if (opts.path && !opts.path.startsWith('/')) {
|
if (opts.workspace && !opts.workspace.startsWith('/')) {
|
||||||
opts.path = path.join(process.cwd(), opts.path);
|
opts.workspace = path.join(process.cwd(), opts.workspace);
|
||||||
} else if (opts.path) {
|
} else if (opts.workspace) {
|
||||||
opts.path = path.resolve(opts.path);
|
opts.workspace = path.resolve(opts.workspace);
|
||||||
}
|
}
|
||||||
const configDir = AssistantInit.detectConfigDir(opts.path);
|
const configDir = AssistantInit.detectConfigDir(opts.workspace);
|
||||||
console.log('configDir', configDir);
|
console.log('configDir', configDir);
|
||||||
const assistantInit = new AssistantInit({
|
const assistantInit = new AssistantInit({
|
||||||
path: configDir,
|
path: configDir,
|
||||||
|
|||||||
9
assistant/src/main.ts
Normal file
9
assistant/src/main.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { app } from './app.ts';
|
||||||
|
import { type Plugin } from "@opencode-ai/plugin"
|
||||||
|
import { createRouterAgentPluginFn } from '@kevisual/router/opencode';
|
||||||
|
import './routes/index.ts';
|
||||||
|
import './routes-simple/index.ts';
|
||||||
|
|
||||||
|
export const AgentPlugin: Plugin = createRouterAgentPluginFn({
|
||||||
|
router: app.router,
|
||||||
|
})
|
||||||
@@ -4,12 +4,13 @@ import fs from 'fs';
|
|||||||
import { checkFileExists, createDir } from '../file/index.ts';
|
import { checkFileExists, createDir } from '../file/index.ts';
|
||||||
import { ProxyInfo } from '../proxy/proxy.ts';
|
import { ProxyInfo } from '../proxy/proxy.ts';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
import { logger } from '@/module/logger.ts';
|
||||||
|
|
||||||
let kevisualDir = path.join(homedir(), 'kevisual');
|
let kevisualDir = path.join(homedir(), 'kevisual');
|
||||||
const envKevisualDir = process.env.ASSISTANT_CONFIG_DIR
|
const envKevisualDir = process.env.ASSISTANT_CONFIG_DIR
|
||||||
if (envKevisualDir) {
|
if (envKevisualDir) {
|
||||||
kevisualDir = envKevisualDir;
|
kevisualDir = envKevisualDir;
|
||||||
console.log('使用环境变量 ASSISTANT_CONFIG_DIR 作为 kevisual 目录:', kevisualDir);
|
logger.debug('使用环境变量 ASSISTANT_CONFIG_DIR 作为 kevisual 目录:', kevisualDir);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 助手配置文件路径, 全局配置文件目录
|
* 助手配置文件路径, 全局配置文件目录
|
||||||
@@ -93,6 +94,15 @@ export type AssistantConfigData = {
|
|||||||
* 例子: { path: '/root/home', target: 'https://kevisual.cn', pathname: '/root/home' }
|
* 例子: { path: '/root/home', target: 'https://kevisual.cn', pathname: '/root/home' }
|
||||||
*/
|
*/
|
||||||
proxy?: ProxyInfo[];
|
proxy?: ProxyInfo[];
|
||||||
|
/**
|
||||||
|
* Router代理, 会自动获取 {path: 'router', key: 'list'}的路由信息,然后注入到整个router应用当中.
|
||||||
|
* 例子: { proxy: [ { type: 'router', api: 'https://localhost:50002/api/router' } ] }
|
||||||
|
* base: 是否使用 /api/router的基础路径,默认false
|
||||||
|
*/
|
||||||
|
router?: {
|
||||||
|
proxy: ProxyInfo[];
|
||||||
|
base?: boolean;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* API 代理配置, 比如,api开头的,v1开头的等等
|
* API 代理配置, 比如,api开头的,v1开头的等等
|
||||||
*/
|
*/
|
||||||
@@ -191,10 +201,11 @@ export class AssistantConfig {
|
|||||||
}
|
}
|
||||||
return this.#configPath;
|
return this.#configPath;
|
||||||
}
|
}
|
||||||
init() {
|
init(configDir?: string) {
|
||||||
this.configPath = initConfig(this.configDir);
|
this.configPath = initConfig(configDir || this.configDir);
|
||||||
this.isMountedConfig = true;
|
this.isMountedConfig = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkMounted() {
|
checkMounted() {
|
||||||
if (!this.isMountedConfig) {
|
if (!this.isMountedConfig) {
|
||||||
this.init();
|
this.init();
|
||||||
@@ -356,7 +367,7 @@ type AppConfig = {
|
|||||||
list: any[];
|
list: any[];
|
||||||
};
|
};
|
||||||
export function parseArgs(args: string[]) {
|
export function parseArgs(args: string[]) {
|
||||||
const result: { root?: string; home?: boolean; help?: boolean } = {};
|
const result: { root?: string; home?: boolean; help?: boolean } = { home: true };
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
// 处理 root 参数
|
// 处理 root 参数
|
||||||
@@ -366,14 +377,13 @@ export function parseArgs(args: string[]) {
|
|||||||
i++; // 跳过下一个参数,因为它是值
|
i++; // 跳过下一个参数,因为它是值
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 处理 home 参数
|
|
||||||
// if (arg === '--home') {
|
|
||||||
result.home = true;
|
|
||||||
// }
|
|
||||||
if (arg === '--help' || arg === '-h') {
|
if (arg === '--help' || arg === '-h') {
|
||||||
result.help = true;
|
result.help = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (result.root) {
|
||||||
|
result.home = false;
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -383,6 +393,7 @@ export function parseArgs(args: string[]) {
|
|||||||
*/
|
*/
|
||||||
export const parseHomeArg = (homedir?: string) => {
|
export const parseHomeArg = (homedir?: string) => {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
const execPath = process.execPath;
|
||||||
const options = parseArgs(args);
|
const options = parseArgs(args);
|
||||||
let _configDir = undefined;
|
let _configDir = undefined;
|
||||||
if (options.home && homedir) {
|
if (options.home && homedir) {
|
||||||
@@ -390,7 +401,10 @@ export const parseHomeArg = (homedir?: string) => {
|
|||||||
} else if (options.root) {
|
} else if (options.root) {
|
||||||
_configDir = options.root;
|
_configDir = options.root;
|
||||||
}
|
}
|
||||||
|
const checkUrl = ['.opencode', 'bin/opencode', 'opencode.exe']
|
||||||
|
const isOpencode = checkUrl.some((item) => execPath.includes(item))
|
||||||
return {
|
return {
|
||||||
|
isOpencode,
|
||||||
options,
|
options,
|
||||||
configDir: _configDir,
|
configDir: _configDir,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export const checkFileDir = (filePath: string, create = true) => {
|
|||||||
return exist;
|
return exist;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDir = (dirPath: string) => {
|
export const createDir = (dirPath: string, isCreate = true) => {
|
||||||
|
if (!isCreate) return dirPath;
|
||||||
if (!checkFileExists(dirPath)) {
|
if (!checkFileExists(dirPath)) {
|
||||||
fs.mkdirSync(dirPath, { recursive: true });
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ import glob from 'fast-glob';
|
|||||||
import type { App } from '@kevisual/router';
|
import type { App } from '@kevisual/router';
|
||||||
import { RemoteApp } from '@/module/remote-app/remote-app.ts';
|
import { RemoteApp } from '@/module/remote-app/remote-app.ts';
|
||||||
import { logger } from '@/module/logger.ts';
|
import { logger } from '@/module/logger.ts';
|
||||||
|
import { getEnvToken } from '@/module/http-token.ts';
|
||||||
|
import { initApi } from '@kevisual/api/proxy'
|
||||||
|
import { Query } from '@kevisual/query';
|
||||||
export class AssistantApp extends Manager {
|
export class AssistantApp extends Manager {
|
||||||
config: AssistantConfig;
|
config: AssistantConfig;
|
||||||
pagesPath: string;
|
pagesPath: string;
|
||||||
remoteIsConnected = false;
|
remoteIsConnected = false;
|
||||||
attemptedConnectTimes = 0;
|
attemptedConnectTimes = 0;
|
||||||
remoteApp: RemoteApp | null = null;
|
remoteApp: RemoteApp | null = null;
|
||||||
|
remoteUrl: string | null = null;
|
||||||
constructor(config: AssistantConfig, mainApp?: App) {
|
constructor(config: AssistantConfig, mainApp?: App) {
|
||||||
config.checkMounted();
|
config.checkMounted();
|
||||||
const appsPath = config?.configPath?.appsDir || path.join(process.cwd(), 'apps');
|
const appsPath = config?.configPath?.appsDir || path.join(process.cwd(), 'apps');
|
||||||
@@ -71,11 +75,17 @@ export class AssistantApp extends Manager {
|
|||||||
return pagesParse;
|
return pagesParse;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initRemoteApp() {
|
async initRemoteApp(opts?: { token?: string, enabled?: boolean }) {
|
||||||
const config = this.config.getConfig();
|
const config = this.config.getConfig();
|
||||||
const share = config?.share;
|
const share = config?.share;
|
||||||
if (share && share.enabled !== false) {
|
const enabled = opts?.enabled ?? share?.enabled ?? false;
|
||||||
const token = config?.token;
|
if (share && enabled !== false) {
|
||||||
|
if (this.remoteApp) {
|
||||||
|
this.remoteApp.ws?.close();
|
||||||
|
this.remoteApp = null;
|
||||||
|
this.remoteIsConnected = false;
|
||||||
|
}
|
||||||
|
const token = config?.token || opts?.token || getEnvToken() as string;
|
||||||
const url = new URL(share.url || 'https://kevisual.cn/ws/proxy');
|
const url = new URL(share.url || 'https://kevisual.cn/ws/proxy');
|
||||||
const id = config?.app?.id;
|
const id = config?.app?.id;
|
||||||
if (token && url && id) {
|
if (token && url && id) {
|
||||||
@@ -99,12 +109,63 @@ export class AssistantApp extends Manager {
|
|||||||
}, 5 * 1000); // 第一次断开5秒后重连
|
}, 5 * 1000); // 第一次断开5秒后重连
|
||||||
});
|
});
|
||||||
logger.debug('链接到了远程应用服务器');
|
logger.debug('链接到了远程应用服务器');
|
||||||
|
const appId = id;
|
||||||
|
const username = config?.auth.username || 'unknown';
|
||||||
|
const url = new URL(`/${username}/v1/${appId}`, 'https://kevisual.cn/');
|
||||||
|
this.remoteUrl = url.toString();
|
||||||
|
console.log('远程地址', this.remoteUrl);
|
||||||
} else {
|
} else {
|
||||||
console.log('Not connected to remote app server');
|
console.log('Not connected to remote app server');
|
||||||
}
|
}
|
||||||
this.remoteApp = remoteApp;
|
this.remoteApp = remoteApp;
|
||||||
} else {
|
} else {
|
||||||
//
|
if (!token) {
|
||||||
|
logger.error('Token是远程应用连接必须的参数');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async initRouterApp() {
|
||||||
|
const config = this.config.getConfig();
|
||||||
|
const routerProxy = config?.router?.proxy || [];
|
||||||
|
const base = config.router?.base ?? false;
|
||||||
|
if (base) {
|
||||||
|
routerProxy.push({
|
||||||
|
type: 'router',
|
||||||
|
router: {
|
||||||
|
url: `${this.config.getRegistry()}/api/router`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (routerProxy.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (const proxyInfo of routerProxy) {
|
||||||
|
if (proxyInfo.type !== 'router') {
|
||||||
|
console.warn('路由的type必须是"router"');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const url = proxyInfo.router!.url;
|
||||||
|
if (!url) {
|
||||||
|
console.warn('路由的api地址不能为空', proxyInfo.router);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const query = new Query({ url });
|
||||||
|
try {
|
||||||
|
initApi({
|
||||||
|
router: this.mainApp,
|
||||||
|
item: {
|
||||||
|
type: 'api',
|
||||||
|
api: {
|
||||||
|
url,
|
||||||
|
query: query as any,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exclude: "WHERE path = 'auth' OR path = 'router' OR path = 'call'",
|
||||||
|
})
|
||||||
|
console.log('Router API 已初始化', url.toString());
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Router API 初始化失败', url.toString(), err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,10 +180,11 @@ export class AssistantApp extends Manager {
|
|||||||
remoteApp.listenProxy();
|
remoteApp.listenProxy();
|
||||||
this.attemptedConnectTimes = 0;
|
this.attemptedConnectTimes = 0;
|
||||||
console.log('重新连接到了远程应用服务器');
|
console.log('重新连接到了远程应用服务器');
|
||||||
|
this.reconnectRemoteApp();
|
||||||
} else {
|
} else {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.reconnectRemoteApp();
|
this.initRouterApp()
|
||||||
}, 30 * 1000); // 30秒后重连
|
}, 30 * 1000 + this.attemptedConnectTimes * 10 * 1000); // 30秒后重连 + 每次增加10秒
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,49 +5,39 @@ import path from 'node:path';
|
|||||||
import { ProxyInfo } from './proxy.ts';
|
import { ProxyInfo } from './proxy.ts';
|
||||||
import { checkFileExists } from '../file/index.ts';
|
import { checkFileExists } from '../file/index.ts';
|
||||||
import { log } from '@/module/logger.ts';
|
import { log } from '@/module/logger.ts';
|
||||||
|
import { pipeFileStream } from './pipe.ts';
|
||||||
|
import { getContentType } from './module/mime.ts';
|
||||||
|
|
||||||
export const fileProxy = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
export const fileProxy2 = (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
||||||
// url开头的文件
|
res.statusCode = 501;
|
||||||
const url = new URL(req.url, 'http://localhost');
|
const url = new URL(req.url, 'http://localhost');
|
||||||
const [user, key, _info] = url.pathname.split('/');
|
const { rootPath, indexPath = '' } = proxyApi?.file || {}
|
||||||
const pathname = url.pathname.slice(1);
|
if (!rootPath) {
|
||||||
const { indexPath = '', target = '', rootPath = process.cwd() } = proxyApi;
|
res.end(`系统未配置根路径 rootPath id:[${proxyApi?.file?.id}]`);
|
||||||
if (!indexPath) {
|
|
||||||
return res.end('Not Found indexPath');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// 检测文件是否存在,如果文件不存在,则返回404
|
|
||||||
let filePath = '';
|
|
||||||
let exist = false;
|
|
||||||
if (_info) {
|
|
||||||
filePath = path.join(rootPath, target, pathname);
|
|
||||||
exist = checkFileExists(filePath, true);
|
|
||||||
}
|
|
||||||
if (!exist) {
|
|
||||||
filePath = path.join(rootPath, target, indexPath);
|
|
||||||
exist = checkFileExists(filePath, true);
|
|
||||||
}
|
|
||||||
log.debug('filePath', { filePath, exist });
|
|
||||||
|
|
||||||
if (!exist) {
|
|
||||||
res.statusCode = 404;
|
|
||||||
res.end('Not Found File');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ext = path.extname(filePath);
|
const pathname = url.pathname;
|
||||||
let maxAge = 24 * 60 * 60 * 1000; // 24小时
|
let targetFilepath = pathname.replace(proxyApi.path || '', '');
|
||||||
if (ext === '.html') {
|
if (targetFilepath.endsWith('/')) {
|
||||||
maxAge = 0;
|
// 没有指定文件,访问index.html
|
||||||
|
targetFilepath += 'index.html';
|
||||||
}
|
}
|
||||||
let sendFilePath = path.relative(rootPath, filePath);
|
const filePath = path.join(rootPath || process.cwd(), targetFilepath);
|
||||||
const file = send(req, sendFilePath, {
|
const indexTargetPath = path.join(rootPath || process.cwd(), indexPath);
|
||||||
root: rootPath,
|
let sendPath = filePath;
|
||||||
maxAge,
|
if (!checkFileExists(filePath)) {
|
||||||
});
|
res.setHeader('X-Proxy-File', 'false');
|
||||||
file.pipe(res);
|
if (indexPath && checkFileExists(indexTargetPath)) {
|
||||||
} catch (error) {
|
sendPath = indexTargetPath;
|
||||||
|
} else {
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
res.end('Error:Not Found File');
|
res.end(`文件不存在, 路径: ${filePath}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
res.setHeader('X-Proxy-File', 'true');
|
||||||
|
}
|
||||||
|
const contentType = getContentType(sendPath);
|
||||||
|
res.setHeader('Content-Type', contentType);
|
||||||
|
pipeFileStream(sendPath, res);
|
||||||
};
|
};
|
||||||
50
assistant/src/module/assistant/proxy/module/mime.ts
Normal file
50
assistant/src/module/assistant/proxy/module/mime.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import path from 'path';
|
||||||
|
// 获取文件的 content-type
|
||||||
|
export const getContentType = (filePath: string) => {
|
||||||
|
const extname = path.extname(filePath);
|
||||||
|
const contentType = {
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.js': 'text/javascript; charset=utf-8',
|
||||||
|
'.mjs': 'text/javascript; charset=utf-8',
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.txt': 'text/plain; charset=utf-8',
|
||||||
|
'.json': 'application/json; charset=utf-8',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpg',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.wav': 'audio/wav',
|
||||||
|
'.mp4': 'video/mp4',
|
||||||
|
'.md': 'text/markdown; charset=utf-8', // utf-8配置
|
||||||
|
'.ico': 'image/x-icon', // Favicon 图标
|
||||||
|
'.webp': 'image/webp', // WebP 图像格式
|
||||||
|
'.webm': 'video/webm', // WebM 视频格式
|
||||||
|
'.ogg': 'audio/ogg', // Ogg 音频格式
|
||||||
|
'.mp3': 'audio/mpeg', // MP3 音频格式
|
||||||
|
'.m4a': 'audio/mp4', // M4A 音频格式
|
||||||
|
'.m3u8': 'application/vnd.apple.mpegurl', // HLS 播放列表
|
||||||
|
'.ts': 'video/mp2t', // MPEG Transport Stream
|
||||||
|
'.pdf': 'application/pdf', // PDF 文档
|
||||||
|
'.doc': 'application/msword', // Word 文档
|
||||||
|
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // Word 文档 (新版)
|
||||||
|
'.ppt': 'application/vnd.ms-powerpoint', // PowerPoint 演示文稿
|
||||||
|
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // PowerPoint (新版)
|
||||||
|
'.xls': 'application/vnd.ms-excel', // Excel 表格
|
||||||
|
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // Excel 表格 (新版)
|
||||||
|
'.csv': 'text/csv; charset=utf-8', // CSV 文件
|
||||||
|
'.xml': 'application/xml; charset=utf-8', // XML 文件
|
||||||
|
'.rtf': 'application/rtf', // RTF 文本文件
|
||||||
|
'.eot': 'application/vnd.ms-fontobject', // Embedded OpenType 字体
|
||||||
|
'.ttf': 'font/ttf', // TrueType 字体
|
||||||
|
'.woff': 'font/woff', // Web Open Font Format 1.0
|
||||||
|
'.woff2': 'font/woff2', // Web Open Font Format 2.0
|
||||||
|
'.otf': 'font/otf', // OpenType 字体
|
||||||
|
'.wasm': 'application/wasm', // WebAssembly 文件
|
||||||
|
'.pem': 'application/x-pem-file', // PEM 证书文件
|
||||||
|
'.crt': 'application/x-x509-ca-cert', // CRT 证书文件
|
||||||
|
'.yaml': 'application/x-yaml; charset=utf-8', // YAML 文件
|
||||||
|
'.yml': 'application/x-yaml; charset=utf-8', // YAML 文件(别名)
|
||||||
|
'.zip': 'application/octet-stream',
|
||||||
|
};
|
||||||
|
return contentType[extname] || 'application/octet-stream';
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import * as http from 'http';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { isBun } from './utils.ts';
|
import { isBun } from './utils.ts';
|
||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
import { logger } from '@/module/logger.ts';
|
||||||
/**
|
/**
|
||||||
* 文件流管道传输函数
|
* 文件流管道传输函数
|
||||||
* 将指定文件的内容通过流的方式传输给客户端响应
|
* 将指定文件的内容通过流的方式传输给客户端响应
|
||||||
@@ -99,7 +100,7 @@ export const pipeProxyReq = async (req: http.IncomingMessage, proxyReq: http.Cli
|
|||||||
proxyReq.end();
|
proxyReq.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log('Bun pipeProxyReq content-type', contentType);
|
logger.debug('Bun pipeProxyReq content-type', contentType);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const bodyString = req.body;
|
const bodyString = req.body;
|
||||||
bodyString && proxyReq.write(bodyString);
|
bodyString && proxyReq.write(bodyString);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import { httpProxy } from './http-proxy.ts';
|
import { httpProxy } from './http-proxy.ts';
|
||||||
import { s3Proxy } from './s3.ts';
|
import { s3Proxy } from './s3.ts';
|
||||||
|
import { fileProxy2 } from './file-proxy.ts';
|
||||||
export type ProxyInfo = {
|
export type ProxyInfo = {
|
||||||
/**
|
/**
|
||||||
* 代理路径, 比如/root/home, 匹配的路径
|
* 代理路径, 比如/root/home, 匹配的路径
|
||||||
@@ -13,7 +14,7 @@ export type ProxyInfo = {
|
|||||||
/**
|
/**
|
||||||
* 类型
|
* 类型
|
||||||
*/
|
*/
|
||||||
type?: 'file' | 'dynamic' | 'minio' | 'http' | 's3';
|
type?: 'file' | 'dynamic' | 'minio' | 'http' | 's3' | 'router';
|
||||||
/**
|
/**
|
||||||
* 目标的 pathname, 默认为请求的url.pathname, 设置了pathname,则会使用pathname作为请求的url.pathname
|
* 目标的 pathname, 默认为请求的url.pathname, 设置了pathname,则会使用pathname作为请求的url.pathname
|
||||||
* @default undefined
|
* @default undefined
|
||||||
@@ -25,16 +26,6 @@ export type ProxyInfo = {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
ws?: boolean;
|
ws?: boolean;
|
||||||
/**
|
|
||||||
* type为file时有效
|
|
||||||
* 索引文件,比如index.html, type为fileProxy代理有用 设置了索引文件,如果文件不存在,则访问索引文件
|
|
||||||
*/
|
|
||||||
indexPath?: string;
|
|
||||||
/**
|
|
||||||
* type为file时有效
|
|
||||||
* 根路径, 默认是process.cwd(), type为fileProxy代理有用,必须为绝对路径
|
|
||||||
*/
|
|
||||||
rootPath?: string;
|
|
||||||
s3?: {
|
s3?: {
|
||||||
/**
|
/**
|
||||||
* 如何id存在,使用assistan-config的storage配置
|
* 如何id存在,使用assistan-config的storage配置
|
||||||
@@ -45,6 +36,15 @@ export type ProxyInfo = {
|
|||||||
accessKeyId?: string;
|
accessKeyId?: string;
|
||||||
secretAccessKey?: string;
|
secretAccessKey?: string;
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
|
},
|
||||||
|
file?: {
|
||||||
|
id?: string;
|
||||||
|
indexPath?: string;
|
||||||
|
rootPath?: string;
|
||||||
|
},
|
||||||
|
router?: {
|
||||||
|
id?: string;
|
||||||
|
url?: string;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,4 +56,7 @@ export const proxy = (req: http.IncomingMessage, res: http.ServerResponse, proxy
|
|||||||
if (proxyApi.type === 's3') {
|
if (proxyApi.type === 's3') {
|
||||||
return s3Proxy(req, res, proxyApi);
|
return s3Proxy(req, res, proxyApi);
|
||||||
}
|
}
|
||||||
|
if (proxyApi.type === 'file') {
|
||||||
|
return fileProxy2(req, res, proxyApi);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -39,6 +39,14 @@ export const s3Proxy = async (req: http.IncomingMessage, res: http.ServerRespons
|
|||||||
if (objectPath.startsWith(s3.bucket + '/')) {
|
if (objectPath.startsWith(s3.bucket + '/')) {
|
||||||
objectPath = objectPath.replace(s3.bucket + '/', '');
|
objectPath = objectPath.replace(s3.bucket + '/', '');
|
||||||
}
|
}
|
||||||
|
if (objectPath.endsWith('/')) {
|
||||||
|
// 获取目录列表
|
||||||
|
res.statusCode = 200;
|
||||||
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
|
const list = await oss.listObjects(objectPath);
|
||||||
|
res.end(JSON.stringify(list, null, 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
oss.getObject(objectPath).then((response) => {
|
oss.getObject(objectPath).then((response) => {
|
||||||
if (!response.Body) {
|
if (!response.Body) {
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useKey } from '@kevisual/use-config';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
export const error = (msg: string, code = 500) => {
|
export const error = (msg: string, code = 500) => {
|
||||||
return JSON.stringify({ code, message: msg });
|
return JSON.stringify({ code, message: msg });
|
||||||
@@ -32,3 +33,7 @@ export const getToken = async (req: http.IncomingMessage) => {
|
|||||||
return { token };
|
return { token };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getEnvToken = () => {
|
||||||
|
const envTokne = useKey('KEVISUAL_TOKEN') || '';
|
||||||
|
return envTokne;
|
||||||
|
}
|
||||||
@@ -17,8 +17,11 @@ type ProxyType = {
|
|||||||
user: string;
|
user: string;
|
||||||
key: string;
|
key: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
type?: 'file';
|
||||||
|
file: {
|
||||||
indexPath: string;
|
indexPath: string;
|
||||||
absolutePath?: string;
|
absolutePath: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
export type LocalProxyOpts = {
|
export type LocalProxyOpts = {
|
||||||
watch?: boolean; // 是否监听文件变化
|
watch?: boolean; // 是否监听文件变化
|
||||||
@@ -79,8 +82,10 @@ export class LocalProxy {
|
|||||||
user: user,
|
user: user,
|
||||||
key: app,
|
key: app,
|
||||||
path: `/${user}/${app}/`,
|
path: `/${user}/${app}/`,
|
||||||
|
file: {
|
||||||
indexPath: `${user}/${app}/index.html`,
|
indexPath: `${user}/${app}/index.html`,
|
||||||
absolutePath: appPath,
|
absolutePath: appPath,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,15 +8,17 @@ export class UploadManager {
|
|||||||
}
|
}
|
||||||
mvFile(opts: {
|
mvFile(opts: {
|
||||||
temppath: string;
|
temppath: string;
|
||||||
type: 'file' | 's3';
|
type: 'file' | 's3' | 'app-file';
|
||||||
targetPath: string;
|
targetPath: string;
|
||||||
}) {
|
}) {
|
||||||
const { type, temppath, targetPath } = opts;
|
const { type = 'file', temppath, targetPath } = opts;
|
||||||
if (type === 'file') {
|
|
||||||
const pageDir = this.config.configPath?.pagesDir!;
|
const pageDir = this.config.configPath?.pagesDir!;
|
||||||
|
const appsDir = this.config.configPath?.appsDir!;
|
||||||
|
let dir = type === 'app-file' ? appsDir : pageDir;
|
||||||
|
if (type === 'file' || type === 'app-file') {
|
||||||
const fullTargetPath = targetPath.startsWith('/')
|
const fullTargetPath = targetPath.startsWith('/')
|
||||||
? targetPath
|
? targetPath
|
||||||
: path.join(pageDir, targetPath);
|
: path.join(dir, targetPath);
|
||||||
const targetDir = fullTargetPath.substring(0, fullTargetPath.lastIndexOf('/'));
|
const targetDir = fullTargetPath.substring(0, fullTargetPath.lastIndexOf('/'));
|
||||||
if (!fs.existsSync(targetDir)) {
|
if (!fs.existsSync(targetDir)) {
|
||||||
fs.mkdirSync(targetDir, { recursive: true });
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import { simpleRouter } from '@/app.ts';
|
import { simpleRouter } from '@/app.ts';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import { useContextKey } from '@kevisual/context';
|
import { useContextKey } from '@kevisual/context';
|
||||||
import { useFileStore } from '@kevisual/use-config';
|
|
||||||
export { simpleRouter };
|
export { simpleRouter };
|
||||||
|
import os from 'node:os';
|
||||||
export const cacheFilePath = useFileStore('cache-file', { needExists: true });
|
import path from 'node:path';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
/**
|
||||||
|
* 缓存文件路径
|
||||||
|
*/
|
||||||
|
const defaultCachePath = path.join(os.homedir(), '.envision', 'cache-file');
|
||||||
|
if (!fs.existsSync(defaultCachePath)) {
|
||||||
|
fs.mkdirSync(defaultCachePath, { recursive: true });
|
||||||
|
}
|
||||||
|
export const cacheFilePath = defaultCachePath;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 事件客户端
|
* 事件客户端
|
||||||
|
|||||||
32
assistant/src/routes/call/index.ts
Normal file
32
assistant/src/routes/call/index.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
import { app } from '../../app.ts';
|
||||||
|
import { createSkill, tool } from '@kevisual/router';
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -1,2 +1,37 @@
|
|||||||
import { LightHA } from "@kevisual/ha-api";
|
import { LightHA } from "@kevisual/ha-api";
|
||||||
export const lightHA = new LightHA({ token: process.env.HAAS_TOKEN || '', homeassistantURL: process.env.HAAS_URL });
|
export const lightHA = new LightHA({ token: process.env.HAAS_TOKEN || '', homeassistantURL: process.env.HAAS_URL });
|
||||||
|
|
||||||
|
export const callText = async (text: string) => {
|
||||||
|
const command = text?.trim().slice(0, 20);
|
||||||
|
type ParseCommand = {
|
||||||
|
type?: '打开' | '关闭',
|
||||||
|
appName?: string,
|
||||||
|
command?: string,
|
||||||
|
}
|
||||||
|
let obj: ParseCommand = {};
|
||||||
|
if (command.startsWith('打开')) {
|
||||||
|
obj.appName = command.replace('打开', '').trim();
|
||||||
|
obj.type = '打开';
|
||||||
|
} else if (command.startsWith('关闭')) {
|
||||||
|
obj.appName = command.replace('关闭', '').trim();
|
||||||
|
obj.type = '关闭';
|
||||||
|
}
|
||||||
|
let endTime = Date.now();
|
||||||
|
if (obj.type) {
|
||||||
|
try {
|
||||||
|
const search = await lightHA.searchLight(obj.appName || '');
|
||||||
|
console.log('searchTime', Date.now() - endTime);
|
||||||
|
if (search.id) {
|
||||||
|
await lightHA.runService({ entity_id: search.id, service: obj.type === '打开' ? 'turn_on' : 'turn_off' });
|
||||||
|
} else if (search.hasMore) {
|
||||||
|
const [first] = search.result;
|
||||||
|
await lightHA.runService({ entity_id: first.entity_id, service: obj.type === '打开' ? 'turn_on' : 'turn_off' });
|
||||||
|
} else {
|
||||||
|
console.log('未找到对应设备:', obj.appName);
|
||||||
|
}
|
||||||
|
console.log('解析到控制指令', obj);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('控制失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,17 @@ import './ai/index.ts';
|
|||||||
// TODO:
|
// TODO:
|
||||||
// import './light-code/index.ts';
|
// import './light-code/index.ts';
|
||||||
import './user/index.ts';
|
import './user/index.ts';
|
||||||
|
import './call/index.ts'
|
||||||
|
|
||||||
// TODO: 移除
|
// TODO: 移除
|
||||||
// import './hot-api/key-sender/index.ts';
|
// import './hot-api/key-sender/index.ts';
|
||||||
|
import './opencode/index.ts';
|
||||||
|
import './remote/index.ts';
|
||||||
|
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import { authCache } from '@/module/cache/auth.ts';
|
import { authCache } from '@/module/cache/auth.ts';
|
||||||
|
import { createSkill } from '@kevisual/router';
|
||||||
|
import { logger } from '@/module/logger.ts';
|
||||||
const getTokenUser = async (token: string) => {
|
const getTokenUser = async (token: string) => {
|
||||||
const query = assistantConfig.query
|
const query = assistantConfig.query
|
||||||
const res = await query.post({
|
const res = await query.post({
|
||||||
@@ -38,7 +43,7 @@ export const checkAuth = async (ctx: any, isAdmin = false) => {
|
|||||||
const config = assistantConfig.getConfig();
|
const config = assistantConfig.getConfig();
|
||||||
const { auth = {} } = config;
|
const { auth = {} } = config;
|
||||||
const token = ctx.query.token;
|
const token = ctx.query.token;
|
||||||
console.log('checkAuth', ctx.query, { token });
|
logger.debug('checkAuth', ctx.query, { token });
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return {
|
return {
|
||||||
code: 401,
|
code: 401,
|
||||||
@@ -101,6 +106,9 @@ app
|
|||||||
description: '获取当前登录用户信息, 第一个登录的用户为管理员用户',
|
description: '获取当前登录用户信息, 第一个登录的用户为管理员用户',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
|
if (!ctx.query?.token && ctx.appId === app.appId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const authResult = await checkAuth(ctx);
|
const authResult = await checkAuth(ctx);
|
||||||
if (authResult.code !== 200) {
|
if (authResult.code !== 200) {
|
||||||
ctx.throw(authResult.code, authResult.message);
|
ctx.throw(authResult.code, authResult.message);
|
||||||
@@ -114,7 +122,10 @@ app
|
|||||||
description: '管理员鉴权, 获取用户信息,并验证是否为管理员。',
|
description: '管理员鉴权, 获取用户信息,并验证是否为管理员。',
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
console.log('query', ctx.query);
|
logger.debug('query', ctx.query);
|
||||||
|
if (!ctx.query?.token && ctx.appId === app.appId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const authResult = await checkAuth(ctx, true);
|
const authResult = await checkAuth(ctx, true);
|
||||||
if (authResult.code !== 200) {
|
if (authResult.code !== 200) {
|
||||||
ctx.throw(authResult.code, authResult.message);
|
ctx.throw(authResult.code, authResult.message);
|
||||||
@@ -152,6 +163,14 @@ app
|
|||||||
path: 'client',
|
path: 'client',
|
||||||
key: 'system',
|
key: 'system',
|
||||||
description: '获取系统信息',
|
description: '获取系统信息',
|
||||||
|
metadata: {
|
||||||
|
tags: ['opencode'],
|
||||||
|
...createSkill({
|
||||||
|
skill: 'view-system-info',
|
||||||
|
title: '查看系统信息',
|
||||||
|
summary: '获取服务器操作系统平台、架构和版本信息',
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.define(async (ctx) => {
|
.define(async (ctx) => {
|
||||||
const { platform, arch, release } = os;
|
const { platform, arch, release } = os;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { runCode } from '../../module/light-code/run.ts'
|
|||||||
|
|
||||||
// http://localhost:4005/api/router?path=call
|
// http://localhost:4005/api/router?path=call
|
||||||
app.route({
|
app.route({
|
||||||
path: 'call',
|
path: 'light-code',
|
||||||
|
key: 'call',
|
||||||
// middleware: ['auth']
|
// middleware: ['auth']
|
||||||
}).define(async (ctx) => {
|
}).define(async (ctx) => {
|
||||||
const filename = ctx.query?.filename || 'root/light-code-demo/demo-router.ts'
|
const filename = ctx.query?.filename || 'root/light-code-demo/demo-router.ts'
|
||||||
|
|||||||
1
assistant/src/routes/opencode/index.ts
Normal file
1
assistant/src/routes/opencode/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import './ls.ts'
|
||||||
127
assistant/src/routes/opencode/ls.ts
Normal file
127
assistant/src/routes/opencode/ls.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { app } from '@/app.ts'
|
||||||
|
import { createSkill, tool } from "@kevisual/router";
|
||||||
|
import { opencodeManager } from './module/open.ts'
|
||||||
|
import path from "node:path";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { useKey } from '@kevisual/use-config';
|
||||||
|
|
||||||
|
// 创建一个opencode 客户端
|
||||||
|
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 = { content: `${opencodeManager.url} OpenCode 客户端已就绪` };
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 关闭 opencode 客户端
|
||||||
|
app.route({
|
||||||
|
path: 'opencode',
|
||||||
|
key: 'close',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '关闭 OpenCode 客户端',
|
||||||
|
metadata: {
|
||||||
|
tags: ['opencode'],
|
||||||
|
...createSkill({
|
||||||
|
skill: 'close-opencode-client',
|
||||||
|
title: '关闭 OpenCode 客户端',
|
||||||
|
summary: '关闭 OpenCode 客户端',
|
||||||
|
args: {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
await opencodeManager.close();
|
||||||
|
ctx.body = { content: 'OpenCode 客户端已关闭' };
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
// 调用 path: opencode key: getUrl
|
||||||
|
app.route({
|
||||||
|
path: 'opencode',
|
||||||
|
key: 'getUrl',
|
||||||
|
middleware: ['auth'],
|
||||||
|
description: '获取 OpenCode 服务 URL',
|
||||||
|
metadata: {
|
||||||
|
tags: ['opencode'],
|
||||||
|
...createSkill({
|
||||||
|
skill: 'get-opencode-url',
|
||||||
|
title: '获取 OpenCode 服务 URL',
|
||||||
|
summary: '获取当前 OpenCode 服务的 URL 地址',
|
||||||
|
args: {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const url = await opencodeManager.getUrl();
|
||||||
|
const cnbURL = useKey('CNB_VSCODE_PROXY_URI') as string | undefined;
|
||||||
|
let content = `本地访问地址: ${url}`
|
||||||
|
if (cnbURL) {
|
||||||
|
content += `\n云端访问地址: ${cnbURL.replace('{{port}}', '5000')}`;
|
||||||
|
}
|
||||||
|
ctx.body = { content };
|
||||||
|
}).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);
|
||||||
|
|
||||||
|
// 调用 path: opencode key: runProject 参数 /home/ubuntu/cli/assistant
|
||||||
|
app.route({
|
||||||
|
path: 'opencode',
|
||||||
|
key: 'runProject',
|
||||||
|
middleware: ['auth'],
|
||||||
|
metadata: {
|
||||||
|
tags: ['opencode'],
|
||||||
|
...createSkill({
|
||||||
|
skill: 'run-opencode-project',
|
||||||
|
title: '运行 OpenCode 项目',
|
||||||
|
summary: '运行一个已有的 OpenCode 项目',
|
||||||
|
args: {
|
||||||
|
projectPath: tool.schema.string().optional().describe('OpenCode 项目的路径, 默认为 /workspace')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const { projectPath = '/workspace' } = ctx.query;
|
||||||
|
try {
|
||||||
|
|
||||||
|
// const directory = path.resolve(projectPath);
|
||||||
|
// const runOpencodeCli = 'opencode run hello';
|
||||||
|
// execSync(runOpencodeCli, { cwd: directory, stdio: 'inherit' });
|
||||||
|
// ctx.body = { content: `OpenCode 项目已在路径 ${directory} 运行` };
|
||||||
|
const client = await opencodeManager.getClient();
|
||||||
|
const session = await client.session.create({
|
||||||
|
query: {
|
||||||
|
directory: projectPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('Created session:', session.data.id);
|
||||||
|
ctx.body = { content: `OpenCode 项目已在路径 ${projectPath} 运行` };
|
||||||
|
} catch (error) {
|
||||||
|
ctx.body = { content: `运行 OpenCode 项目失败, 请手动运行命令初始化: opencode run hello` };
|
||||||
|
}
|
||||||
|
}).addTo(app);
|
||||||
127
assistant/src/routes/opencode/module/open.ts
Normal file
127
assistant/src/routes/opencode/module/open.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { createOpencode, createOpencodeClient, OpencodeClient, } from "@opencode-ai/sdk";
|
||||||
|
import { randomInt } from "es-toolkit";
|
||||||
|
import getPort from "get-port";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
private currentPort: number | null = null;
|
||||||
|
|
||||||
|
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 port = 5000;
|
||||||
|
const currentPort = await getPort({ port: port });
|
||||||
|
if (port === currentPort) {
|
||||||
|
const result = await createOpencode({
|
||||||
|
hostname: '0.0.0.0',
|
||||||
|
port: port
|
||||||
|
});
|
||||||
|
this.url = result.server.url;
|
||||||
|
this.client = result.client;
|
||||||
|
this.server = result.server;
|
||||||
|
return this.client;
|
||||||
|
} else {
|
||||||
|
this.client = await this.createOpencodeProject({ port });
|
||||||
|
this.url = `http://localhost:${port}`;
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isInitializing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async createOpencodeProject({
|
||||||
|
directory,
|
||||||
|
port = 5000
|
||||||
|
}: { directory?: string, port?: number }): Promise<OpencodeClient> {
|
||||||
|
const client = createOpencodeClient({
|
||||||
|
baseUrl: `http://localhost:${port}`,
|
||||||
|
directory
|
||||||
|
});
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
async killPort(port: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 尝试 使用命令行去关闭 port为5000的服务
|
||||||
|
if (os.platform() === 'win32') {
|
||||||
|
// Windows 平台
|
||||||
|
execSync(`netstat -ano | findstr :${port} | findstr LISTENING`).toString().split('\n').forEach(line => {
|
||||||
|
const parts = line.trim().split(/\s+/);
|
||||||
|
const pid = parts[parts.length - 1];
|
||||||
|
if (pid) {
|
||||||
|
execSync(`taskkill /PID ${pid} /F`);
|
||||||
|
console.log(`OpencodeManager: 已关闭占用端口 ${port} 的进程 PID ${pid}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Unix-like 平台
|
||||||
|
const result = execSync(`lsof -i :${port} -t`).toString();
|
||||||
|
result.split('\n').forEach(pid => {
|
||||||
|
if (pid) {
|
||||||
|
execSync(`kill -9 ${pid}`);
|
||||||
|
console.log(`OpencodeManager: 已关闭占用端口 ${port} 的进程 PID ${pid}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to close OpenCode server:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async close(): Promise<void> {
|
||||||
|
if (this.server) {
|
||||||
|
this.server.close();
|
||||||
|
this.server = null;
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const port = 5000;
|
||||||
|
const currentPort = await getPort({ port: port });
|
||||||
|
if (port === currentPort) {
|
||||||
|
this.client = null;
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
await this.killPort(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = null;
|
||||||
|
}
|
||||||
|
async getUrl(): Promise<string> {
|
||||||
|
if (this.url) {
|
||||||
|
return this.url;
|
||||||
|
}
|
||||||
|
if (!this.url) {
|
||||||
|
await this.getClient();
|
||||||
|
}
|
||||||
|
return 'http://localhost:5000';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const opencodeManager = OpencodeManager.getInstance();
|
||||||
54
assistant/src/routes/remote/index.ts
Normal file
54
assistant/src/routes/remote/index.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { useContextKey } from "@kevisual/context";
|
||||||
|
import { app } from "../../app.ts";
|
||||||
|
import { AssistantApp } from "@/lib.ts";
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'remote',
|
||||||
|
key: 'status',
|
||||||
|
middleware: ['admin-auth'],
|
||||||
|
description: '获取远程app连接状态',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const manager = useContextKey('manager') as AssistantApp;
|
||||||
|
if (manager?.remoteApp?.isConnect()) {
|
||||||
|
const url = manager.remoteUrl || ''
|
||||||
|
ctx.body = {
|
||||||
|
content: `远程app已经链接, 访问地址:${url}`,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.body = {
|
||||||
|
content: '远程app未连接',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).addTo(app);
|
||||||
|
|
||||||
|
app.route({
|
||||||
|
path: 'remote',
|
||||||
|
key: 'connect',
|
||||||
|
middleware: ['admin-auth'],
|
||||||
|
description: '连接远程app',
|
||||||
|
}).define(async (ctx) => {
|
||||||
|
const manager = useContextKey('manager') as AssistantApp;
|
||||||
|
if (!manager) {
|
||||||
|
ctx.body = {
|
||||||
|
content: '远程app管理器未初始化',
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (manager?.remoteApp?.isConnect()) {
|
||||||
|
const url = manager.remoteUrl || ''
|
||||||
|
ctx.body = {
|
||||||
|
content: `远程app已经链接, 访问地址:${url}`,
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await manager.initRemoteApp({ enabled: true, token: ctx.query?.token }).then(() => {
|
||||||
|
ctx.body = {
|
||||||
|
content: '远程app连接成功',
|
||||||
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
ctx.body = {
|
||||||
|
content: `远程app连接失败: ${err.message}`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}).addTo(app);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useContextKey } from '@kevisual/context';
|
||||||
import { app, assistantConfig } from './app.ts';
|
import { app, assistantConfig } from './app.ts';
|
||||||
import { proxyRoute, proxyWs } from './services/proxy/proxy-page-index.ts';
|
import { proxyRoute, proxyWs } from './services/proxy/proxy-page-index.ts';
|
||||||
import './routes/index.ts';
|
import './routes/index.ts';
|
||||||
@@ -8,7 +9,7 @@ import { program } from 'commander';
|
|||||||
import { spawnSync } from 'node:child_process';
|
import { spawnSync } from 'node:child_process';
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { AssistantApp } from './lib.ts';
|
import { AssistantApp, checkFileExists } from './lib.ts';
|
||||||
import { getBunPath } from './module/get-bun-path.ts';
|
import { getBunPath } from './module/get-bun-path.ts';
|
||||||
import { qwenAsr } from './services/asr/qwen-asr.ts';
|
import { qwenAsr } from './services/asr/qwen-asr.ts';
|
||||||
export const runServer = async (port: number = 51515, listenPath = '127.0.0.1') => {
|
export const runServer = async (port: number = 51515, listenPath = '127.0.0.1') => {
|
||||||
@@ -50,12 +51,13 @@ export const runServer = async (port: number = 51515, listenPath = '127.0.0.1')
|
|||||||
...proxyWs(),
|
...proxyWs(),
|
||||||
qwenAsr,
|
qwenAsr,
|
||||||
]);
|
]);
|
||||||
const manager = new AssistantApp(assistantConfig, app);
|
const manager = useContextKey('manager', new AssistantApp(assistantConfig, app));
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
manager.load({ runtime: 'client' }).then(() => {
|
manager.load({ runtime: 'client' }).then(() => {
|
||||||
console.log('Assistant App Loaded');
|
console.log('Assistant App Loaded');
|
||||||
});
|
});
|
||||||
manager.initRemoteApp()
|
manager.initRemoteApp()
|
||||||
|
manager.initRouterApp()
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -69,6 +71,8 @@ program
|
|||||||
.option('-n, --name <name>', '服务名称', 'assistant-server')
|
.option('-n, --name <name>', '服务名称', 'assistant-server')
|
||||||
.option('-p, --port <port>', '服务端口')
|
.option('-p, --port <port>', '服务端口')
|
||||||
.option('-s, --start', '是否启动服务')
|
.option('-s, --start', '是否启动服务')
|
||||||
|
.option('-r, --root <root>', '工作空间路径')
|
||||||
|
.option('-i, --input <input>', '启动的输入文件,例如/workspace/src/main.ts')
|
||||||
.option('-e, --interpreter <interpreter>', '指定使用的解释器', 'bun')
|
.option('-e, --interpreter <interpreter>', '指定使用的解释器', 'bun')
|
||||||
.action(async (options) => {
|
.action(async (options) => {
|
||||||
// console.log('当前执行路径:', execPath, inte);
|
// console.log('当前执行路径:', execPath, inte);
|
||||||
@@ -99,7 +103,12 @@ program
|
|||||||
if (port) {
|
if (port) {
|
||||||
pm2Command += ` -p ${port}`;
|
pm2Command += ` -p ${port}`;
|
||||||
}
|
}
|
||||||
|
if (options.root) {
|
||||||
|
pm2Command += ` --root ${options.root}`;
|
||||||
|
}
|
||||||
|
if (options.input) {
|
||||||
|
pm2Command += ` --input ${options.input}`;
|
||||||
|
}
|
||||||
console.log(chalk.gray('执行命令:'), pm2Command);
|
console.log(chalk.gray('执行命令:'), pm2Command);
|
||||||
console.log(chalk.gray('脚本路径:'), runPath);
|
console.log(chalk.gray('脚本路径:'), runPath);
|
||||||
|
|
||||||
@@ -146,6 +155,12 @@ program
|
|||||||
const listenPort = parseInt(options.port || config?.server?.port);
|
const listenPort = parseInt(options.port || config?.server?.port);
|
||||||
const listenPath = config?.server?.path || '::';
|
const listenPath = config?.server?.path || '::';
|
||||||
const server = await runServer(listenPort, listenPath);
|
const server = await runServer(listenPort, listenPath);
|
||||||
|
if (options.input) {
|
||||||
|
const _input = path.resolve(options.input);
|
||||||
|
if (checkFileExists(_input)) {
|
||||||
|
await import(_input);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('请使用 -s 参数启动服务');
|
console.log('请使用 -s 参数启动服务');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { QwenAsrRelatime } from "@kevisual/video-tools/src/asr/index.ts";
|
import { QwenAsrRelatime } from "@kevisual/video-tools/src/asr/index.ts";
|
||||||
|
|
||||||
import { Listener, WebSocketListenerFun, WebSocketReq } from "@kevisual/router";
|
import { Listener, WebSocketListenerFun, WebSocketReq } from "@kevisual/router";
|
||||||
import { lightHA } from "@/routes/ha-api/ha.ts";
|
import { callText } from "@/routes/ha-api/ha.ts";
|
||||||
import { assistantConfig } from "@/app.ts";
|
import { assistantConfig } from "@/app.ts";
|
||||||
|
|
||||||
const func: WebSocketListenerFun = async (req: WebSocketReq<{ asr: QwenAsrRelatime, msgId: string, startTime?: number, loading?: boolean }>, res) => {
|
const func: WebSocketListenerFun = async (req: WebSocketReq<{ asr: QwenAsrRelatime, msgId: string, startTime?: number, loading?: boolean }>, res) => {
|
||||||
@@ -61,37 +61,7 @@ const func: WebSocketListenerFun = async (req: WebSocketReq<{ asr: QwenAsrRelati
|
|||||||
text,
|
text,
|
||||||
}));
|
}));
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
const command = text?.trim().slice(0, 20);
|
await callText(text);
|
||||||
type ParseCommand = {
|
|
||||||
type?: '打开' | '关闭',
|
|
||||||
appName?: string,
|
|
||||||
command?: string,
|
|
||||||
}
|
|
||||||
let obj: ParseCommand = {};
|
|
||||||
if (command.startsWith('打开')) {
|
|
||||||
obj.appName = command.replace('打开', '').trim();
|
|
||||||
obj.type = '打开';
|
|
||||||
} else if (command.startsWith('关闭')) {
|
|
||||||
obj.appName = command.replace('关闭', '').trim();
|
|
||||||
obj.type = '关闭';
|
|
||||||
}
|
|
||||||
if (obj.type) {
|
|
||||||
try {
|
|
||||||
const search = await lightHA.searchLight(obj.appName || '');
|
|
||||||
console.log('searchTime', Date.now() - endTime);
|
|
||||||
if (search.id) {
|
|
||||||
await lightHA.runService({ entity_id: search.id, service: obj.type === '打开' ? 'turn_on' : 'turn_off' });
|
|
||||||
} else if (search.hasMore) {
|
|
||||||
const [first] = search.result;
|
|
||||||
await lightHA.runService({ entity_id: first.entity_id, service: obj.type === '打开' ? 'turn_on' : 'turn_off' });
|
|
||||||
} else {
|
|
||||||
console.log('未找到对应设备:', obj.appName);
|
|
||||||
}
|
|
||||||
console.log('解析到控制指令', obj);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('控制失败', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('toogle light time', Date.now() - endTime);
|
console.log('toogle light time', Date.now() - endTime);
|
||||||
});
|
});
|
||||||
asr.start();
|
asr.start();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export { parseHomeArg, parseHelpArg };
|
|||||||
export type AssistantInitOptions = {
|
export type AssistantInitOptions = {
|
||||||
path?: string;
|
path?: string;
|
||||||
init?: boolean;
|
init?: boolean;
|
||||||
|
initWorkspace?: boolean;
|
||||||
};
|
};
|
||||||
const randomId = () => Math.random().toString(36).substring(2, 8);
|
const randomId = () => Math.random().toString(36).substring(2, 8);
|
||||||
/**
|
/**
|
||||||
@@ -16,24 +17,31 @@ const randomId = () => Math.random().toString(36).substring(2, 8);
|
|||||||
*/
|
*/
|
||||||
export class AssistantInit extends AssistantConfig {
|
export class AssistantInit extends AssistantConfig {
|
||||||
#query: Query
|
#query: Query
|
||||||
|
initWorkspace: boolean = false;
|
||||||
constructor(opts?: AssistantInitOptions) {
|
constructor(opts?: AssistantInitOptions) {
|
||||||
const configDir = opts?.path || process.cwd();
|
const configDir = opts?.path || process.cwd();
|
||||||
super({
|
super({
|
||||||
configDir,
|
configDir,
|
||||||
init: false,
|
init: false,
|
||||||
});
|
});
|
||||||
|
this.initWorkspace = opts?.initWorkspace ?? true;
|
||||||
if (opts?.init) {
|
if (opts?.init) {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init(configDir?: string) {
|
||||||
|
if (configDir) {
|
||||||
|
this.configDir = configDir;
|
||||||
|
}
|
||||||
// 1. 检查助手路径是否存在
|
// 1. 检查助手路径是否存在
|
||||||
if (!this.checkConfigPath()) {
|
if (!this.checkConfigPath()) {
|
||||||
|
super.init(configDir);
|
||||||
|
if (!this.initWorkspace) { return }
|
||||||
console.log(chalk.blue('助手路径不存在,正在创建...'));
|
console.log(chalk.blue('助手路径不存在,正在创建...'));
|
||||||
super.init();
|
|
||||||
} else {
|
} else {
|
||||||
super.init();
|
super.init(configDir);
|
||||||
|
if (!this.initWorkspace) { return }
|
||||||
const assistantConfig = this;
|
const assistantConfig = this;
|
||||||
console.log(chalk.yellow('助手路径已存在'), chalk.green(assistantConfig.configDir));
|
console.log(chalk.yellow('助手路径已存在'), chalk.green(assistantConfig.configDir));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { fileProxy, httpProxy, createApiProxy, ProxyInfo, proxy } from '@/module/assistant/index.ts';
|
import { createApiProxy, ProxyInfo, proxy } from '@/module/assistant/index.ts';
|
||||||
import http from 'node:http';
|
import http from 'node:http';
|
||||||
import { LocalProxy } from './local-proxy.ts';
|
import { LocalProxy } from './local-proxy.ts';
|
||||||
import { assistantConfig, app, simpleRouter } from '@/app.ts';
|
import { assistantConfig, simpleRouter } from '@/app.ts';
|
||||||
import { log, logger } from '@/module/logger.ts';
|
import { log, logger } from '@/module/logger.ts';
|
||||||
import { getToken } from '@/module/http-token.ts';
|
import { getToken } from '@/module/http-token.ts';
|
||||||
import { getTokenUserCache } from '@/routes/index.ts';
|
import { getTokenUserCache } from '@/routes/index.ts';
|
||||||
@@ -12,7 +12,7 @@ const localProxy = new LocalProxy({});
|
|||||||
localProxy.initFromAssistantConfig(assistantConfig);
|
localProxy.initFromAssistantConfig(assistantConfig);
|
||||||
|
|
||||||
const isOpenPath = (pathname: string): boolean => {
|
const isOpenPath = (pathname: string): boolean => {
|
||||||
const openPaths = ['/root/home', '/root/cli'];
|
const openPaths = ['/root/home', '/root/cli', '/root/login'];
|
||||||
for (const openPath of openPaths) {
|
for (const openPath of openPaths) {
|
||||||
if (pathname.startsWith(openPath)) {
|
if (pathname.startsWith(openPath)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -31,7 +31,7 @@ const authFilter = async (req: http.IncomingMessage, res: http.ServerResponse) =
|
|||||||
const auth = _assistantConfig?.auth || {};
|
const auth = _assistantConfig?.auth || {};
|
||||||
const share = auth.share || 'protected';
|
const share = auth.share || 'protected';
|
||||||
const noAdmin = !auth.username;
|
const noAdmin = !auth.username;
|
||||||
if (noAdmin) return { code: 500, message: '没有管理员' };
|
if (noAdmin) return { code: 200, message: '没有管理员, 直接放过, 让管理登录和自己设置' };
|
||||||
const admin = auth.username;
|
const admin = auth.username;
|
||||||
const admins = auth.admin || [];
|
const admins = auth.admin || [];
|
||||||
if (admin) {
|
if (admin) {
|
||||||
@@ -48,10 +48,10 @@ const authFilter = async (req: http.IncomingMessage, res: http.ServerResponse) =
|
|||||||
return { code: 200, message: '允许访问首页' };
|
return { code: 200, message: '允许访问首页' };
|
||||||
}
|
}
|
||||||
// 放开api, 以 /api, /v1, /client, /serve 开头的请求
|
// 放开api, 以 /api, /v1, /client, /serve 开头的请求
|
||||||
const openApiPaths = ['/api', '/v1', '/client', '/serve', '/proxy'];
|
const openApiPaths = ['/api', '/v1', '/client', '/serve', '/proxy', '/root'];
|
||||||
for (const openPath of openApiPaths) {
|
for (const openPath of openApiPaths) {
|
||||||
if (pathname.startsWith(openPath)) {
|
if (pathname.startsWith(openPath)) {
|
||||||
return { code: 200, message: '允许访问API' };
|
return { code: 200, message: '允许公共访问模块' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (share === 'public') {
|
if (share === 'public') {
|
||||||
@@ -65,7 +65,6 @@ const authFilter = async (req: http.IncomingMessage, res: http.ServerResponse) =
|
|||||||
return { code: 500, message: '未登录' };
|
return { code: 500, message: '未登录' };
|
||||||
}
|
}
|
||||||
const tokenUser = await getTokenUserCache(token);
|
const tokenUser = await getTokenUserCache(token);
|
||||||
console.log('authFilter tokenUser', tokenUser, token);
|
|
||||||
if (share === 'protected' && tokenUser?.code === 200) {
|
if (share === 'protected' && tokenUser?.code === 200) {
|
||||||
return { code: 200, message: '受保护模式已登录允许访问' };
|
return { code: 200, message: '受保护模式已登录允许访问' };
|
||||||
}
|
}
|
||||||
@@ -112,32 +111,46 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
|||||||
logger.debug('handle by router', { url: req.url });
|
logger.debug('handle by router', { url: req.url });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (pathname.startsWith('/router') || pathname.startsWith('/opencode')) {
|
||||||
|
logger.debug('handle by router (opencode/router)', { url: req.url });
|
||||||
|
return;
|
||||||
|
}
|
||||||
// client, api, v1, serve 开头的拦截
|
// client, api, v1, serve 开头的拦截
|
||||||
const apiProxy = _assistantConfig?.api?.proxy || [];
|
const apiProxy = _assistantConfig?.api?.proxy || [];
|
||||||
const defaultApiProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn');
|
const defaultApiProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn');
|
||||||
const allProxy = [...apiProxy, ...defaultApiProxy];
|
const allProxy = [...apiProxy, ...defaultApiProxy];
|
||||||
const apiBackendProxy = allProxy.find((item) => pathname.startsWith(item.path));
|
const apiBackendProxy = allProxy.find((item) => pathname.startsWith(item.path));
|
||||||
// console.log('apiBackendProxy', allProxy, apiBackendProxy, pathname, apiProxy[0].path);
|
const proxyFn = async (req: http.IncomingMessage, res: http.ServerResponse, proxyApi: ProxyInfo) => {
|
||||||
if (apiBackendProxy) {
|
log.debug('proxyApi', { proxyApi, url: req.url });
|
||||||
log.debug('apiBackendProxy', { apiBackendProxy, url: req.url });
|
|
||||||
// 设置 CORS 头
|
// 设置 CORS 头
|
||||||
// res.setHeader('Access-Control-Allow-Origin', '*');
|
// res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
// res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
// res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
// res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
|
// res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
|
||||||
if (apiBackendProxy.s3?.id) {
|
if (proxyApi.s3?.id) {
|
||||||
const storage = _assistantConfig?.storage || []
|
const storage = _assistantConfig?.storage || []
|
||||||
const storageConfig = storage.find((item) => item.id === apiBackendProxy.s3?.id);
|
const storageConfig = storage.find((item) => item.id === proxyApi.s3?.id);
|
||||||
apiBackendProxy.s3 = {
|
proxyApi.s3 = {
|
||||||
...storageConfig,
|
...storageConfig,
|
||||||
...apiBackendProxy.s3,
|
...proxyApi.s3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proxyApi.file?.id) {
|
||||||
|
const storage = _assistantConfig?.storage || []
|
||||||
|
const storageConfig = storage.find((item) => item.id === proxyApi.file?.id);
|
||||||
|
proxyApi.file = {
|
||||||
|
...storageConfig,
|
||||||
|
...proxyApi.file,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return proxy(req, res, {
|
return proxy(req, res, {
|
||||||
path: apiBackendProxy.path,
|
path: proxyApi.path,
|
||||||
target: apiBackendProxy.target,
|
target: proxyApi.target,
|
||||||
...apiBackendProxy,
|
...proxyApi,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (apiBackendProxy) {
|
||||||
|
return proxyFn(req, res, apiBackendProxy);
|
||||||
|
}
|
||||||
logger.debug('proxyRoute handle by router', { url: req.url }, noAdmin);
|
logger.debug('proxyRoute handle by router', { url: req.url }, noAdmin);
|
||||||
const urls = pathname.split('/');
|
const urls = pathname.split('/');
|
||||||
const [_, _user, _app] = urls;
|
const [_, _user, _app] = urls;
|
||||||
@@ -147,7 +160,7 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isOpen = isOpenPath(pathname)
|
const isOpen = isOpenPath(pathname)
|
||||||
log.debug('proxyRoute', { _user, _app, pathname, noAdmin, isOpen });
|
logger.debug('proxyRoute', { _user, _app, pathname, noAdmin, isOpen });
|
||||||
if (noAdmin && !isOpen) {
|
if (noAdmin && !isOpen) {
|
||||||
return toSetting();
|
return toSetting();
|
||||||
}
|
}
|
||||||
@@ -158,54 +171,40 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
|||||||
}
|
}
|
||||||
const proxyApiList = _assistantConfig?.proxy || [];
|
const proxyApiList = _assistantConfig?.proxy || [];
|
||||||
const proxyApi = proxyApiList.find((item) => pathname.startsWith(item.path));
|
const proxyApi = proxyApiList.find((item) => pathname.startsWith(item.path));
|
||||||
if (proxyApi && proxyApi.type === 'file') {
|
if (proxyApi) {
|
||||||
log.debug('proxyApi', { proxyApi, pathname });
|
logger.debug('proxyPage', { proxyApi, pathname });
|
||||||
const _indexPath = proxyApi.indexPath || `${_user}/${_app}/index.html`;
|
return proxyFn(req, res, proxyApi);
|
||||||
const _rootPath = proxyApi.rootPath;
|
|
||||||
if (!_rootPath) {
|
|
||||||
log.error('Not Found rootPath', { proxyApi, pathname });
|
|
||||||
return res.end(`Not Found [${proxyApi.path}] rootPath`);
|
|
||||||
}
|
|
||||||
return fileProxy(req, res, {
|
|
||||||
path: proxyApi.path, // 代理路径, 比如/root/home
|
|
||||||
rootPath: proxyApi.rootPath,
|
|
||||||
...proxyApi,
|
|
||||||
indexPath: _indexPath, // 首页路径
|
|
||||||
});
|
|
||||||
} else if (proxyApi && proxyApi.type === 'http') {
|
|
||||||
log.debug('proxyApi http', { proxyApi, pathname });
|
|
||||||
return httpProxy(req, res, {
|
|
||||||
path: proxyApi.path,
|
|
||||||
target: proxyApi.target,
|
|
||||||
type: 'http',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
const filter = await authFilter(req, res);
|
const filter = await authFilter(req, res);
|
||||||
if (filter.code !== 200) {
|
if (filter.code !== 200) {
|
||||||
console.log('auth filter deny', filter);
|
logger.debug('auth filter deny', filter);
|
||||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
|
// TODO: 这里可以做成可配置的登录页面
|
||||||
return res.end(renderNoAuthAndLogin('Not Authorized Proxy'));
|
return res.end(renderNoAuthAndLogin('Not Authorized Proxy'));
|
||||||
}
|
}
|
||||||
const localProxyProxyList = localProxy.getLocalProxyList();
|
const localProxyProxyList = localProxy.getLocalProxyList();
|
||||||
const localProxyProxy = localProxyProxyList.find((item) => pathname.startsWith(item.path));
|
const localProxyProxy = localProxyProxyList.find((item) => pathname.startsWith(item.path));
|
||||||
if (localProxyProxy) {
|
if (localProxyProxy) {
|
||||||
log.log('localProxyProxy', { localProxyProxy, url: req.url });
|
logger.debug('localProxyProxy', { localProxyProxy, url: req.url });
|
||||||
return fileProxy(req, res, {
|
return proxyFn(req, res, {
|
||||||
path: localProxyProxy.path,
|
path: localProxyProxy.path,
|
||||||
|
"type": 'file',
|
||||||
|
file: {
|
||||||
rootPath: localProxy.pagesDir,
|
rootPath: localProxy.pagesDir,
|
||||||
indexPath: localProxyProxy.indexPath,
|
indexPath: localProxyProxy.file.indexPath,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const creatCenterProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn', ['/root', '/' + _user]);
|
const creatCenterProxy = createApiProxy(_assistantConfig?.app?.url || 'https://kevisual.cn', ['/root', '/' + _user]);
|
||||||
const centerProxy = creatCenterProxy.find((item) => pathname.startsWith(item.path));
|
const centerProxy = creatCenterProxy.find((item) => pathname.startsWith(item.path));
|
||||||
if (centerProxy) {
|
if (centerProxy) {
|
||||||
return httpProxy(req, res, {
|
return proxyFn(req, res, {
|
||||||
path: centerProxy.path,
|
path: centerProxy.path,
|
||||||
target: centerProxy.target,
|
target: centerProxy.target,
|
||||||
type: 'http',
|
type: 'http',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
log.debug('handle by router 404', req.url);
|
logger.debug('handle by router 404', req.url);
|
||||||
|
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
res.end('Not Found Proxy');
|
res.end('Not Found Proxy');
|
||||||
|
|||||||
@@ -14,5 +14,6 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
|
"agent/**/*"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: '配置项介绍'
|
title: '配置项介绍'
|
||||||
description: 'Assistant 应用配置项完整说明文档,包括应用信息、代理、服务器、认证、AI等各项配置详解'
|
description: 'Assistant 应用配置项完整说明文档,包括应用信息、代理、服务器、认证、AI、存储等各项配置详解'
|
||||||
tags: ['config', 'configuration', 'settings', 'assistant']
|
tags: ['config', 'configuration', 'settings', 'assistant']
|
||||||
createdAt: '2025-12-18'
|
createdAt: '2025-12-18'
|
||||||
---
|
---
|
||||||
@@ -22,28 +22,38 @@ createdAt: '2025-12-18'
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **id**: `string` - 应用唯一标识符,用于识别具体设备或应用实例
|
| 字段 | 类型 | 说明 |
|
||||||
- **url**: `string` - 应用访问地址
|
| ---- | -------- | ------------------------------------------ |
|
||||||
|
| id | `string` | 应用唯一标识符,用于识别具体设备或应用实例 |
|
||||||
|
| url | `string` | 应用访问地址 |
|
||||||
|
|
||||||
## token - 访问令牌
|
## token - 访问令牌
|
||||||
|
|
||||||
|
用于身份验证的访问令牌。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"token": "your-access-token"
|
"token": "your-access-token"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **token**: `string` - 用于身份验证的访问令牌
|
| 字段 | 类型 | 说明 |
|
||||||
|
| ----- | -------- | -------- |
|
||||||
|
| token | `string` | 访问令牌 |
|
||||||
|
|
||||||
## registry - 注册中心
|
## registry - 注册中心
|
||||||
|
|
||||||
|
注册中心地址,默认为 `https://kevisual.cn`。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"registry": "https://kevisual.cn"
|
"registry": "https://kevisual.cn"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **registry**: `string` - 注册中心地址,默认为 `https://kevisual.cn`
|
| 字段 | 类型 | 说明 |
|
||||||
|
| -------- | -------- | ------------ |
|
||||||
|
| registry | `string` | 注册中心地址 |
|
||||||
|
|
||||||
## proxy - 前端代理配置
|
## proxy - 前端代理配置
|
||||||
|
|
||||||
@@ -61,16 +71,18 @@ createdAt: '2025-12-18'
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **proxy**: `ProxyInfo[]` - 代理配置数组
|
| 字段 | 类型 | 说明 |
|
||||||
- **path**: `string` - 匹配的路径前缀
|
| ---------------- | ------------- | ---------------------- |
|
||||||
- **target**: `string` - 目标服务器地址
|
| proxy | `ProxyInfo[]` | 代理配置数组 |
|
||||||
- **pathname**: `string` - 转发到目标服务器的路径
|
| proxy[].path | `string` | 匹配的路径前缀 |
|
||||||
|
| proxy[].target | `string` | 目标服务器地址 |
|
||||||
|
| proxy[].pathname | `string` | 转发到目标服务器的路径 |
|
||||||
|
|
||||||
示例:访问 `/root/home` 会被转发到 `https://kevisual.cn/root/home`
|
示例:访问 `/root/home` 会被转发到 `https://kevisual.cn/root/home`
|
||||||
|
|
||||||
## api - API代理配置
|
## api - API代理配置
|
||||||
|
|
||||||
专门用于API请求的代理配置。
|
专门用于 API 请求的代理配置,例如 `/api` 或 `/v1` 开头的请求。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -89,17 +101,48 @@ createdAt: '2025-12-18'
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **api.proxy**: `ProxyInfo[]` - API代理配置数组,配置方式同 `proxy`
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|{------|
|
||||||
|
| api.proxy | `ProxyInfo[]` | API代理配置数组 |
|
||||||
|
|
||||||
|
## router - 路由配置
|
||||||
|
|
||||||
|
配置应用的路由代理功能。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"router": {
|
||||||
|
"proxy": [
|
||||||
|
{
|
||||||
|
"type": "router",
|
||||||
|
"router": {
|
||||||
|
"url": "https://kevisual.cn/api/router"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| ------------ | ------------- | ----------------------------------------------------------------- |
|
||||||
|
| router.proxy | `ProxyInfo[]` | 代理配置数组 |
|
||||||
|
| router.base | `boolean` | 是否注册基础路由,监听https://kevisual.cn/api/router,默认 `false` |
|
||||||
|
|
||||||
## description - 应用描述
|
## description - 应用描述
|
||||||
|
|
||||||
|
应用的描述信息。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"description": "我的助手应用"
|
"description": "我的助手应用"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **description**: `string` - 应用的描述信息
|
| 字段 | 类型 | 说明 |
|
||||||
|
| ----------- | -------- | ------------ |
|
||||||
|
| description | `string` | 应用描述信息 |
|
||||||
|
|
||||||
## server - 服务器配置
|
## server - 服务器配置
|
||||||
|
|
||||||
@@ -114,8 +157,10 @@ createdAt: '2025-12-18'
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **server.path**: `string` - 服务器监听地址,默认 `127.0.0.1`
|
| 字段 | 类型 | 说明 |
|
||||||
- **server.port**: `number` - 服务器监听端口号
|
| ----------- | --------- | -------------------------------- |
|
||||||
|
| server.path | `string`` | 服务器监听地址,默认 `127.0.0.1` |
|
||||||
|
| server.port | `number` | 服务器监听端口号 |
|
||||||
|
|
||||||
## share - 远程访问配置
|
## share - 远程访问配置
|
||||||
|
|
||||||
@@ -130,8 +175,10 @@ createdAt: '2025-12-18'
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **share.url**: `string` - 远程应用代理地址
|
| 字段 | 类型 | 说明 |
|
||||||
- **share.enabled**: `boolean` - 是否启用远程访问功能
|
| ------------- | --------- | -------------------- |
|
||||||
|
| share.url | `string` | 远程应用代理地址 |
|
||||||
|
| share.enabled | `boolean` | 是否启用远程访问功能 |
|
||||||
|
|
||||||
## watch - 文件监听配置
|
## watch - 文件监听配置
|
||||||
|
|
||||||
@@ -145,21 +192,27 @@ createdAt: '2025-12-18'
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **watch.enabled**: `boolean` - 是否启用文件监听
|
| 字段 | 类型 | 说明 |
|
||||||
|
| ------------- | --------- | ---------------- |
|
||||||
|
| watch.enabled | `boolean` | 是否启用文件监听 |
|
||||||
|
|
||||||
## home - 首页路径
|
## home - 首页路径
|
||||||
|
|
||||||
|
访问根路径 `/` 时自动重定向的首页地址。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"home": "/root/home"
|
"home": "/root/home"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **home**: `string` - 访问根路径 `/` 时自动重定向的首页地址
|
| 字段 | 类型 | 说明 |
|
||||||
|
| ---- | -------- | -------- |
|
||||||
|
| home | `string` | 首页路径 |
|
||||||
|
|
||||||
## ai - AI功能配置
|
## ai - AI功能配置
|
||||||
|
|
||||||
启用和配置本地AI代理功能。
|
启用和配置本地 AI 代理功能。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -172,28 +225,32 @@ createdAt: '2025-12-18'
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **ai.enabled**: `boolean` - 是否启用AI功能
|
| 字段 | 类型 | 说明 |
|
||||||
- **ai.provider**: `string` - AI提供商,可选 `'DeepSeek'` | `'Custom'` 或其他自定义值
|
| ----------- | --------- | ---------------------------- | ---------- |
|
||||||
- **ai.apiKey**: `string` - API密钥
|
| ai.enabled | `boolean` | 是否启用 AI 功能 |
|
||||||
- **ai.model**: `string` - 使用的模型名称
|
| ai.provider | `string` | AI 提供商,可选 `'DeepSeek'` | `'Custom'` |
|
||||||
|
| ai.apiKey | `string` | API 密钥 |
|
||||||
|
| ai.model | `string` | 使用的模型名称 |
|
||||||
|
|
||||||
## scripts - 自定义脚本
|
## asr - 语音识别配置
|
||||||
|
|
||||||
定义自定义脚本命令,在应用启动时执行。
|
配置阿里云语音识别服务。
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"scripts": {
|
"asr": {
|
||||||
"start": "node server.js",
|
"enabled": true,
|
||||||
"build": "npm run build",
|
"token": "your-asr-token"
|
||||||
"custom": "echo 'Hello World'"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **scripts**: `Record<string, string>` - 键值对形式的脚本配置
|
| 字段 | 类型 | 说明 |
|
||||||
- key: 脚本名称
|
| ----------- | --------- | -------------------- |
|
||||||
- value: 要执行的命令
|
| asr.enabled | `boolean` | 是否启用语音识别功能 |
|
||||||
|
| asr.token | `string` | 阿里云 ASR 服务令牌 |
|
||||||
|
|
||||||
|
使用模型:`qwen3-asr-flash-realtime`
|
||||||
|
|
||||||
## auth - 认证和权限配置
|
## auth - 认证和权限配置
|
||||||
|
|
||||||
@@ -207,33 +264,71 @@ createdAt: '2025-12-18'
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **auth**: `AuthPermission` - 认证权限配置对象
|
| 字段 | 类型 | 说明 |
|
||||||
- **share**: 共享访问模式
|
| ---------- | -------- | ------------------------------------------------- |
|
||||||
- `"protected"` - 需要认证才能访问(默认)
|
| auth.share | `string` | 共享访问模式,影响 pages 目录下页面的对外共享权限 |
|
||||||
- `"public"` - 公开访问,无需认证
|
|
||||||
- `"private"` - 私有访问,完全禁止外部访问
|
|
||||||
|
|
||||||
> **说明**: `share` 配置影响 pages 目录下页面的对外共享权限
|
**share 可选值:**
|
||||||
|
|
||||||
## https - HTTPS证书配置
|
- `"protected"` - 需要认证才能访问(默认)
|
||||||
|
- `"public"` - 公开访问,无需认证
|
||||||
|
- `"private"` - 私有访问,完全禁止外部访问
|
||||||
|
|
||||||
配置HTTPS服务和证书。
|
## storage - 存储配置
|
||||||
|
|
||||||
|
配置文件存储,支持本地文件系统和 S3/MinIO 两种存储方式。
|
||||||
|
|
||||||
|
### 本地文件存储 (FileStorage)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"https": {
|
"storage": [
|
||||||
"type": "https",
|
{
|
||||||
"keyPath": "/path/to/private.key",
|
"id": "local-storage",
|
||||||
"certPath": "/path/to/certificate.crt"
|
"path": "./uploads",
|
||||||
|
"type": "local"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- **https.type**: `'https' | 'http'` - 服务协议类型,默认 `'http'`
|
### S3/MinIO 存储 (S3Storage)
|
||||||
- **https.keyPath**: `string` - SSL证书私钥文件路径
|
|
||||||
- **https.certPath**: `string` - SSL证书文件路径
|
|
||||||
|
|
||||||
> **注意**: 通常不需要配置HTTPS,可以通过反向代理(如Nginx)实现HTTPS访问
|
```json
|
||||||
|
{
|
||||||
|
"storage": [
|
||||||
|
{
|
||||||
|
"id": "s3-storage",
|
||||||
|
"bucket": "my-bucket",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"accessKeyId": "AKIAXXXXXXXXXXXXXXXX",
|
||||||
|
"secretAccessKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
|
"endpoint": "https://s3.amazonaws.com",
|
||||||
|
"type": "s3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "minio-storage",
|
||||||
|
"bucket": "my-bucket",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"accessKeyId": "minioadmin",
|
||||||
|
"secretAccessKey": "minioadmin",
|
||||||
|
"endpoint": "http://localhost:9000",
|
||||||
|
"type": "minio"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
| ------------------------- | ---------------------------- | ------------------------------------------------------- |
|
||||||
|
| storage[].id | `string` | 存储标识符,唯一标识一个存储配置 |
|
||||||
|
| storage[].type | `'local' \| 's3' \| 'minio'` | 存储类型 |
|
||||||
|
| storage[].path | `string` | 本地存储路径(仅 type 为 `local` 时有效) |
|
||||||
|
| storage[].bucket | `string` | 存储桶名称(仅 type 为 `s3` 或 `minio` 时有效) |
|
||||||
|
| storage[].region | `string` | 存储区域(仅 type 为 `s3` 或 `minio` 时有效) |
|
||||||
|
| storage[].accessKeyId | `string` | 访问密钥 ID(仅 type 为 `s3` 或 `minio` 时有效) |
|
||||||
|
| storage[].secretAccessKey | `string` | 访问密钥(仅 type 为 `s3` 或 `minio` 时有效) |
|
||||||
|
| storage[].endpoint | `string` | 服务端点地址(仅 type 为 `s3` 或 `minio` 时有效,可选) |
|
||||||
|
|
||||||
## 完整配置示例
|
## 完整配置示例
|
||||||
|
|
||||||
@@ -260,6 +355,17 @@ createdAt: '2025-12-18'
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"router": {
|
||||||
|
"proxy": [
|
||||||
|
{
|
||||||
|
"type": "router",
|
||||||
|
"router": {
|
||||||
|
"url": "https://kevisual.cn/api/router"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"base": true
|
||||||
|
},
|
||||||
"description": "生产环境助手应用",
|
"description": "生产环境助手应用",
|
||||||
"server": {
|
"server": {
|
||||||
"path": "0.0.0.0",
|
"path": "0.0.0.0",
|
||||||
@@ -279,13 +385,29 @@ createdAt: '2025-12-18'
|
|||||||
"apiKey": "sk-xxx",
|
"apiKey": "sk-xxx",
|
||||||
"model": "deepseek-chat"
|
"model": "deepseek-chat"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"asr": {
|
||||||
"setup": "npm install",
|
"enabled": true,
|
||||||
"dev": "npm run dev"
|
"token": "your-asr-token"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"share": "protected"
|
"share": "protected"
|
||||||
|
},
|
||||||
|
"storage": [
|
||||||
|
{
|
||||||
|
"id": "local-storage",
|
||||||
|
"path": "./uploads",
|
||||||
|
"type": "local"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "s3-storage",
|
||||||
|
"bucket": "my-bucket",
|
||||||
|
"region": "us-east-1",
|
||||||
|
"accessKeyId": "AKIAXXXXXXXXXXXXXXXX",
|
||||||
|
"secretAccessKey": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
|
"endpoint": "https://s3.amazonaws.com",
|
||||||
|
"type": "s3"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -295,8 +417,8 @@ createdAt: '2025-12-18'
|
|||||||
|
|
||||||
## 最佳实践
|
## 最佳实践
|
||||||
|
|
||||||
1. **安全性**: 不要在配置文件中硬编码敏感信息(如 token、apiKey),建议使用环境变量
|
1. **安全性**: 不要在配置文件中硬编码敏感信息(如 token、apiKey、secretAccessKey),建议使用环境变量
|
||||||
2. **端口选择**: 确保选择的端口未被占用
|
2. **端口选择**: 确保选择的端口未被占用
|
||||||
3. **代理配置**: 合理配置代理路径,避免路径冲突
|
3. **代理配置**: 合理配置代理路径,避免路径冲突
|
||||||
4. **HTTPS**: 生产环境建议使用反向代理配置HTTPS,而非直接在应用中配置
|
4. **权限控制**: 根据实际需求选择合适的 `auth.share` 模式
|
||||||
5. **权限控制**: 根据实际需求选择合适的 `auth.share` 模式
|
5. **存储配置**: 根据应用规模选择合适的存储方式,本地存储适合开发和小型应用,S3/MinIO 适合生产环境
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@kevisual/cli",
|
"name": "@kevisual/cli",
|
||||||
"version": "0.0.87",
|
"version": "0.0.92",
|
||||||
"description": "envision 命令行工具",
|
"description": "envision 命令行工具",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"basename": "/root/cli",
|
"basename": "/root/cli",
|
||||||
@@ -20,6 +20,9 @@
|
|||||||
"asst": "bin/assistant.js",
|
"asst": "bin/assistant.js",
|
||||||
"asst-server": "bin/assistant-server.js"
|
"asst-server": "bin/assistant-server.js"
|
||||||
},
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/assistant-opencode.js"
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"bin",
|
"bin",
|
||||||
@@ -45,21 +48,22 @@
|
|||||||
"@kevisual/app": "^0.0.2",
|
"@kevisual/app": "^0.0.2",
|
||||||
"@kevisual/context": "^0.0.4",
|
"@kevisual/context": "^0.0.4",
|
||||||
"@kevisual/use-config": "^1.0.28",
|
"@kevisual/use-config": "^1.0.28",
|
||||||
|
"@opencode-ai/sdk": "^1.1.28",
|
||||||
"@types/busboy": "^1.5.4",
|
"@types/busboy": "^1.5.4",
|
||||||
"busboy": "^1.6.0",
|
"busboy": "^1.6.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.4",
|
||||||
"lowdb": "^7.0.1",
|
"lowdb": "^7.0.1",
|
||||||
|
"pm2": "latest",
|
||||||
"lru-cache": "^11.2.4",
|
"lru-cache": "^11.2.4",
|
||||||
"micromatch": "^4.0.8",
|
"micromatch": "^4.0.8",
|
||||||
"semver": "^7.7.3",
|
"semver": "^7.7.3",
|
||||||
"unstorage": "^1.17.4"
|
"unstorage": "^1.17.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"pm2": "^6.0.14",
|
|
||||||
"@kevisual/dts": "^0.0.3",
|
"@kevisual/dts": "^0.0.3",
|
||||||
"@kevisual/load": "^0.0.6",
|
"@kevisual/load": "^0.0.6",
|
||||||
"@kevisual/logger": "^0.0.4",
|
"@kevisual/logger": "^0.0.4",
|
||||||
"@kevisual/query": "0.0.35",
|
"@kevisual/query": "0.0.37",
|
||||||
"@kevisual/query-login": "0.0.7",
|
"@kevisual/query-login": "0.0.7",
|
||||||
"@types/bun": "^1.3.6",
|
"@types/bun": "^1.3.6",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
@@ -75,7 +79,8 @@
|
|||||||
"form-data": "^4.0.5",
|
"form-data": "^4.0.5",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"tar": "^7.5.3",
|
"pm2": "^6.0.14",
|
||||||
|
"tar": "^7.5.6",
|
||||||
"zustand": "^5.0.10"
|
"zustand": "^5.0.10"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
567
pnpm-lock.yaml
generated
567
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
# 一个简单的cli工具
|
# 一个简单的cli工具
|
||||||
|
|
||||||
```
|
```
|
||||||
npm i @kevisual/cli -g --registry=https://npm.cnb.cool/kevisual/registry/-/packages/
|
npm i -g @kevisual/cli
|
||||||
```
|
```
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { program, Command } from '@/program.ts';
|
import { program, Command } from '@/program.ts';
|
||||||
import { chalk } from '../module/chalk.ts';
|
import { chalk } from '../../module/chalk.ts';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { useKey } from '@kevisual/use-config';
|
import { useKey } from '@kevisual/use-config';
|
||||||
@@ -7,7 +7,7 @@ import os from 'node:os'
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import { select } from '@inquirer/prompts';
|
import { select } from '@inquirer/prompts';
|
||||||
|
|
||||||
const MODELS = ['minimax', 'glm', 'volcengine'] as const;
|
const MODELS = ['minimax', 'glm', 'volcengine', 'bailian'] as const;
|
||||||
type Model = typeof MODELS[number];
|
type Model = typeof MODELS[number];
|
||||||
|
|
||||||
const changeMinimax = (token?: string) => {
|
const changeMinimax = (token?: string) => {
|
||||||
@@ -53,7 +53,20 @@ const changeVolcengine = (token?: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const changeBailian = (token?: string) => {
|
||||||
|
const auth_token = token || useKey('BAILIAN_API_KEY')
|
||||||
|
return {
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": auth_token,
|
||||||
|
"ANTHROPIC_BASE_URL": "https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||||
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "qwen3-coder-plus",
|
||||||
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "qwen3-coder-plus",
|
||||||
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "qwen3-coder-plus",
|
||||||
|
"ANTHROPIC_MODEL": "qwen3-coder-plus"
|
||||||
|
},
|
||||||
|
"includeCoAuthoredBy": false
|
||||||
|
}
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 跳过登录检查,在~/.claude.json的配置中添加字段 "hasCompletedOnboarding": true
|
* 跳过登录检查,在~/.claude.json的配置中添加字段 "hasCompletedOnboarding": true
|
||||||
*/
|
*/
|
||||||
@@ -83,6 +96,7 @@ const modelConfig: Record<Model, (token?: string) => object> = {
|
|||||||
minimax: changeMinimax,
|
minimax: changeMinimax,
|
||||||
glm: changeGLM,
|
glm: changeGLM,
|
||||||
volcengine: changeVolcengine,
|
volcengine: changeVolcengine,
|
||||||
|
bailian: changeBailian,
|
||||||
};
|
};
|
||||||
|
|
||||||
const readOrCreateConfig = (configPath: string): Record<string, unknown> => {
|
const readOrCreateConfig = (configPath: string): Record<string, unknown> => {
|
||||||
2
src/command/opencode/plugin.ts
Normal file
2
src/command/opencode/plugin.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// TODO: 对 .opencode/plugin/agent.ts 的内容进行管理
|
||||||
|
// 例如添加、删除、列出等操作
|
||||||
@@ -17,7 +17,7 @@ import './command/gist/index.ts';
|
|||||||
import './command/config-remote.ts';
|
import './command/config-remote.ts';
|
||||||
import './command/config-secret-remote.ts';
|
import './command/config-secret-remote.ts';
|
||||||
import './command/ai.ts';
|
import './command/ai.ts';
|
||||||
import './command/cc.ts'
|
import './command/claude/cc.ts'
|
||||||
import './command/docker.ts';
|
import './command/docker.ts';
|
||||||
// program.parse(process.argv);
|
// program.parse(process.argv);
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,5 @@ export const writeConfig = (config: Record<string, any>) => {
|
|||||||
|
|
||||||
export const getEnvToken = () => {
|
export const getEnvToken = () => {
|
||||||
const envTokne = useKey('KEVISUAL_TOKEN') || '';
|
const envTokne = useKey('KEVISUAL_TOKEN') || '';
|
||||||
console.log('getEnvToken', envTokne);
|
return envTokne;
|
||||||
// return envTokne;
|
|
||||||
return '';
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user