From 15d81c4f6855c0b9ded556c2213ab272be0f8280 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Mon, 26 Jan 2026 04:18:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BE=9D=E8=B5=96=E9=A1=B9?= =?UTF-8?q?=E7=89=88=E6=9C=AC=EF=BC=8C=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=93=88=E5=B8=8C=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E8=BF=9C=E7=A8=8B=E5=BA=94=E7=94=A8=E8=BF=9E=E6=8E=A5=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assistant/package.json | 4 +- .../local-app-manager/assistant-app.ts | 6 +- assistant/src/module/file-hash.ts | 16 ++ assistant/src/module/light-code/index.ts | 143 +++++++++++++++--- assistant/src/module/light-code/run.ts | 15 +- assistant/src/module/remote-app/remote-app.ts | 4 + pnpm-lock.yaml | 21 ++- 7 files changed, 174 insertions(+), 35 deletions(-) create mode 100644 assistant/src/module/file-hash.ts diff --git a/assistant/package.json b/assistant/package.json index 3ae3a4d..57d0b60 100644 --- a/assistant/package.json +++ b/assistant/package.json @@ -51,7 +51,7 @@ "@kevisual/router": "^0.0.62", "@kevisual/types": "^0.0.12", "@kevisual/use-config": "^1.0.28", - "@opencode-ai/plugin": "^1.1.35", + "@opencode-ai/plugin": "^1.1.36", "@types/bun": "^1.3.6", "@types/node": "^25.0.10", "@types/send": "^1.2.1", @@ -81,7 +81,7 @@ "@kevisual/js-filter": "^0.0.5", "@kevisual/oss": "^0.0.16", "@kevisual/video-tools": "^0.0.13", - "@opencode-ai/sdk": "^1.1.35", + "@opencode-ai/sdk": "^1.1.36", "es-toolkit": "^1.44.0", "eventemitter3": "^5.0.4", "lowdb": "^7.0.1", diff --git a/assistant/src/module/assistant/local-app-manager/assistant-app.ts b/assistant/src/module/assistant/local-app-manager/assistant-app.ts index 74dc945..a6423dd 100644 --- a/assistant/src/module/assistant/local-app-manager/assistant-app.ts +++ b/assistant/src/module/assistant/local-app-manager/assistant-app.ts @@ -112,7 +112,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 { @@ -190,6 +190,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(); diff --git a/assistant/src/module/file-hash.ts b/assistant/src/module/file-hash.ts new file mode 100644 index 0000000..d820721 --- /dev/null +++ b/assistant/src/module/file-hash.ts @@ -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'); +} \ No newline at end of file diff --git a/assistant/src/module/light-code/index.ts b/assistant/src/module/light-code/index.ts index 3999427..cd438ca 100644 --- a/assistant/src/module/light-code/index.ts +++ b/assistant/src/module/light-code/index.ts @@ -1,70 +1,169 @@ import { App, QueryRouterServer } from '@kevisual/router'; -import { AssistantConfig } from '../assistant/index.ts'; +import { AssistantInit } from '../../services/init/index.ts'; import path from 'node:path'; -import fs from 'node:fs'; +import fs, { write } from 'node:fs'; +import os from 'node:os'; import glob from 'fast-glob'; import { runCode } from './run.ts'; -import { pick } from 'es-toolkit'; const codeDemoId = '0e700dc8-90dd-41b7-91dd-336ea51de3d2' import { filter } from "@kevisual/js-filter"; -const codeDemo = `// 这是一个示例代码文件 -import {Mini} from '@kevisual/router'; +import { getHash, getStringHash } from '../file-hash.ts'; -const app = new Mini(); +const codeDemo = `// 这是一个示例代码文件 +import {App} from '@kevisual/router'; + +const app = new App(); app.route({ path: 'hello', - describetion: 'LightCode 示例路由', + 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 + config: AssistantInit + sync?: boolean +} +type LightCodeFile = { + id?: string, code?: string, hash?: string, filepath: string } export const initLightCode = async (opts: opts) => { - // 注册 lightcode 路由 - console.log('初始化 lightcode 路由'); + // 注册 light-code 路由 + console.log('初始化 light-code 路由'); const config = opts.config; + 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 (codeFiles.length === 0) { - // 如果没有代码文件,创建一个示例文件 - const demoPath = path.join(lightcodeDir, `${codeDemoId}.ts`); - fs.writeFileSync(demoPath, codeDemo, 'utf-8'); + + 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 codeFiles) { - const tsPath = path.join(lightcodeDir, file); + + 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) { - const pickValues = pick(routerItem, ['path', 'id', 'description', 'metadata']); if (routerItem.path?.includes('auth') || routerItem.path?.includes('router') || routerItem.path?.includes('call')) { continue; } - console.log('lightcode 路由必须包含 path 和 id', pickValues); - console.log(`注册 lightcode 路由: ${routerItem.path} ${routerItem.id} 来自文件: ${file}`); + // console.log(`注册 light-code 路由: [${routerItem.path}] ${routerItem.id} 来自文件: ${file.filepath}`); + app.route({ + id: routerItem.id, + path: routerItem.id!, + description: routerItem.description || '', + metadata: routerItem.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); - const runRes2 = await runCode(tsPath, { path: routerItem.path, key: routerItem.key, id: routerItem.id }, { timeout: 10000 }); - console.log('lightcode 路由执行结果', runRes2); } } } else { - console.error('lightcode 路由执行失败', runRes.error); + console.error('light-code 路由执行失败', runRes.error); } } + console.log(`light-code 路由注册成功`, `注册${diffList.length}个路由`); } \ No newline at end of file diff --git a/assistant/src/module/light-code/run.ts b/assistant/src/module/light-code/run.ts index cfba2d4..cd91a09 100644 --- a/assistant/src/module/light-code/run.ts +++ b/assistant/src/module/light-code/run.ts @@ -32,6 +32,7 @@ type RunCode = { }; error?: any timestamp?: string + output?: string } export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?: RunCodeOptions): Promise => { return new Promise((resolve, reject) => { @@ -42,7 +43,7 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?: }) return } - + let output = '' const timeoutMs = opts?.timeout || 30000; // 默认30秒超时 let child @@ -69,6 +70,7 @@ export const runCode = async (tsPath: string, params: RunCodeParams = {}, opts?: if (!resolved) { resolved = true cleanup() + result.output = output resolve(result) } } @@ -76,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) => { @@ -93,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) => { diff --git a/assistant/src/module/remote-app/remote-app.ts b/assistant/src/module/remote-app/remote-app.ts index c31b532..5e3a64f 100644 --- a/assistant/src/module/remote-app/remote-app.ts +++ b/assistant/src/module/remote-app/remote-app.ts @@ -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 () { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac28043..57ce126 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -139,8 +139,8 @@ importers: specifier: ^0.0.13 version: 0.0.13(dotenv@17.2.3)(supports-color@10.2.2) '@opencode-ai/sdk': - specifier: ^1.1.35 - version: 1.1.35 + specifier: ^1.1.36 + version: 1.1.36 es-toolkit: specifier: ^1.44.0 version: 1.44.0 @@ -191,8 +191,8 @@ importers: specifier: ^1.0.28 version: 1.0.28(dotenv@17.2.3) '@opencode-ai/plugin': - specifier: ^1.1.35 - version: 1.1.35 + specifier: ^1.1.36 + version: 1.1.36 '@types/bun': specifier: ^1.3.6 version: 1.3.6 @@ -1468,12 +1468,15 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@opencode-ai/plugin@1.1.35': - resolution: {integrity: sha512-mn96oPcPxAjBcRuG/ivtJAOujJeyUPmL+D+/79Fs29MqIkfxJ/x+SVfNf8IXTFfkyt8FzZ3gF+Vuk1z/QjTkPA==} + '@opencode-ai/plugin@1.1.36': + resolution: {integrity: sha512-b2XWeFZN7UzgwkkzTIi6qSntkpEA9En2zvpqakQzZAGQm6QBdGAlv6r1u5hEnmF12Gzyj5umTMWr5GzVbP/oAA==} '@opencode-ai/sdk@1.1.35': resolution: {integrity: sha512-1RfjXvc5nguurpGXyKk8aJ4Rb3ix1IZ5V7itPB3SMq7c6OkmbE/5wzN2KUT9zATWj7ZDjmShkxEjvkRsOhodtw==} + '@opencode-ai/sdk@1.1.36': + resolution: {integrity: sha512-feNHWnbxhg03TI2QrWnw3Chc0eYrWSDSmHIy/ejpSVfcKlfXREw1Tpg0L4EjrpeSc4jB1eM673dh+WM/Ko2SFQ==} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} @@ -6814,13 +6817,15 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - '@opencode-ai/plugin@1.1.35': + '@opencode-ai/plugin@1.1.36': dependencies: - '@opencode-ai/sdk': 1.1.35 + '@opencode-ai/sdk': 1.1.36 zod: 4.1.8 '@opencode-ai/sdk@1.1.35': {} + '@opencode-ai/sdk@1.1.36': {} + '@oslojs/encoding@1.1.0': {} '@peculiar/asn1-cms@2.6.0':