feat: add zod dependency and implement kevisual routes for CLI commands

- Added zod as a dependency in package.json.
- Enhanced assistant configuration to include skills and plugins directories.
- Implemented runCmd function to execute CLI commands in run.ts.
- Updated light-code module to use node:child_process.
- Created new kevisual routes for checking CLI login status and deploying web pages.
- Added restart functionality for OpenCode client in opencode module.
This commit is contained in:
2026-01-28 00:02:38 +08:00
parent 98f21d8aaa
commit 742a7a2992
11 changed files with 352 additions and 146 deletions

View File

@@ -87,6 +87,7 @@
"lowdb": "^7.0.1",
"lru-cache": "^11.2.4",
"pm2": "^6.0.14",
"unstorage": "^1.17.4"
"unstorage": "^1.17.4",
"zod": "^4.3.6"
}
}

View File

@@ -5,6 +5,7 @@ import { checkFileExists, createDir } from '../file/index.ts';
import { ProxyInfo } from '../proxy/proxy.ts';
import dotenv from 'dotenv';
import { logger } from '@/module/logger.ts';
import { z } from 'zod'
let kevisualDir = path.join(homedir(), 'kevisual');
const envKevisualDir = process.env.ASSISTANT_CONFIG_DIR
@@ -28,12 +29,15 @@ export const initConfig = (configRootPath: string) => {
const pageConfigPath = path.join(configDir, 'assistant-page-config.json');
const pagesDir = createDir(path.join(configDir, 'pages'));
const appsDir = createDir(path.join(configDir, 'apps'));
const skillsDir = createDir(path.join(configDir, 'skills'), false);
const pluginsDir = createDir(path.join(configDir, 'plugins'), false);
const appsConfigPath = path.join(configDir, 'assistant-apps-config.json');
const appPidPath = path.join(configDir, 'assistant-app.pid');
const envConfigPath = path.join(configDir, '.env');
return {
/**
* 助手配置文件路径
* 助手配置文件路径, assistant-app 目录
*/
configDir,
/**
@@ -41,7 +45,7 @@ export const initConfig = (configRootPath: string) => {
*/
configPath,
/**
* 服务目录, 后端服务目录
* 服务目录, 后端服务目录, apps 目录
*/
appsDir,
/**
@@ -49,7 +53,7 @@ export const initConfig = (configRootPath: string) => {
*/
appsConfigPath,
/**
* 应用目录, 前端应用目录
* 应用目录, 前端应用目录 pages 目录
*/
pagesDir,
/**
@@ -64,6 +68,14 @@ export const initConfig = (configRootPath: string) => {
* 环境变量配置文件路径
*/
envConfigPath,
/**
* 技能目录,配置给 opencode 去用的
*/
skillsDir,
/**
* 插件目录, 给 cli 用的,动态加载插件,每一个都是独立的
*/
pluginsDir,
};
};
export type ReturnInitConfigType = ReturnType<typeof initConfig>;

View File

@@ -0,0 +1,49 @@
import { spawn } from 'node:child_process'
type RunCmdOptions = {
cmd: string;
cwd?: string;
env?: Record<string, string>;
}
type RunResult = {
code: number;
data: string;
}
/**
* 运行命令行指令
* @param opts
* @returns
*/
export const runCmd = (opts: RunCmdOptions): Promise<RunResult> => {
const { cmd, cwd } = opts || {};
return new Promise<RunResult>((resolve) => {
const parts = cmd.split(' ');
const command = parts[0];
const args = parts.slice(1);
const proc = spawn(command, args, {
cwd: cwd || process.cwd(),
shell: true,
env: { ...process.env, ...opts?.env },
});
let stdout = '';
let stderr = '';
let result = ''
proc.stdout.on('data', (data: Buffer) => {
stdout += data.toString();
});
proc.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
});
proc.on('close', (code: number) => {
result = stdout;
if (stderr) {
result += '\n' + stderr;
}
resolve({ code: code === 0 ? 200 : code, data: result });
});
proc.on('error', (err: Error) => {
resolve({ code: 500, data: err.message });
});
});
}

View File

