"feat: 增加助手应用配置管理功能与服务器守护进程支持"
This commit is contained in:
@@ -1,17 +1,26 @@
|
||||
// @ts-check
|
||||
// https://bun.sh/docs/bundler
|
||||
// @ts-ignore
|
||||
import path from 'node:path';
|
||||
import pkg from './package.json';
|
||||
import fs from 'node:fs';
|
||||
// bun run src/index.ts --
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
/**
|
||||
*
|
||||
* @param {string} p
|
||||
* @returns
|
||||
*/
|
||||
export const w = (p) => path.join(__dirname, p);
|
||||
await Bun.build({
|
||||
target: 'node',
|
||||
format: 'esm',
|
||||
entrypoints: ['./src/index.ts'],
|
||||
outdir: './dist',
|
||||
entrypoints: [w('./src/index.ts')],
|
||||
outdir: w('./dist'),
|
||||
naming: {
|
||||
entry: 'assistant.mjs',
|
||||
},
|
||||
|
||||
external: ['pm2'],
|
||||
define: {
|
||||
ENVISION_VERSION: JSON.stringify(pkg.version),
|
||||
},
|
||||
@@ -21,13 +30,33 @@ await Bun.build({
|
||||
await Bun.build({
|
||||
target: 'node',
|
||||
format: 'esm',
|
||||
entrypoints: ['./src/server.ts'],
|
||||
outdir: './dist',
|
||||
entrypoints: [w('./src/server.ts')],
|
||||
outdir: w('./dist'),
|
||||
naming: {
|
||||
entry: 'assistan-server.mjs',
|
||||
},
|
||||
define: {
|
||||
ENVISION_VERSION: JSON.stringify(pkg.version),
|
||||
entry: 'assistant-server.mjs',
|
||||
},
|
||||
external: ['pm2'],
|
||||
env: 'ENVISION_*',
|
||||
});
|
||||
|
||||
export const copyFileToEnvision = async () => {
|
||||
const src = ['dist', 'bin'].map((dir) => {
|
||||
return { absolute: path.join(__dirname, dir), name: dir };
|
||||
});
|
||||
const dest = path.join(__dirname, '..');
|
||||
for (const dir of src) {
|
||||
const files = fs.readdirSync(dir.absolute);
|
||||
for (const file of files) {
|
||||
const srcFile = path.join(dir.absolute, file);
|
||||
const destFile = path.join(dest, dir.name, file);
|
||||
try {
|
||||
fs.copyFileSync(srcFile, destFile);
|
||||
} catch (err) {
|
||||
console.error('Error copying files: origin: ', srcFile, 'dest: ', destFile);
|
||||
console.error('Error copying files:', err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
await copyFileToEnvision();
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "bun run src/run.ts ",
|
||||
"dev:server": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 bun --watch src/server.ts",
|
||||
"dev:server": "cross-env NODE_TLS_REJECT_UNAUTHORIZED=0 bun --watch src/run-server.ts",
|
||||
"build": "rimraf dist && bun run bun.config.mjs"
|
||||
},
|
||||
"bin": {
|
||||
@@ -27,6 +27,7 @@
|
||||
"ev-asst": "bin/assistant.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/ai-center": "^0.0.3",
|
||||
"@kevisual/load": "^0.0.6",
|
||||
"@kevisual/local-app-manager": "^0.1.16",
|
||||
"@kevisual/query": "0.0.17",
|
||||
@@ -41,6 +42,8 @@
|
||||
"chalk": "^5.4.1",
|
||||
"commander": "^13.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.5.0",
|
||||
"get-port": "^7.1.0",
|
||||
"inquirer": "^12.6.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^5.1.5",
|
||||
@@ -49,6 +52,7 @@
|
||||
"send": "^1.2.0",
|
||||
"supports-color": "^10.0.0",
|
||||
"ws": "npm:@kevisual/ws",
|
||||
"dayjs": "^1.11.13",
|
||||
"zustand": "^5.0.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -58,9 +62,11 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"dayjs": "^1.11.13"
|
||||
"pm2": "^6.0.5"
|
||||
},
|
||||
"overrides": {
|
||||
"ws": "npm:@kevisual/ws"
|
||||
"ws": "npm:@kevisual/ws",
|
||||
"@kevisual/query": "0.0.17",
|
||||
"@kevisual/router": "0.0.13"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICXTCCAcagAwIBAgIJUmN5oWFZdxK8MA0GCSqGSIb3DQEBBQUAMF8xCjAIBgNV
|
||||
BAMTASoxCzAJBgNVBAYTAkNOMREwDwYDVQQIEwhaaGVKaWFuZzERMA8GA1UEBxMI
|
||||
SGFuZ3pob3UxETAPBgNVBAoTCEVudmlzaW9uMQswCQYDVQQLEwJJVDAeFw0yNTA0
|
||||
MjQxODExMjZaFw0yNjA0MjQxODExMjZaMF8xCjAIBgNVBAMTASoxCzAJBgNVBAYT
|
||||
AkNOMREwDwYDVQQIEwhaaGVKaWFuZzERMA8GA1UEBxMISGFuZ3pob3UxETAPBgNV
|
||||
BAoTCEVudmlzaW9uMQswCQYDVQQLEwJJVDCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
|
||||
gYkCgYEAirpqS9Lwh5JNY7N303wphXCR/HZDgfw1HnP6b62WVJTHtU97hLKjrTXx
|
||||
zUYPEyySXLzFGjptKSjT3ZgulV1I9YBXg2gdDibxxxZUZHoJ8j0oh+MSxRv1fTzw
|
||||
+HEBErUJQJ4lHnf9nbi7Tf48XiNWqh9Lce3XvyDFQoRDASX5yeUCAwEAAaMhMB8w
|
||||
HQYDVR0RBBYwFIIBKoIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBBQUAA4GB
|
||||
AGCYapPzhY0zUVZxo6CsijdDQpuHe2G3cDs4bzpF2YHRGN3t8/cPwROt7FWCkzBt
|
||||
b7g/Tar+200fGspmLS95QisjiKo0fAKfaEE8CHXr2jlt8+omOz0tPg9LCZi2GtgI
|
||||
8EC+Vvvcd9UjzHmoPBZQF4qAvJ2IyOwBh6Vwyh8las+e
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,15 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIICWwIBAAKBgQCKumpL0vCHkk1js3fTfCmFcJH8dkOB/DUec/pvrZZUlMe1T3uE
|
||||
sqOtNfHNRg8TLJJcvMUaOm0pKNPdmC6VXUj1gFeDaB0OJvHHFlRkegnyPSiH4xLF
|
||||
G/V9PPD4cQEStQlAniUed/2duLtN/jxeI1aqH0tx7de/IMVChEMBJfnJ5QIDAQAB
|
||||
AoGAGmBUKoN6OQSPk0fBniOqz1S2ZP5lWncF8HrToF0sSnuZNvdcQEAoz5uElGdg
|
||||
IWClmV1IynJWY+9/zM+M99grMT6it3VHHVM3MQoTf1Am4Vy0qgKR6Y1TzE0XLrVW
|
||||
3e3ezDph3gG0EQsRxVbn/goCEfstuhJaFyxHvsQRtPY+Z1ECQQC8ffbjV8hb911o
|
||||
iUw67FquOL9AYrFfQfohkQZ1TrDv0VTCAYpB7e5ml4dBhUL1dQbFV7avZIufD5fl
|
||||
pxCKkAPNAkEAvGnQPByrKj6ggf2l1CzgjXzZ24wm3AkJutWkFjAcf5EFy0+MIBOi
|
||||
ejyrGcGi9eovXCLGLrgzaBeAHa4XNkX2eQJAEZE73VxlFA0t63xAWo2EthAb4whP
|
||||
t60SfuZhT7WR0AgWei5ikFp4iZ89v+GHqBDMHMBcCmS4jo6JfaHgbMmXUQJAAIDL
|
||||
1I1DC77VEOPLgJCKHPabYlGyfN3tT7loUcLZIKITgOJ6fk9vHKJy1oPE2qFAdR+G
|
||||
pfNJ99owNmQTncp8CQJAI3fp5VABViB3uha4cHmpRUvoGNWmmh9Ob6LypDsGtd8z
|
||||
8ah+4Ek1DvsQC4XDuwgwnQsCmEYfa2P1T/GIdqPadw==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -15,8 +15,10 @@ const Init = new Command('init')
|
||||
} else if (opts.path) {
|
||||
opts.path = path.resolve(opts.path);
|
||||
}
|
||||
const configDir = AssistantInit.detectConfigDir(opts.path);
|
||||
console.log('configDir', configDir);
|
||||
const assistantInit = new AssistantInit({
|
||||
path: opts.path,
|
||||
path: configDir,
|
||||
});
|
||||
assistantInit.init();
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { program, runProgram } from '@/program.ts';
|
||||
import './command/init/index.ts';
|
||||
import './command/config-manager/index.ts';
|
||||
import './command/app-manager/index.ts';
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,7 +37,7 @@ export const initConfig = (configRootPath: string) => {
|
||||
*/
|
||||
appsDir,
|
||||
/**
|
||||
* 服务配置文件路径 assistant-service-config.json
|
||||
* 服务配置文件路径 assistant-apps-config.json
|
||||
*/
|
||||
appsConfigPath,
|
||||
/**
|
||||
@@ -65,6 +65,15 @@ type AssistantConfigData = {
|
||||
proxy?: ProxyInfo[];
|
||||
apiProxyList?: ProxyInfo[];
|
||||
description?: string;
|
||||
/**
|
||||
* 首页
|
||||
*/
|
||||
home?: string;
|
||||
ai?: {
|
||||
provider?: string | 'DeepSeek' | 'SiliconFlow';
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
};
|
||||
};
|
||||
let assistantConfig: AssistantConfigData;
|
||||
type AssistantConfigOptions = {
|
||||
@@ -142,7 +151,7 @@ export class AssistantConfig {
|
||||
fs.writeFileSync(pageConfigPath, JSON.stringify(_saveConfig, null, 2));
|
||||
return _saveConfig;
|
||||
}
|
||||
assAppConfig(app: any) {
|
||||
addAppConfig(app: any) {
|
||||
const config = this.getPageConfig();
|
||||
const assistantConfig = this.getCacheAssistantConfig();
|
||||
const _apps = config.list;
|
||||
|
||||
3
assistant/src/run-server.ts
Normal file
3
assistant/src/run-server.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { runServer } from "./server.ts";
|
||||
|
||||
runServer();
|
||||
@@ -1,10 +1,71 @@
|
||||
import { app } from './app.ts';
|
||||
import { proxyRoute, proxyWs } from './services/proxy/proxy-page-index.ts';
|
||||
import getPort, { portNumbers } from 'get-port';
|
||||
import { program } from 'commander';
|
||||
import { spawnSync } from 'child_process';
|
||||
export const runServer = async (port?: number) => {
|
||||
let _port: number | undefined;
|
||||
if (port) {
|
||||
_port = await getPort({ port });
|
||||
if (_port !== port) {
|
||||
console.log(`Port ${port} is not available`);
|
||||
_port = undefined;
|
||||
}
|
||||
}
|
||||
if (!_port) {
|
||||
// 检车端口可用性
|
||||
const isPortAvailable = await getPort({ port: portNumbers(51015, 52000) });
|
||||
if (!isPortAvailable) {
|
||||
console.log(`Port ${isPortAvailable} is not available`);
|
||||
process.exit(1);
|
||||
}
|
||||
_port = isPortAvailable;
|
||||
}
|
||||
app.listen(_port, () => {
|
||||
console.log(`Server is running on https://localhost:${_port}`);
|
||||
});
|
||||
app.server.on(proxyRoute);
|
||||
proxyWs();
|
||||
return {
|
||||
app,
|
||||
port: _port,
|
||||
};
|
||||
};
|
||||
program
|
||||
.description('启动服务')
|
||||
.option('-d, --daemon', '是否以守护进程方式运行')
|
||||
.option('-n, --name <name>', '服务名称', 'assistant-server')
|
||||
.option('-p, --port <port>', '服务端口')
|
||||
.option('-s, --start', '是否启动服务')
|
||||
.action(async (options) => {
|
||||
// console.log('当前执行路径:', execPath, inte);
|
||||
if (options.daemon) {
|
||||
const [_interpreter, execPath] = process.argv;
|
||||
const name = options.name;
|
||||
const port = options.port;
|
||||
let pm2Command = `pm2 start ${execPath} --name ${name} -- -s `;
|
||||
if (port) {
|
||||
pm2Command += ` -p ${port}`;
|
||||
}
|
||||
const result = spawnSync(pm2Command, {
|
||||
shell: true,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
if (result.error) {
|
||||
console.error('Error starting server:', result.error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('以守护进程方式运行');
|
||||
} else if (options.start) {
|
||||
console.log('启动服务');
|
||||
const server = await runServer(options.port);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(51015, () => {
|
||||
console.log('Server is running on http://localhost:51015');
|
||||
});
|
||||
|
||||
app.server.on(proxyRoute);
|
||||
|
||||
proxyWs();
|
||||
export const runParser = async (argv: string[]) => {
|
||||
try {
|
||||
program.parse(argv);
|
||||
} catch (error) {
|
||||
console.error('执行错误:', error.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { checkFileExists, AssistantConfig } from '@/module/assistant/index.ts';
|
||||
import { chalk } from '@/module/chalk.ts';
|
||||
export type AssistantInitOptions = {
|
||||
path?: string;
|
||||
init?: boolean;
|
||||
};
|
||||
/**
|
||||
* 助手初始化类
|
||||
@@ -14,6 +15,7 @@ export class AssistantInit extends AssistantConfig {
|
||||
const configDir = opts?.path || process.cwd();
|
||||
super({
|
||||
configDir,
|
||||
init: opts?.init ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,21 +32,30 @@ export class AssistantInit extends AssistantConfig {
|
||||
}
|
||||
}
|
||||
checkConfigPath() {
|
||||
const assistantPath = path.join(this.configDir, 'assistant-config.json');
|
||||
const assistantPath = path.join(this.configDir, 'assistant-app', 'assistant-config.json');
|
||||
return checkFileExists(assistantPath);
|
||||
}
|
||||
createAssistantConfig() {
|
||||
const assistantPath = this.configPath?.configPath;
|
||||
// 创建助手配置文件 assistant-config.json
|
||||
if (!checkFileExists(assistantPath, true)) {
|
||||
this.setConfig({
|
||||
description: '助手配置文件',
|
||||
});
|
||||
console.log(chalk.green('助手配置文件创建成功'));
|
||||
console.log(chalk.green('助手配置文件assistant-config.json创建成功'));
|
||||
}
|
||||
const env = this.configPath?.envConfigPath;
|
||||
// 创建助手环境配置文件 env
|
||||
if (!checkFileExists(env, true)) {
|
||||
fs.writeFileSync(env, '# 环境配置文件\n');
|
||||
console.log(chalk.green('助手环境配置文件创建成功'));
|
||||
console.log(chalk.green('助手环境配置.env文件创建成功'));
|
||||
}
|
||||
|
||||
const appsConfig = this.configPath?.appsConfigPath;
|
||||
// 创建助手应用配置文件 apps
|
||||
if (!checkFileExists(appsConfig, true)) {
|
||||
fs.writeFileSync(appsConfig, JSON.stringify({ description: 'apps manager.', list: [] }));
|
||||
console.log(chalk.green('助手应用配置文件apps.json创建成功'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp
|
||||
const appDir = assistantConfig.configPath?.pageDir;
|
||||
const url = new URL(req.url, 'http://localhost');
|
||||
const pathname = url.pathname;
|
||||
if (pathname === '/' && _assistantConfig?.home) {
|
||||
res.writeHead(302, { Location: `${_assistantConfig?.home}/` });
|
||||
return res.end();
|
||||
}
|
||||
if (pathname.startsWith('/favicon.ico')) {
|
||||
res.statusCode = 404;
|
||||
res.end('Not Found Favicon');
|
||||
|
||||
53
assistant/src/test/chat.ts
Normal file
53
assistant/src/test/chat.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// import { AssistantConfig } from '@/module/assistant/index.ts';
|
||||
import { assistantConfig } from '../config.ts';
|
||||
|
||||
import { ProviderManager, SiliconFlowProvider } from '@kevisual/ai-center';
|
||||
|
||||
const config = assistantConfig.getConfig();
|
||||
console.log('aiConfig', config.ai);
|
||||
const providerManager = new ProviderManager({
|
||||
provider: config.ai.provider,
|
||||
apiKey: config.ai.apiKey!,
|
||||
model: config.ai.model!,
|
||||
});
|
||||
|
||||
const chatTest = async (text: string) => {
|
||||
const result = await providerManager.provider.chat([
|
||||
{
|
||||
role: 'user',
|
||||
content: text,
|
||||
},
|
||||
]);
|
||||
console.log('result', result);
|
||||
return result.choices[0].message.content;
|
||||
};
|
||||
|
||||
chatTest('你好').then((res) => {
|
||||
console.log('chatTest', res);
|
||||
});
|
||||
|
||||
// providerManager.provider.test().then((res) => {
|
||||
// console.log('test', res);
|
||||
// });
|
||||
// const siliconflow = providerManager.provider as any;
|
||||
const siliconflow = new SiliconFlowProvider({
|
||||
apiKey: config.ai.apiKey!,
|
||||
model: config.ai.model!,
|
||||
});
|
||||
const main = async () => {
|
||||
const usage = await siliconflow.getUsageInfo();
|
||||
console.log(usage);
|
||||
};
|
||||
|
||||
// main();
|
||||
const test = async () => {
|
||||
const result = await siliconflow.chat([
|
||||
{
|
||||
role: 'user',
|
||||
content: '你好',
|
||||
},
|
||||
]);
|
||||
console.log('result', result, 'result.choices[0].message.content', result.choices[0].message.content);
|
||||
return result.choices[0].message.content;
|
||||
};
|
||||
// test();
|
||||
22
assistant/src/test/provider/model-scope.ts
Normal file
22
assistant/src/test/provider/model-scope.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ProviderManager, SiliconFlowProvider, ModelScopeProvider } from '@kevisual/ai-center';
|
||||
|
||||
import { config } from 'dotenv';
|
||||
config();
|
||||
const providerTest = async () => {
|
||||
const providerConfig = { provider: 'ModelScope', model: 'Qwen/Qwen2.5-Coder-32B-Instruct', apiKey: process.env.MODEL_SCOPE_API_KEY };
|
||||
const provider = await ProviderManager.createProvider(providerConfig);
|
||||
const result = await provider.chat([{ role: 'user', content: '你好' }]);
|
||||
console.log(result);
|
||||
};
|
||||
|
||||
providerTest();
|
||||
|
||||
const modelScopeTest = async () => {
|
||||
const provider = new ModelScopeProvider({
|
||||
apiKey: process.env.MODEL_SCOPE_API_KEY,
|
||||
model: 'Qwen/Qwen2.5-Coder-32B-Instruct',
|
||||
});
|
||||
const result = await provider.chat([{ role: 'user', content: '你好' }]);
|
||||
console.log(result);
|
||||
};
|
||||
// modelScopeTest();
|
||||
9
assistant/task-command/.gitignore
vendored
Normal file
9
assistant/task-command/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
|
||||
dist
|
||||
|
||||
pack-dist
|
||||
|
||||
.env*
|
||||
!.env*example
|
||||
4
assistant/task-command/.npmrc
Normal file
4
assistant/task-command/.npmrc
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||
ignore-workspace-root-check=true
|
||||
67
assistant/task-command/mod.d.ts
vendored
Normal file
67
assistant/task-command/mod.d.ts
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
declare const TaskCommandType: readonly ["npm-install"];
|
||||
type TaskCommand = {
|
||||
key?: string;
|
||||
/**
|
||||
* 任务描述
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* 命令, 执行的任务
|
||||
*/
|
||||
command: string;
|
||||
type?: (typeof TaskCommandType)[number] | string;
|
||||
/**
|
||||
* 任务执行完成后,执行判断的命令
|
||||
*/
|
||||
after?: string;
|
||||
/**
|
||||
* 任务执行前,执行判断的命令
|
||||
*/
|
||||
before?: string;
|
||||
/**
|
||||
* 任务执行完成后, 检测输出的文本内容,如果有这个文本,表示任务执行成功
|
||||
* 如果没有这个文本,表示任务执行失败
|
||||
*/
|
||||
afterCheck?: string;
|
||||
/**
|
||||
* 任务执行前, 检测输出的文本内容,如果有这个文本,表示任务已经安装
|
||||
* 如果没有这个文本,表示任务没有安装
|
||||
*/
|
||||
beforeCheck?: string;
|
||||
};
|
||||
declare class TasksCommand {
|
||||
tasks: Map<string, TaskCommand>;
|
||||
constructor();
|
||||
addTask(task: TaskCommand, run?: boolean): this;
|
||||
getTask(name: string): TaskCommand;
|
||||
runTask(name: string): {
|
||||
task: TaskCommand;
|
||||
code: number;
|
||||
} | {
|
||||
code: number;
|
||||
message: string;
|
||||
};
|
||||
/**
|
||||
* 检测是否需要继续执行。
|
||||
* 1. res.code === 500 代表执行失败, 需要继续执行
|
||||
* 2. res.code === 200 代表执行成功,但是需要检测输出内容
|
||||
* 2.1 如果有 check 的内容,代表不需要继续执行
|
||||
* 2.2 如果没有 check 的内容,代表需要继续执行
|
||||
* @param res
|
||||
* @param check
|
||||
* @returns
|
||||
*/
|
||||
private checkForContainue;
|
||||
runCommand(command: string): {
|
||||
code: number;
|
||||
data: string;
|
||||
message?: undefined;
|
||||
} | {
|
||||
code: number;
|
||||
data: string;
|
||||
message: any;
|
||||
};
|
||||
}
|
||||
|
||||
export { TaskCommandType, TasksCommand };
|
||||
export type { TaskCommand };
|
||||
142
assistant/task-command/mod.ts
Normal file
142
assistant/task-command/mod.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
export const TaskCommandType = ['npm-install'] as const;
|
||||
export type TaskCommand = {
|
||||
key?: string;
|
||||
/**
|
||||
* 任务描述
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* 命令, 执行的任务
|
||||
*/
|
||||
command: string;
|
||||
type?: (typeof TaskCommandType)[number] | string;
|
||||
/**
|
||||
* 任务执行完成后,执行判断的命令
|
||||
*/
|
||||
after?: string;
|
||||
/**
|
||||
* 任务执行前,执行判断的命令
|
||||
*/
|
||||
before?: string;
|
||||
/**
|
||||
* 任务执行完成后, 检测输出的文本内容,如果有这个文本,表示任务执行成功
|
||||
* 如果没有这个文本,表示任务执行失败
|
||||
*/
|
||||
afterCheck?: string;
|
||||
/**
|
||||
* 任务执行前, 检测输出的文本内容,如果有这个文本,表示任务已经安装
|
||||
* 如果没有这个文本,表示任务没有安装
|
||||
*/
|
||||
beforeCheck?: string;
|
||||
};
|
||||
export class TasksCommand {
|
||||
tasks: Map<string, TaskCommand> = new Map();
|
||||
constructor() {}
|
||||
addTask(task: TaskCommand, run?: boolean) {
|
||||
const key = task?.key || task?.description;
|
||||
if (!key) {
|
||||
throw new Error('当前的任务没有key');
|
||||
}
|
||||
this.tasks.set(key, task);
|
||||
if (run) {
|
||||
this.runTask(key);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
getTask(name: string) {
|
||||
return this.tasks.get(name);
|
||||
}
|
||||
runTask(name: string) {
|
||||
const task = this.getTask(name);
|
||||
const end = (data?: { code: number; [key: string]: any }) => {
|
||||
return {
|
||||
...data,
|
||||
task,
|
||||
};
|
||||
};
|
||||
if (!task) {
|
||||
return {
|
||||
code: 500,
|
||||
message: `没有找到 ${name} 这个任务`,
|
||||
};
|
||||
}
|
||||
let { command, before, after, afterCheck, beforeCheck, type } = task;
|
||||
if (type === 'npm-install' && !afterCheck) {
|
||||
afterCheck = 'added';
|
||||
}
|
||||
if (before) {
|
||||
const res = this.runCommand(before);
|
||||
console.log('before', res, beforeCheck, this.checkForContainue(res, beforeCheck));
|
||||
if (!this.checkForContainue(res, beforeCheck)) {
|
||||
return end({
|
||||
code: 200,
|
||||
message: `当前任务不需要执行, ${command}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
const res = this.runCommand(command);
|
||||
console.log('runCommand', res);
|
||||
if (res.code !== 200) {
|
||||
return end(res);
|
||||
}
|
||||
let checkText = res.data || '';
|
||||
if (after) {
|
||||
const res = this.runCommand(after);
|
||||
if (res.code !== 200) {
|
||||
return end(res);
|
||||
}
|
||||
checkText = res.data || '';
|
||||
}
|
||||
if (afterCheck) {
|
||||
const isSuccess = checkText?.includes?.(afterCheck);
|
||||
return end({
|
||||
code: isSuccess ? 200 : 500,
|
||||
output: res.data,
|
||||
check: afterCheck,
|
||||
message: isSuccess ? `当前任务执行成功, ${command}` : `当前任务执行失败, ${command}`,
|
||||
});
|
||||
}
|
||||
return end(res);
|
||||
}
|
||||
/**
|
||||
* 检测是否需要继续执行。
|
||||
* 1. res.code === 500 代表执行失败, 需要继续执行
|
||||
* 2. res.code === 200 代表执行成功,但是需要检测输出内容
|
||||
* 2.1 如果有 check 的内容,代表不需要继续执行
|
||||
* 2.2 如果没有 check 的内容,代表需要继续执行
|
||||
* @param res
|
||||
* @param check
|
||||
* @returns
|
||||
*/
|
||||
private checkForContainue(res: { data: string; code: number; [key: string]: any }, check?: string, isBefore = true) {
|
||||
if (res.code !== 200) {
|
||||
return true;
|
||||
}
|
||||
if (!check) {
|
||||
return true;
|
||||
}
|
||||
const hasIncludes = res.data?.includes?.(check);
|
||||
if (isBefore) {
|
||||
// 代表已经安装, 不需要继续执行
|
||||
return !hasIncludes;
|
||||
}
|
||||
return hasIncludes;
|
||||
}
|
||||
runCommand(command: string) {
|
||||
try {
|
||||
const res = execSync(command, { encoding: 'utf-8' });
|
||||
return {
|
||||
code: 200,
|
||||
data: res.toString(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
code: 500,
|
||||
data: '',
|
||||
message: error.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
26
assistant/task-command/package.json
Normal file
26
assistant/task-command/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@kevisual/task-command",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"types": "mod.d.ts",
|
||||
"scripts": {
|
||||
"dts": "dts -i mod.ts -o mod.d.ts -d ."
|
||||
},
|
||||
"keywords": [],
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"mod.ts",
|
||||
"mod.d.ts"
|
||||
],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.7.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./mod.ts",
|
||||
"./mod.ts": "./mod.ts"
|
||||
}
|
||||
}
|
||||
3
assistant/task-command/readme.md
Normal file
3
assistant/task-command/readme.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# task-command
|
||||
|
||||
command line task runner
|
||||
34
assistant/tasks/silkyai/deno.json
Normal file
34
assistant/tasks/silkyai/deno.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"tasks": {
|
||||
"dev": {
|
||||
"command": "deno run -A talkshow.ts",
|
||||
"desc": "Run the server in development mode"
|
||||
},
|
||||
"build": {
|
||||
"command": "deno run --allow-all --unstable src/build.ts",
|
||||
"desc": "Build the project"
|
||||
},
|
||||
"build:dts": {
|
||||
"command": "deno run -A talkshow.ts --dts",
|
||||
"desc": "Build the project with dts"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"files": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.js",
|
||||
"src/**/*.jsx"
|
||||
],
|
||||
"options": {
|
||||
"rules": {
|
||||
"ban-untagged-todo": false,
|
||||
"no-explicit-any": false,
|
||||
"no-unused-vars": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"imports": {
|
||||
"https://esm.xiongxiao.me/@kevisual/task-command/mod.ts": "../../task-command/mod.ts"
|
||||
}
|
||||
}
|
||||
17
assistant/tasks/silkyai/deno.lock
generated
Normal file
17
assistant/tasks/silkyai/deno.lock
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": "4",
|
||||
"specifiers": {
|
||||
"npm:@types/node@*": "22.12.0"
|
||||
},
|
||||
"npm": {
|
||||
"@types/node@22.12.0": {
|
||||
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
|
||||
"dependencies": [
|
||||
"undici-types"
|
||||
]
|
||||
},
|
||||
"undici-types@6.20.0": {
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,40 @@
|
||||
import { TasksCommand } from 'https://esm.xiongxiao.me/@kevisual/task-command/mod.ts';
|
||||
// import { TasksCommand } from '../../task-command/mod.ts';
|
||||
const init = {
|
||||
description: '安装依赖',
|
||||
command: 'npm install -g @kevisual/cli --registry=https://registry.npmmirror.com',
|
||||
afterCheck: 'added',
|
||||
};
|
||||
const init1 = {
|
||||
description: '安装 pm2',
|
||||
type: 'npm-install',
|
||||
command: 'npm install -g pm2 --registry=https://registry.npmmirror.com',
|
||||
before: 'pm2 -v',
|
||||
};
|
||||
const init2 = {
|
||||
description: '安装 deno',
|
||||
command: 'npm install -g deno --registry=https://registry.npmmirror.com',
|
||||
type: 'npm-install',
|
||||
before: 'deno -v',
|
||||
beforeCheck: 'deno',
|
||||
};
|
||||
const init3 = {
|
||||
description: '安装 bun',
|
||||
type: 'npm-install',
|
||||
command: 'npm install -g bun --registry=https://registry.npmmirror.com',
|
||||
beforeCheck: 'bun -v',
|
||||
};
|
||||
|
||||
// ===========================
|
||||
// 安装talkshow的程序到本地
|
||||
const talk = {
|
||||
description: '设置默认的 base registry 地址 为 https://kevisual.silkyai.cn',
|
||||
command: 'ev base -s https://kevisual.silkyai.cn',
|
||||
};
|
||||
const talkInit = {
|
||||
description: '初始化一个助手客户端,生成配置文件。',
|
||||
command: 'asst init',
|
||||
};
|
||||
const task1 = {
|
||||
description: '下载前端应用 root/center 应用',
|
||||
command: 'ev app download -i root/center -o assistant-app/page',
|
||||
@@ -12,3 +49,26 @@ const task3 = {
|
||||
description: '安装后端应用 root/talkshow-code-center 应用',
|
||||
command: 'ev app download -i root/talkshow-code-center -t app -o assistant-app/apps/talkshow-code-center',
|
||||
};
|
||||
|
||||
// ===========================
|
||||
|
||||
const runTask1 = async () => {
|
||||
const tasksCommand = new TasksCommand();
|
||||
tasksCommand.addTask(init2);
|
||||
const res = tasksCommand.runTask(init2.description);
|
||||
console.log(res);
|
||||
};
|
||||
// runTask1();
|
||||
const runTestTask = async () => {
|
||||
const tasksCommand = new TasksCommand();
|
||||
const task = {
|
||||
description: 'test',
|
||||
command: 'npm i -g rollup --registry=https://registry.npmmirror.com',
|
||||
type: 'npm-install',
|
||||
};
|
||||
tasksCommand.addTask(task);
|
||||
const res = tasksCommand.runTask(task.description);
|
||||
console.log(res);
|
||||
return res;
|
||||
};
|
||||
runTestTask();
|
||||
|
||||
Reference in New Issue
Block a user