import { App, QueryRouterServer } from '@kevisual/router'; import { AssistantInit } from '../../services/init/index.ts'; import path from 'node:path'; import fs from 'node:fs'; import glob from 'fast-glob'; import { runCode } from './run.ts'; const codeDemoId = '0e700dc8-90dd-41b7-91dd-336ea51de3d2' import { getHash, getStringHash } from '../file-hash.ts'; import { AssistantConfig } from '@/lib.ts'; import { assistantQuery } from '@/app.ts'; import { logger } from '../logger.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?: 'remote' | 'local' | 'both' rootPath?: string } type LightCodeFile = { id?: string, code?: string, hash?: string, filepath: string } export const initLightCode = async (opts: Opts) => { const token = await assistantQuery.getToken(); if (!token) { logger.error('[light-code] 当前未登录,无法初始化 light-code'); return; } // 注册 light-code 路由 const config = opts.config as AssistantInit; const app = opts.router; logger.log('[light-code] 初始化 light-code 路由'); const query = config.query; const sync = opts.sync ?? 'remote'; if (!config || !app) { logger.error('[light-code] initLightCode 缺少必要参数, config 或 app'); return; } const lightcodeDir = opts.rootPath; if (!fs.existsSync(lightcodeDir)) { fs.mkdirSync(lightcodeDir, { recursive: true }); } let diffList: LightCodeFile[] = []; const findGlob = (opts: { cwd: string }) => { return glob.sync(['**/*.ts', '**/*.js'], { cwd: opts.cwd, onlyFiles: true, }).map(file => { return { filepath: path.join(opts.cwd, file), // hash: getHash(path.join(lightcodeDir, file)) } }); } const codeFiles = findGlob({ cwd: lightcodeDir }); if (sync === 'remote' || sync === 'both') { 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 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 // })); if (sync === 'remote') { // 执行删除 for (const filepath of toDelete) { // console.log(`删除 light-code 文件: ${filepath.filepath}`); const parentDir = path.dirname(filepath.filepath); // console.log('parentDir', parentDir, lightcodeDir); if (parentDir === lightcodeDir) { fs.unlinkSync(filepath.filepath); } } } diffList = findGlob({ cwd: lightcodeDir }); } else { console.error('[light-code] 同步失败', queryRes.message); diffList = codeFiles; } } else if (sync === 'local') { diffList = codeFiles; } for (const file of diffList) { const tsPath = file.filepath; const runRes = await runCode(tsPath, { message: { path: 'router', key: 'list' } }, { timeout: 10000 }); // console.log('light-code 运行结果', file.filepath, runRes); if (runRes.success) { const res = runRes.data; if (res.code === 200) { const list = res.data?.list || []; for (const routerItem of list) { // console.log('注册 light-code 路由项:', routerItem.id, routerItem.path); 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']; } metadata.source = 'light-code'; metadata['light-code'] = { id: file.id }; (app as 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 } const runRes2 = await runCode(tsPath, { message: query, context: { state: { tokenUser, user: tokenUser }, } }, { 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, { overwrite: false });// 不允许覆盖已存在的路由 // console.log(`light-code 路由注册成功: [${routerItem.path}] ${routerItem.id} 来自文件: ${file.filepath}`); } } } else { console.error('[light-code] 路由执行失败', runRes.error); } } console.log(`[light-code] 路由注册成功`, `注册${diffList.length}个路由`); } export const clearLightCodeRoutes = (opts: Pick) => { const app = opts.router; if (!app) { console.error('[light-code] clearLightCodeRoutes 缺少必要参数, app'); return; } const routes = app.getList(); for (const route of routes) { if (route.metadata?.source === 'light-code') { // console.log(`[light-code] 删除 light-code 路由: ${route.path} ${route.id}`); app.removeById(route.id); } } }