@@ -1,4 +1,4 @@
import { fork } from 'child_process'
import { fork } from 'node:child_process'
import fs from 'fs';
export const fileExists = (path: string): boolean => {

View File

@@ -9,6 +9,7 @@ import './call/index.ts'
// import './hot-api/key-sender/index.ts';
import './opencode/index.ts';
import './remote/index.ts';
import './kevisual/index.ts'
import os from 'node:os';
import { authCache } from '@/module/cache/auth.ts';
@@ -160,6 +161,7 @@ app
})
.addTo(app);
// 调用 path: client key: system
app
.route({
path: 'client',

View File

@@ -0,0 +1,67 @@
import { app } from '@/app.ts'
import { runCmd } from '@/module/cmd/run.ts';
import { createSkill, tool } from "@kevisual/router";
import { useKey } from '@kevisual/use-config';
// 查看 ev cli 是否登录
app.route({
path: 'kevisual',
key: ' me',
description: '查看 ev cli 是否登录',
middleware: ['admin-auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'kevisual-me',
title: '查看 ev cli 是否登录',
summary: '查看 ev cli 是否登录',
args: {
}
})
},
}).define(async (ctx) => {
const cmd = 'ev me';
const res = await runCmd({ cmd })
if (res.code === 200) {
ctx.body = { content: res.data };
} else {
ctx.throw(500, res.data);
}
}).addTo(app);
// 执行工具 kevisual-login-by-admin
// 执行工具 通过当前登录用户 ev cl
// 调用 path: kevisual key: loginByAdmin
app.route({
path: 'kevisual',
key: 'loginByAdmin',
description: '通过当前登录用户 ev cli',
middleware: ['admin-auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'kevisual-login-by-admin',
title: '通过当前登录用户 ev cli',
summary: '通过当前登录用户登录 ev cli, 直接用当前的用户的 token 直接设置 token 给 ev cli, 登录失败直接停止任务',
args: {}
})
},
}).define(async (ctx) => {
const token = ctx.query?.token || useKey('KEVISUAL_TOKEN');
if (!token) {
ctx.throw(400, '登录的 token 不能为空,请传入 token 参数');
return;
}
const cmd = `ev login -e `;
const res = await runCmd({
cmd,
env: {
'KEVISUAL_TOKEN': token
}
})
if (res.code === 200) {
ctx.body = { content: res.data };
} else {
ctx.throw(500, res.data);
}
}).addTo(app);

View File

@@ -0,0 +1,45 @@
import { app } from '@/app.ts'
import { runCmd } from '@/module/cmd/run.ts';
import { createSkill, tool } from "@kevisual/router";
// 调用 path: kevisual key: deploy
app.route({
path: 'kevisual',
key: 'deploy',
description: '部署一个网页',
middleware: ['admin-auth'],
metadata: {
tags: ['kevisual'],
...createSkill({
skill: 'kevisual-deploy',
title: '部署一个网页',
summary: '部署一个网页到 kevisual 平台',
args: {
filepath: tool.schema.string().describe('要部署的网页文件路径'),
appKey: tool.schema.string().optional().describe('应用的 appKey如果不传则创建一个新的应用'),
version: tool.schema.string().optional().describe('应用的版本号,默认为 1.0.0'),
update: tool.schema.boolean().optional().describe('是否同时更新部署,默认为 false'),
}
})
},
}).define(async (ctx) => {
const { filepath, appKey, update } = ctx.query;
console.log('部署网页filepath:', filepath, 'appKey:', appKey);
ctx.body = { content: '部署功能正在开发中,敬请期待!' };
// ev deloly ${filepath} -k ${appKey} -v 1.0.0 -u -y y
// if (!filepath) {
// ctx.throw(400, '文件路径 filepath 不能为空');
// return;
// }
// let cmd = `ev deploy ${filepath} --type web`;
// if (appKey) {
// cmd += ` --appKey ${appKey}`;
// }
// const res = await runCmd({ cmd });
// if (res.code === 200) {
// ctx.body = { content: res.data };
// } else {
// ctx.throw(500, res.data);
// }
}).addTo(app);

View File

@@ -0,0 +1,2 @@
import './auth.ts'
import './deploy.ts'

View File

@@ -1,8 +1,6 @@
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 客户端
@@ -27,7 +25,7 @@ app.route({
ctx.body = { content: `${opencodeManager.url} OpenCode 客户端已就绪` };
}).addTo(app);
// 关闭 opencode 客户端
// 关闭 opencode 客户端 5000
app.route({
path: 'opencode',
key: 'close',
@@ -38,17 +36,39 @@ app.route({
...createSkill({
skill: 'close-opencode-client',
title: '关闭 OpenCode 客户端',
summary: '关闭 OpenCode 客户端',
summary: '关闭 OpenCode 客户端, 未提供端口则关闭默认端口',
args: {
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 5000')
}
})
},
}).define(async (ctx) => {
await opencodeManager.close();
const port = ctx.query.port;
await opencodeManager.close({ port });
ctx.body = { content: 'OpenCode 客户端已关闭' };
}).addTo(app);
app.route({
path: 'opencode',
key: 'restart',
middleware: ['auth'],
description: '重启 OpenCode 客户端',
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'restart-opencode-client',
title: '重启 OpenCode 客户端',
summary: '重启 OpenCode 客户端',
args: {
port: tool.schema.number().optional().describe('OpenCode 服务端口,默认为 5000')
}
})
},
}).define(async (ctx) => {
const port = ctx.query.port;
const res = await opencodeManager.restart({ port });
ctx.body = { content: `${opencodeManager.url} OpenCode 客户端已经重启` };
}).addTo(app);
// 调用 path: opencode key: getUrl
app.route({
path: 'opencode',

View File

@@ -122,6 +122,11 @@ export class OpencodeManager {
}
return `http://localhost:${port}`;
}
async restart(opts?: { port?: number }): Promise<OpencodeClient> {
const port = opts?.port ?? DEFAULT_PORT;
await this.close({ port });
return await this.getClient({ port });
}
}
export const opencodeManager = OpencodeManager.getInstance();