Compare commits
8 Commits
ddfdb63598
...
14f2dad837
| Author | SHA1 | Date | |
|---|---|---|---|
| 14f2dad837 | |||
| 915f7aff6b | |||
| 15d81c4f68 | |||
| 48dafc6040 | |||
| 2ccda9f008 | |||
| 6cf3beae94 | |||
| a583fdc211 | |||
| 59b3961ff9 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -8,4 +8,6 @@ pack-dist
|
||||
assistant-app
|
||||
|
||||
build
|
||||
.pnpm-store
|
||||
.pnpm-store
|
||||
|
||||
jwt
|
||||
2
apps/.gitignore
vendored
2
apps/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
container
|
||||
root
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "apps",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kevisual/assistant-cli",
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.8",
|
||||
"description": "",
|
||||
"main": "dist/assistant.mjs",
|
||||
"keywords": [
|
||||
@@ -41,20 +41,19 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/ai": "^0.0.21",
|
||||
"@kevisual/api": "^0.0.22",
|
||||
"@kevisual/ai": "^0.0.22",
|
||||
"@kevisual/api": "^0.0.26",
|
||||
"@kevisual/load": "^0.0.6",
|
||||
"@kevisual/local-app-manager": "^0.1.32",
|
||||
"@kevisual/logger": "^0.0.4",
|
||||
"@kevisual/query": "0.0.37",
|
||||
"@kevisual/query": "0.0.38",
|
||||
"@kevisual/query-login": "0.0.7",
|
||||
"@kevisual/router": "^0.0.60",
|
||||
"@kevisual/router": "^0.0.62",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@kevisual/use-config": "^1.0.28",
|
||||
"@opencode-ai/plugin": "^1.1.28",
|
||||
"@opencode-ai/plugin": "^1.1.36",
|
||||
"@types/bun": "^1.3.6",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^25.0.9",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/send": "^1.2.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"chalk": "^5.6.2",
|
||||
@@ -64,7 +63,6 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"get-port": "^7.1.0",
|
||||
"inquirer": "^13.2.1",
|
||||
"lodash-es": "^4.17.22",
|
||||
"nanoid": "^5.1.6",
|
||||
"send": "^1.2.1",
|
||||
"supports-color": "^10.2.2",
|
||||
@@ -78,11 +76,12 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.972.0",
|
||||
"@aws-sdk/client-s3": "^3.975.0",
|
||||
"@kevisual/ha-api": "^0.0.6",
|
||||
"@kevisual/js-filter": "^0.0.5",
|
||||
"@kevisual/oss": "^0.0.16",
|
||||
"@kevisual/video-tools": "^0.0.13",
|
||||
"@opencode-ai/sdk": "^1.1.28",
|
||||
"@opencode-ai/sdk": "^1.1.36",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"eventemitter3": "^5.0.4",
|
||||
"lowdb": "^7.0.1",
|
||||
|
||||
@@ -53,7 +53,7 @@ app.route({
|
||||
description: '获取路由列表',
|
||||
}).define(async (ctx) => {
|
||||
const list = ctx.app.getList((item) => {
|
||||
if (item.id === 'auth') return false;
|
||||
if (item?.path?.includes('auth') || item?.id?.includes('auth')) return false;
|
||||
return true;
|
||||
})
|
||||
ctx.body = { list }
|
||||
|
||||
@@ -5,5 +5,5 @@ import './routes/index.ts';
|
||||
import './routes-simple/index.ts';
|
||||
|
||||
export const AgentPlugin: Plugin = createRouterAgentPluginFn({
|
||||
router: app.router,
|
||||
router: app,
|
||||
})
|
||||
@@ -98,10 +98,12 @@ export type AssistantConfigData = {
|
||||
* Router代理, 会自动获取 {path: 'router', key: 'list'}的路由信息,然后注入到整个router应用当中.
|
||||
* 例子: { proxy: [ { type: 'router', api: 'https://localhost:50002/api/router' } ] }
|
||||
* base: 是否使用 /api/router的基础路径,默认false
|
||||
* lightcode: 是否启用lightcode路由,默认false
|
||||
*/
|
||||
router?: {
|
||||
proxy: ProxyInfo[];
|
||||
base?: boolean;
|
||||
lightcode?: boolean;
|
||||
}
|
||||
/**
|
||||
* API 代理配置, 比如,api开头的,v1开头的等等
|
||||
|
||||
@@ -10,6 +10,7 @@ import { logger } from '@/module/logger.ts';
|
||||
import { getEnvToken } from '@/module/http-token.ts';
|
||||
import { initApi } from '@kevisual/api/proxy'
|
||||
import { Query } from '@kevisual/query';
|
||||
import { initLightCode } from '@/module/light-code/index.ts';
|
||||
export class AssistantApp extends Manager {
|
||||
config: AssistantConfig;
|
||||
pagesPath: string;
|
||||
@@ -99,7 +100,9 @@ export class AssistantApp extends Manager {
|
||||
if (isConnect) {
|
||||
remoteApp.listenProxy();
|
||||
this.remoteIsConnected = true;
|
||||
remoteApp.emitter.once('close', () => {
|
||||
// 清理已有的 close 事件监听器,防止多重绑定
|
||||
remoteApp.emitter.removeAllListeners('close');
|
||||
remoteApp.emitter.on('close', () => {
|
||||
setTimeout(() => {
|
||||
if (remoteApp.isError) {
|
||||
console.error('远程应用发生错误,不重连');
|
||||
@@ -111,7 +114,7 @@ export class AssistantApp extends Manager {
|
||||
logger.debug('链接到了远程应用服务器');
|
||||
const appId = id;
|
||||
const username = config?.auth.username || 'unknown';
|
||||
const url = new URL(`/${username}/v1/${appId}`, 'https://kevisual.cn/');
|
||||
const url = new URL(`/${username}/v1/${appId}`, config?.registry || 'https://kevisual.cn/');
|
||||
this.remoteUrl = url.toString();
|
||||
console.log('远程地址', this.remoteUrl);
|
||||
} else {
|
||||
@@ -129,6 +132,7 @@ export class AssistantApp extends Manager {
|
||||
const config = this.config.getConfig();
|
||||
const routerProxy = config?.router?.proxy || [];
|
||||
const base = config.router?.base ?? false;
|
||||
const lightcode = config.router?.lightcode ?? true;
|
||||
if (base) {
|
||||
routerProxy.push({
|
||||
type: 'router',
|
||||
@@ -137,12 +141,27 @@ export class AssistantApp extends Manager {
|
||||
}
|
||||
})
|
||||
}
|
||||
if (lightcode) {
|
||||
routerProxy.push({
|
||||
type: 'lightcode',
|
||||
lightcode: {
|
||||
check: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
if (routerProxy.length === 0) {
|
||||
return
|
||||
}
|
||||
for (const proxyInfo of routerProxy) {
|
||||
if (proxyInfo.type !== 'router') {
|
||||
console.warn('路由的type必须是"router"');
|
||||
if (proxyInfo.type !== 'router' && proxyInfo.type !== 'lightcode') {
|
||||
console.warn('路由的type必须是"router", 或者lightcode');
|
||||
continue;
|
||||
}
|
||||
if (proxyInfo.type === 'lightcode') {
|
||||
initLightCode({
|
||||
router: this.mainApp,
|
||||
config: this.config
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const url = proxyInfo.router!.url;
|
||||
@@ -173,6 +192,10 @@ export class AssistantApp extends Manager {
|
||||
console.log('重新连接到远程应用服务器...', this.attemptedConnectTimes);
|
||||
const remoteApp = this.remoteApp;;
|
||||
if (remoteApp) {
|
||||
// 先关闭旧的 WebSocket,防止竞态条件
|
||||
if (remoteApp.ws) {
|
||||
remoteApp.ws.close();
|
||||
}
|
||||
remoteApp.init();
|
||||
this.attemptedConnectTimes += 1;
|
||||
const isConnect = await remoteApp.isConnect();
|
||||
@@ -180,8 +203,8 @@ export class AssistantApp extends Manager {
|
||||
remoteApp.listenProxy();
|
||||
this.attemptedConnectTimes = 0;
|
||||
console.log('重新连接到了远程应用服务器');
|
||||
this.reconnectRemoteApp();
|
||||
} else {
|
||||
this.reconnectRemoteApp();
|
||||
setTimeout(() => {
|
||||
this.initRouterApp()
|
||||
}, 30 * 1000 + this.attemptedConnectTimes * 10 * 1000); // 30秒后重连 + 每次增加10秒
|
||||
|
||||
@@ -14,7 +14,7 @@ export type ProxyInfo = {
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
type?: 'file' | 'dynamic' | 'minio' | 'http' | 's3' | 'router';
|
||||
type?: 'file' | 'dynamic' | 'minio' | 'http' | 's3' | 'router' | 'lightcode';
|
||||
/**
|
||||
* 目标的 pathname, 默认为请求的url.pathname, 设置了pathname,则会使用pathname作为请求的url.pathname
|
||||
* @default undefined
|
||||
@@ -45,6 +45,13 @@ export type ProxyInfo = {
|
||||
router?: {
|
||||
id?: string;
|
||||
url?: string;
|
||||
},
|
||||
lightcode?: {
|
||||
id?: string;
|
||||
/**
|
||||
* 是否检测远程服务更新
|
||||
*/
|
||||
check?: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
16
assistant/src/module/file-hash.ts
Normal file
16
assistant/src/module/file-hash.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
|
||||
export const getHash = (file: string) => {
|
||||
if (!fs.existsSync(file)) return '';
|
||||
const buffer = fs.readFileSync(file); // 不指定编码,返回 Buffer
|
||||
return crypto.createHash('md5').update(buffer).digest('hex');
|
||||
};
|
||||
|
||||
export const getBufferHash = (buffer: Buffer) => {
|
||||
return crypto.createHash('md5').update(buffer).digest('hex');
|
||||
};
|
||||
|
||||
export const getStringHash = (str: string) => {
|
||||
return crypto.createHash('md5').update(str).digest('hex');
|
||||
}
|
||||
177
assistant/src/module/light-code/index.ts
Normal file
177
assistant/src/module/light-code/index.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { App, QueryRouterServer } from '@kevisual/router';
|
||||
import { AssistantInit } from '../../services/init/index.ts';
|
||||
import path from 'node:path';
|
||||
import fs, { write } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import glob from 'fast-glob';
|
||||
import { runCode } from './run.ts';
|
||||
const codeDemoId = '0e700dc8-90dd-41b7-91dd-336ea51de3d2'
|
||||
import { filter } from "@kevisual/js-filter";
|
||||
import { getHash, getStringHash } from '../file-hash.ts';
|
||||
import { AssistantConfig } from '@/lib.ts';
|
||||
|
||||
const codeDemo = `// 这是一个示例代码文件
|
||||
import {App} from '@kevisual/router';
|
||||
|
||||
const app = new App();
|
||||
|
||||
app.route({
|
||||
path: 'hello',
|
||||
description: 'LightCode 示例路由',
|
||||
metadata: {
|
||||
tags: ['light-code', 'example'],
|
||||
},
|
||||
}).define(async (ctx) => {
|
||||
console.log('tokenUser:', ctx.query?.tokenUser);
|
||||
ctx.body = 'Hello from LightCode!';
|
||||
}).addTo(app);
|
||||
|
||||
app.wait();
|
||||
`;
|
||||
const writeCodeDemo = async (appDir: string) => {
|
||||
const lightcodeDir = path.join(appDir, 'light-code', 'code');
|
||||
const demoPath = path.join(lightcodeDir, `${codeDemoId}.ts`);
|
||||
fs.writeFileSync(demoPath, codeDemo, 'utf-8');
|
||||
}
|
||||
// writeCodeDemo(path.join(os.homedir(), 'kevisual', 'assistant-app', 'apps'));
|
||||
|
||||
type opts = {
|
||||
router: QueryRouterServer | App
|
||||
config: AssistantConfig | AssistantInit
|
||||
sync?: boolean
|
||||
}
|
||||
type LightCodeFile = {
|
||||
id?: string, code?: string, hash?: string, filepath: string
|
||||
}
|
||||
export const initLightCode = async (opts: opts) => {
|
||||
// 注册 light-code 路由
|
||||
console.log('初始化 light-code 路由');
|
||||
const config = opts.config as AssistantInit;
|
||||
const app = opts.router;
|
||||
const token = config.getConfig()?.token || '';
|
||||
const query = config.query;
|
||||
const sync = opts.sync ?? true;
|
||||
if (!config || !app) {
|
||||
console.error('initLightCode 缺少必要参数, config 或 app');
|
||||
return;
|
||||
}
|
||||
const appDir = config.configPath.appsDir;
|
||||
const lightcodeDir = path.join(appDir, 'light-code', 'code');
|
||||
if (!fs.existsSync(lightcodeDir)) {
|
||||
fs.mkdirSync(lightcodeDir, { recursive: true });
|
||||
}
|
||||
let diffList: LightCodeFile[] = [];
|
||||
|
||||
const codeFiles = glob.sync(['**/*.ts', '**/*.js'], {
|
||||
cwd: lightcodeDir,
|
||||
onlyFiles: true,
|
||||
}).map(file => {
|
||||
return {
|
||||
filepath: path.join(lightcodeDir, file),
|
||||
// hash: getHash(path.join(lightcodeDir, file))
|
||||
}
|
||||
});
|
||||
|
||||
if (sync) {
|
||||
const queryRes = await query.post({
|
||||
path: 'light-code',
|
||||
key: 'list',
|
||||
token,
|
||||
});
|
||||
if (queryRes.code === 200) {
|
||||
const lightQueryList = queryRes.data?.list || [];
|
||||
for (const item of lightQueryList) {
|
||||
const codeHash = getStringHash(item.code || '');
|
||||
diffList.push({ id: item.id!, code: item.code || '', hash: codeHash, filepath: path.join(lightcodeDir, `${item.id}.ts`) });
|
||||
}
|
||||
|
||||
|
||||
const codeFileSet = new Set(codeFiles.map(f => f.filepath));
|
||||
// 需要新增的文件 (在 diffList 中但不在 codeFiles 中)
|
||||
const toAdd = diffList.filter(d => !codeFileSet.has(d.filepath));
|
||||
// 需要删除的文件 (在 codeFiles 中但不在 diffList 中)
|
||||
const toDelete = codeFiles.filter(f => !diffList.some(d => d.filepath === f.filepath));
|
||||
// 需要更新的文件 (两边都有但 hash 不同)
|
||||
const toUpdate = diffList.filter(d => codeFileSet.has(d.filepath) && d.hash !== getHash(d.filepath));
|
||||
const unchanged = diffList.filter(d => codeFileSet.has(d.filepath) && d.hash === getHash(d.filepath));
|
||||
|
||||
// 执行新增
|
||||
for (const item of toAdd) {
|
||||
fs.writeFileSync(item.filepath, item.code, 'utf-8');
|
||||
// console.log(`新增 light-code 文件: ${item.filepath}`);
|
||||
}
|
||||
|
||||
// 执行删除
|
||||
for (const filepath of toDelete) {
|
||||
fs.unlinkSync(filepath.filepath);
|
||||
// console.log(`删除 light-code 文件: ${filepath.filepath}`);
|
||||
}
|
||||
|
||||
// 执行更新
|
||||
for (const item of toUpdate) {
|
||||
fs.writeFileSync(item.filepath, item.code, 'utf-8');
|
||||
// console.log(`更新 light-code 文件: ${item.filepath}`);
|
||||
}
|
||||
// 记录未更新的文件
|
||||
// const lightCodeList = [...toAdd, ...unchanged].map(d => ({
|
||||
// filepath: d.filepath,
|
||||
// hash: d.hash
|
||||
// }));
|
||||
} else {
|
||||
console.error('light-code 同步失败', queryRes.message);
|
||||
diffList = codeFiles;
|
||||
}
|
||||
} else {
|
||||
diffList = codeFiles;
|
||||
}
|
||||
|
||||
|
||||
for (const file of diffList) {
|
||||
const tsPath = file.filepath;
|
||||
const runRes = await runCode(tsPath, { path: 'router', key: 'list' }, { timeout: 10000 });
|
||||
if (runRes.success) {
|
||||
const res = runRes.data;
|
||||
if (res.code === 200) {
|
||||
const list = res.data?.list || [];
|
||||
for (const routerItem of list) {
|
||||
if (routerItem.path?.includes('auth') || routerItem.path?.includes('router') || routerItem.path?.includes('call')) {
|
||||
continue;
|
||||
}
|
||||
// console.log(`注册 light-code 路由: [${routerItem.path}] ${routerItem.id} 来自文件: ${file.filepath}`);
|
||||
const metadata = routerItem.metadata || {};
|
||||
if (metadata.tags && Array.isArray(metadata.tags)) {
|
||||
metadata.tags.push('light-code');
|
||||
} else {
|
||||
metadata.tags = ['light-code'];
|
||||
}
|
||||
app.route({
|
||||
id: routerItem.id,
|
||||
path: `${routerItem.id}__${routerItem.path}`,
|
||||
key: routerItem.key,
|
||||
description: routerItem.description || '',
|
||||
metadata,
|
||||
middleware: ['auth'],
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state?.tokenUser || {};
|
||||
const query = { ...ctx.query, tokenUser }
|
||||
const runRes2 = await runCode(tsPath, query, { timeout: 30000 });
|
||||
if (runRes2.success) {
|
||||
const res2 = runRes2.data;
|
||||
if (res2.code === 200) {
|
||||
ctx.body = res2.data;
|
||||
} else {
|
||||
ctx.throw(res2.code, res2.message || 'Lightcode 路由执行失败');
|
||||
}
|
||||
} else {
|
||||
ctx.throw(runRes2.error || 'Lightcode 路由执行失败');
|
||||
}
|
||||
}).addTo(app);
|
||||
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error('light-code 路由执行失败', runRes.error);
|
||||
}
|
||||
}
|
||||
console.log(`light-code 路由注册成功`, `注册${diffList.length}个路由`);
|
||||
}
|
||||
@@ -32,7 +32,7 @@ type RunCode = {
|
||||
};
|
||||
error?: any
|
||||
timestamp?: string
|
||||
[key: string]: any
|
||||
output?: string
|
||||
}
|
||||
export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?: RunCodeOptions): Promise<RunCode> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -43,7 +43,7 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let output = ''
|
||||
const timeoutMs = opts?.timeout || 30000; // 默认30秒超时
|
||||
|
||||
let child
|
||||
@@ -70,6 +70,7 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
|
||||
if (!resolved) {
|
||||
resolved = true
|
||||
cleanup()
|
||||
result.output = output
|
||||
resolve(result)
|
||||
}
|
||||
}
|
||||
@@ -77,7 +78,11 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
|
||||
try {
|
||||
// 使用 Bun 的 fork 模式启动子进程
|
||||
child = fork(tsPath, [], {
|
||||
silent: true // 启用 stdio 重定向
|
||||
silent: true, // 启用 stdio 重定向
|
||||
env: {
|
||||
...process.env,
|
||||
BUN_CHILD_PROCESS: 'true' // 标记为子进程
|
||||
}
|
||||
})
|
||||
// 监听来自子进程的消息
|
||||
child.on('message', (msg: RunCode) => {
|
||||
@@ -94,6 +99,11 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?:
|
||||
})
|
||||
})
|
||||
}
|
||||
if (child.stdout) {
|
||||
child.stdout.on('data', (data) => {
|
||||
output += data.toString()
|
||||
})
|
||||
}
|
||||
|
||||
// 监听子进程退出事件
|
||||
child.on('exit', (code, signal) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { App } from '@kevisual/router';
|
||||
import { loadAppInfo, AppInfoConfig, saveAppInfo, getAppsPath } from './app-file.ts';
|
||||
import { fork } from 'node:child_process';
|
||||
import { merge } from 'lodash-es';
|
||||
import { merge } from 'es-toolkit';
|
||||
import { deleteFileAppInfo } from './app-file.ts';
|
||||
import { fileIsExist } from '@kevisual/use-config/env';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -64,6 +64,10 @@ export class RemoteApp {
|
||||
throw new Error('No id provided for remote app');
|
||||
}
|
||||
this.isError = false;
|
||||
// 关闭已有连接
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
}
|
||||
const ws = new WebSocket(this.getWsURL(this.url));
|
||||
const that = this;
|
||||
ws.onopen = function () {
|
||||
|
||||
@@ -71,7 +71,7 @@ program
|
||||
.option('-n, --name <name>', '服务名称', 'assistant-server')
|
||||
.option('-p, --port <port>', '服务端口')
|
||||
.option('-s, --start', '是否启动服务')
|
||||
.option('-r, --root <root>', '工作空间路径')
|
||||
.option('-r, --root <root>', 'assistant app的根目录路径')
|
||||
.option('-i, --input <input>', '启动的输入文件,例如/workspace/src/main.ts')
|
||||
.option('-e, --interpreter <interpreter>', '指定使用的解释器', 'bun')
|
||||
.action(async (options) => {
|
||||
|
||||
@@ -136,30 +136,30 @@ export class AssistantInit extends AssistantConfig {
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "latest",
|
||||
"@kevisual/oss": "latest",
|
||||
"@kevisual/query": "latest",
|
||||
"@kevisual/router": "latest",
|
||||
"@kevisual/use-config": "latest",
|
||||
"better-sqlite3": "latest",
|
||||
"crypto-js": "latest",
|
||||
"dayjs": "latest",
|
||||
"dotenv": "latest",
|
||||
"es-toolkit": "latest",
|
||||
"eventemitter3": "latest",
|
||||
"ioredis": "latest",
|
||||
"minio": "latest",
|
||||
"node-cron": "latest",
|
||||
"pg": "latest",
|
||||
"pm2": "latest",
|
||||
"sequelize": "latest",
|
||||
"unstorage": "latest"
|
||||
"@aws-sdk/client-s3": "^3.975.0",
|
||||
"@kevisual/oss": "^0.0.16",
|
||||
"@kevisual/query": "^0.0.38",
|
||||
"eventemitter3": "^5.0.4",
|
||||
"@kevisual/router": "^0.0.62",
|
||||
"@kevisual/use-config": "^1.0.28",
|
||||
"ioredis": "^5.9.2",
|
||||
"minio": "^8.0.6",
|
||||
"pg": "^8.17.2",
|
||||
"pm2": "^6.0.14",
|
||||
"sequelize": "^6.37.7",
|
||||
"crypto-js": "^4.2.0",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"unstorage": "^1.17.4",
|
||||
"dayjs": "^1.11.19",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"dotenv": "^17.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/types": "^0.0.11",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@types/bun": "^1.3.6",
|
||||
"@types/crypto-js": "latest",
|
||||
"@types/node": "latest"
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^25.0.10"
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -12,29 +12,29 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "latest",
|
||||
"@kevisual/oss": "latest",
|
||||
"@kevisual/query": "latest",
|
||||
"@kevisual/router": "latest",
|
||||
"@kevisual/use-config": "latest",
|
||||
"better-sqlite3": "latest",
|
||||
"crypto-js": "latest",
|
||||
"dayjs": "latest",
|
||||
"dotenv": "latest",
|
||||
"es-toolkit": "latest",
|
||||
"eventemitter3": "latest",
|
||||
"ioredis": "latest",
|
||||
"minio": "latest",
|
||||
"node-cron": "latest",
|
||||
"pg": "latest",
|
||||
"pm2": "latest",
|
||||
"sequelize": "latest",
|
||||
"unstorage": "latest"
|
||||
"@aws-sdk/client-s3": "^3.975.0",
|
||||
"@kevisual/oss": "^0.0.16",
|
||||
"@kevisual/query": "^0.0.38",
|
||||
"eventemitter3": "^5.0.4",
|
||||
"@kevisual/router": "^0.0.62",
|
||||
"@kevisual/use-config": "^1.0.28",
|
||||
"ioredis": "^5.9.2",
|
||||
"minio": "^8.0.6",
|
||||
"pg": "^8.17.2",
|
||||
"pm2": "^6.0.14",
|
||||
"sequelize": "^6.37.7",
|
||||
"crypto-js": "^4.2.0",
|
||||
"better-sqlite3": "^12.6.2",
|
||||
"unstorage": "^1.17.4",
|
||||
"dayjs": "^1.11.19",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"dotenv": "^17.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/types": "^0.0.11",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@types/bun": "^1.3.6",
|
||||
"@types/crypto-js": "latest",
|
||||
"@types/node": "latest"
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^25.0.10"
|
||||
}
|
||||
}
|
||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kevisual/cli",
|
||||
"version": "0.0.92",
|
||||
"version": "0.0.94",
|
||||
"description": "envision 命令行工具",
|
||||
"type": "module",
|
||||
"basename": "/root/cli",
|
||||
@@ -46,16 +46,18 @@
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^8.2.0",
|
||||
"@kevisual/app": "^0.0.2",
|
||||
"@kevisual/auth": "^2.0.3",
|
||||
"@kevisual/context": "^0.0.4",
|
||||
"@kevisual/use-config": "^1.0.28",
|
||||
"@opencode-ai/sdk": "^1.1.28",
|
||||
"@opencode-ai/sdk": "^1.1.36",
|
||||
"@types/busboy": "^1.5.4",
|
||||
"busboy": "^1.6.0",
|
||||
"eventemitter3": "^5.0.4",
|
||||
"jose": "^6.1.3",
|
||||
"lowdb": "^7.0.1",
|
||||
"pm2": "latest",
|
||||
"lru-cache": "^11.2.4",
|
||||
"micromatch": "^4.0.8",
|
||||
"pm2": "latest",
|
||||
"semver": "^7.7.3",
|
||||
"unstorage": "^1.17.4"
|
||||
},
|
||||
@@ -63,13 +65,13 @@
|
||||
"@kevisual/dts": "^0.0.3",
|
||||
"@kevisual/load": "^0.0.6",
|
||||
"@kevisual/logger": "^0.0.4",
|
||||
"@kevisual/query": "0.0.37",
|
||||
"@kevisual/query": "0.0.38",
|
||||
"@kevisual/query-login": "0.0.7",
|
||||
"@types/bun": "^1.3.6",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/micromatch": "^4.0.10",
|
||||
"@types/node": "^25.0.9",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/semver": "^7.7.1",
|
||||
"chalk": "^5.6.2",
|
||||
"commander": "^14.0.2",
|
||||
@@ -89,4 +91,4 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
897
pnpm-lock.yaml
generated
897
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -171,7 +171,7 @@ const uploadFiles = async (files: string[], directory: string, opts: UploadFileO
|
||||
logger.error('请检查文件是否存在');
|
||||
}
|
||||
data.files.push({ path: file, hash: hash });
|
||||
if(filePath.includes('readme.md')) {
|
||||
if (filePath.includes('readme.md')) {
|
||||
description = fs.readFileSync(filePath, 'utf-8');
|
||||
}
|
||||
}
|
||||
@@ -215,8 +215,10 @@ const uploadFiles = async (files: string[], directory: string, opts: UploadFileO
|
||||
}
|
||||
const filename = path.basename(filePath);
|
||||
logger.debug('upload file', file, filename);
|
||||
// 解决 busbox 文件名乱码: 将 UTF-8 编码的文件名转换为 binary 字符串
|
||||
const encodedFilename = Buffer.from(filename, 'utf-8').toString('binary');
|
||||
form.append('file', fs.createReadStream(filePath), {
|
||||
filename: filename,
|
||||
filename: encodedFilename,
|
||||
filepath: file,
|
||||
});
|
||||
needUpload = true;
|
||||
|
||||
74
src/command/jwks.ts
Normal file
74
src/command/jwks.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { generate } from '@kevisual/auth'
|
||||
|
||||
import { program, Command } from '@/program.ts';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
export const getPath = async (dir: string) => {
|
||||
const JWKS_PATH = path.join(dir, 'jwks.json');
|
||||
const PRIVATE_JWK_PATH = path.join(dir, 'privateKey.json');
|
||||
|
||||
const PRIVATE_KEY_PATH = path.join(dir, 'privateKey.txt');
|
||||
const PUBLIC_KEY_PATH = path.join(dir, 'publicKey.txt');
|
||||
return {
|
||||
JWKS_PATH,
|
||||
PRIVATE_JWK_PATH,
|
||||
PRIVATE_KEY_PATH,
|
||||
PUBLIC_KEY_PATH,
|
||||
}
|
||||
}
|
||||
const jwksCmd = new Command('jwks')
|
||||
.description('JWKS 相关命令')
|
||||
.action(async (opts) => {
|
||||
});
|
||||
|
||||
const jwksGenerate = new Command('generate')
|
||||
.alias('gen')
|
||||
.option('-d , --dir <dir>', '指定保存目录,默认当前目录下 jwt 文件夹', 'jwt')
|
||||
.description('生成 JWKS 密钥对')
|
||||
.action(async (opts) => {
|
||||
const dir = path.isAbsolute(opts.dir) ? opts.dir : path.join(process.cwd(), opts.dir);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const { JWKS_PATH, PRIVATE_JWK_PATH, PRIVATE_KEY_PATH, PUBLIC_KEY_PATH } = await getPath(dir);
|
||||
const { jwks, privateJWK, privatePEM, publicPEM } = await generate();
|
||||
fs.writeFileSync(PUBLIC_KEY_PATH, publicPEM);
|
||||
fs.writeFileSync(PRIVATE_KEY_PATH, privatePEM);
|
||||
fs.writeFileSync(PRIVATE_JWK_PATH, JSON.stringify(privateJWK, null, 2));
|
||||
fs.writeFileSync(JWKS_PATH, JSON.stringify(jwks, null, 2));
|
||||
console.log(`Keys have been saved to directory: ${dir}`);
|
||||
});
|
||||
|
||||
jwksCmd.addCommand(jwksGenerate);
|
||||
|
||||
const getJWKS = new Command('get')
|
||||
.description('获取 JWKS 内容')
|
||||
.option('-d , --dir <dir>', '指定 JWKS 所在目录,默认当前目录下 jwt 文件夹', 'jwt')
|
||||
.option('-t, --type <type>', '指定获取类型,jwks 或 privateJWK', 'jwks')
|
||||
.action(async (opts) => {
|
||||
const dir = path.isAbsolute(opts.dir) ? opts.dir : path.join(process.cwd(), opts.dir);
|
||||
const { JWKS_PATH, PRIVATE_JWK_PATH } = await getPath(dir);
|
||||
const type = opts.type || 'jwks';
|
||||
if (type !== 'jwks') {
|
||||
if (!fs.existsSync(PRIVATE_JWK_PATH)) {
|
||||
console.error(`Private JWK file not found in directory: ${dir}`);
|
||||
return;
|
||||
}
|
||||
const privateJWKContent = fs.readFileSync(PRIVATE_JWK_PATH, 'utf-8');
|
||||
console.log('Private JWK:\n');
|
||||
console.log(privateJWKContent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(JWKS_PATH)) {
|
||||
console.error(`JWKS file not found in directory: ${dir}`);
|
||||
return;
|
||||
}
|
||||
const jwksContent = fs.readFileSync(JWKS_PATH, 'utf-8');
|
||||
console.log('PublicJWKS:\n');
|
||||
console.log(jwksContent);
|
||||
});
|
||||
|
||||
jwksCmd.addCommand(getJWKS);
|
||||
|
||||
program.addCommand(jwksCmd);
|
||||
@@ -6,8 +6,9 @@ import { getConfig, query } from '@/module/index.ts';
|
||||
import { fileIsExist } from '@/uitls/file.ts';
|
||||
import { chalk } from '@/module/chalk.ts';
|
||||
import * as backServices from '@/query/services/index.ts';
|
||||
import { select, input } from '@inquirer/prompts';
|
||||
import { input } from '@inquirer/prompts';
|
||||
import { logger } from '@/module/logger.ts';
|
||||
|
||||
// 查找文件(忽略大小写)
|
||||
async function findFileInsensitive(targetFile: string): Promise<string | null> {
|
||||
const files = fs.readdirSync('.');
|
||||
@@ -249,14 +250,9 @@ const packCommand = new Command('pack')
|
||||
let appKey: string | undefined;
|
||||
let version = packageInfo.version || '';
|
||||
if (!version) {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'version',
|
||||
message: 'Enter your version:',
|
||||
},
|
||||
]);
|
||||
version = answers.version || version;
|
||||
version = await input({
|
||||
message: 'Enter your version:',
|
||||
});
|
||||
}
|
||||
|
||||
if (basename) {
|
||||
@@ -271,14 +267,9 @@ const packCommand = new Command('pack')
|
||||
appKey = basenameArr[1] || '';
|
||||
}
|
||||
if (!appKey) {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
type: 'input',
|
||||
name: 'appKey',
|
||||
message: 'Enter your appKey:',
|
||||
},
|
||||
]);
|
||||
appKey = answers.appKey || appKey;
|
||||
appKey = await input({
|
||||
message: 'Enter your appKey:',
|
||||
});
|
||||
}
|
||||
let value = await pack({
|
||||
packDist,
|
||||
|
||||
@@ -19,6 +19,8 @@ import './command/config-secret-remote.ts';
|
||||
import './command/ai.ts';
|
||||
import './command/claude/cc.ts'
|
||||
import './command/docker.ts';
|
||||
import './command/jwks.ts';
|
||||
|
||||
// program.parse(process.argv);
|
||||
|
||||
export const runParser = async (argv: string[]) => {
|
||||
|
||||
Reference in New Issue
Block a user