From a76c2235ea221bfd0c349fe2492bf6a57c14d7a5 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Mon, 2 Feb 2026 23:29:58 +0800 Subject: [PATCH] feat: Implement LiveCode module with WebSocket and SSE support - Added config management using `useConfig` for environment variables. - Created `LiveCode` class to manage WebSocket connections and routing. - Implemented `SSEManager` for Server-Sent Events handling. - Developed `WSSManager` for managing WebSocket connections with heartbeat functionality. - Introduced `ReconnectingWebSocket` class for robust WebSocket client with automatic reconnection. - Added test files for live application demonstrating WebSocket and TCP server integration. --- assistant/package.json | 19 +- assistant/src/app.ts | 5 +- .../src/module/assistant/config/index.ts | 2 + .../assistant-app-resolve.ts | 18 +- .../local-app-manager/assistant-app.ts | 2 +- assistant/src/module/config.ts | 10 + assistant/src/module/light-code/index.ts | 6 +- assistant/src/module/livecode/index.ts | 131 ++++++++ assistant/src/module/livecode/sse.ts | 134 ++++++++ assistant/src/module/livecode/wss.ts | 213 +++++++++++++ assistant/src/module/local-proxy/proxy.ts | 2 +- .../src/routes/hot-api/key-sender/index.ts | 20 -- .../src/routes/hot-api/key-sender/lib.ts | 89 ------ assistant/src/routes/index.ts | 2 - assistant/src/server.ts | 3 +- .../src/services/proxy/proxy-page-index.ts | 24 +- assistant/src/test/live-app-origin.ts | 215 +++++++++++++ assistant/src/test/live-app.ts | 75 +++++ pnpm-lock.yaml | 286 ++---------------- 19 files changed, 871 insertions(+), 385 deletions(-) create mode 100644 assistant/src/module/config.ts create mode 100644 assistant/src/module/livecode/index.ts create mode 100644 assistant/src/module/livecode/sse.ts create mode 100644 assistant/src/module/livecode/wss.ts delete mode 100644 assistant/src/routes/hot-api/key-sender/index.ts delete mode 100644 assistant/src/routes/hot-api/key-sender/lib.ts create mode 100644 assistant/src/test/live-app-origin.ts create mode 100644 assistant/src/test/live-app.ts diff --git a/assistant/package.json b/assistant/package.json index aa509ec..c6cb5c7 100644 --- a/assistant/package.json +++ b/assistant/package.json @@ -25,6 +25,7 @@ "dev:share": "bun --watch src/test/remote-app.ts ", "build:lib": "bun run bun-lib.config.mjs", "postbuild:lib": "dts -i src/lib.ts -o assistant-lib.d.ts -d libs -t", + "dev:live": "bun --watch src/test/live-app.ts ", "build": "rimraf dist && bun run bun.config.mjs" }, "bin": { @@ -43,19 +44,18 @@ "devDependencies": { "@inquirer/prompts": "^8.2.0", "@kevisual/ai": "^0.0.24", - "@kevisual/api": "^0.0.35", - "@kevisual/cnb": "^0.0.13", + "@kevisual/api": "^0.0.42", "@kevisual/load": "^0.0.6", "@kevisual/local-app-manager": "^0.1.32", "@kevisual/logger": "^0.0.4", - "@kevisual/query": "0.0.38", + "@kevisual/query": "0.0.39", "@kevisual/query-login": "0.0.7", - "@kevisual/router": "^0.0.64", + "@kevisual/router": "^0.0.67", "@kevisual/types": "^0.0.12", - "@kevisual/use-config": "^1.0.28", - "@opencode-ai/plugin": "^1.1.47", + "@kevisual/use-config": "^1.0.30", + "@opencode-ai/plugin": "^1.1.48", "@types/bun": "^1.3.8", - "@types/node": "^25.1.0", + "@types/node": "^25.2.0", "@types/send": "^1.2.1", "@types/ws": "^8.18.1", "chalk": "^5.6.2", @@ -78,11 +78,10 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.980.0", - "@kevisual/ha-api": "^0.0.8", "@kevisual/js-filter": "^0.0.5", - "@kevisual/oss": "^0.0.18", + "@kevisual/oss": "^0.0.19", "@kevisual/video-tools": "^0.0.13", - "@opencode-ai/sdk": "^1.1.47", + "@opencode-ai/sdk": "^1.1.48", "es-toolkit": "^1.44.0", "eventemitter3": "^5.0.4", "lowdb": "^7.0.1", diff --git a/assistant/src/app.ts b/assistant/src/app.ts index 0f7088e..ca5379a 100644 --- a/assistant/src/app.ts +++ b/assistant/src/app.ts @@ -7,7 +7,8 @@ import { AssistantInit, parseHomeArg } from '@/services/init/index.ts'; import { configDir as HomeConfigDir } from '@/module/assistant/config/index.ts'; import { useContextKey } from '@kevisual/use-config/context'; import { AssistantQuery } from '@/module/assistant/query/index.ts'; - +import { config } from '@/module/config.ts'; +export { config }; const manualParse = parseHomeArg(HomeConfigDir); const _configDir = manualParse.configDir; export const configDir = AssistantInit.detectConfigDir(_configDir); @@ -29,7 +30,7 @@ type Runtime = { isServer?: boolean; } export const runtime: Runtime = useContextKey('runtime', () => { - console.log('Runtime detected:', manualParse); + console.log('Runtime detected:', manualParse.isDev); return { type: 'client', isServer: manualParse.isServer, diff --git a/assistant/src/module/assistant/config/index.ts b/assistant/src/module/assistant/config/index.ts index aa23629..88088e6 100644 --- a/assistant/src/module/assistant/config/index.ts +++ b/assistant/src/module/assistant/config/index.ts @@ -107,11 +107,13 @@ export type AssistantConfigData = { * 例子: { proxy: [ { type: 'router', api: 'https://localhost:50002/api/router' } ] } * base: 是否使用 /api/router的基础路径,默认false * lightcode: 是否启用lightcode路由,默认false + * livecode: 是否启用livecode路由,实时的注册和销毁,默认false */ router?: { proxy: ProxyInfo[]; base?: boolean; lightcode?: boolean; + livecode?: boolean; } routes?: AssistantRoutes[], /** diff --git a/assistant/src/module/assistant/local-app-manager/assistant-app-resolve.ts b/assistant/src/module/assistant/local-app-manager/assistant-app-resolve.ts index bc9205b..682fb9e 100644 --- a/assistant/src/module/assistant/local-app-manager/assistant-app-resolve.ts +++ b/assistant/src/module/assistant/local-app-manager/assistant-app-resolve.ts @@ -36,7 +36,23 @@ export class ModuleResolver { // 相对路径 ./xxx 或 ../xxx const localFullPath = path.resolve(this.root, routePath); - return this.fileIsExists(localFullPath) ? localFullPath : routePath; + if (!this.fileIsExists(localFullPath)) { + return routePath; + } + + // 如果是目录,解析入口文件 + if (fs.statSync(localFullPath).isDirectory()) { + const pkgJsonPath = path.join(localFullPath, 'package.json'); + const pkg = this.readPackageJson(pkgJsonPath); + if (pkg) { + const entryPath = this.resolvePackageExport(pkg, ''); + return path.join(localFullPath, entryPath); + } + // 没有 package.json,默认使用 index.ts + return path.join(localFullPath, 'index.ts'); + } + + return localFullPath; } /** 解析 scoped 包 */ 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 d6f2b98..52c22ed 100644 --- a/assistant/src/module/assistant/local-app-manager/assistant-app.ts +++ b/assistant/src/module/assistant/local-app-manager/assistant-app.ts @@ -222,7 +222,7 @@ export class AssistantApp extends Manager { const routeStr = typeof route === 'string' ? route : route.path; const resolvedPath = this.resolver.resolve(routeStr); await import(resolvedPath); - console.log('路由已初始化', route); + console.log('[routes] 路由已初始化', route, resolvedPath); } catch (err) { console.error('初始化路由失败', route, err); } diff --git a/assistant/src/module/config.ts b/assistant/src/module/config.ts new file mode 100644 index 0000000..442d788 --- /dev/null +++ b/assistant/src/module/config.ts @@ -0,0 +1,10 @@ +import { useConfig } from '@kevisual/use-config'; +import { HomeConfigDir } from './assistant/config/index.ts'; +import path from 'node:path'; +export const config = useConfig({ + dotenvOpts: { + path: [path.join(HomeConfigDir, '.env'), '.env'], + } +}) + +// console.log('配置文件目录:', config, HomeConfigDir); \ 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 0c26a1f..409b9d5 100644 --- a/assistant/src/module/light-code/index.ts +++ b/assistant/src/module/light-code/index.ts @@ -165,7 +165,11 @@ export const initLightCode = async (opts: opts) => { } else { ctx.throw(runRes2.error || 'Lightcode 路由执行失败'); } - }).addTo(app); + }).addTo(app, { + override: false, + // @ts-ignore + overwrite: false + });// 不允许覆盖已存在的路由 } } diff --git a/assistant/src/module/livecode/index.ts b/assistant/src/module/livecode/index.ts new file mode 100644 index 0000000..3726343 --- /dev/null +++ b/assistant/src/module/livecode/index.ts @@ -0,0 +1,131 @@ +import { WSSManager } from './wss.ts'; +import { App, Route } from '@kevisual/router' +import { WebSocketReq } from '@kevisual/router' +import { EventEmitter } from 'eventemitter3'; +import { customAlphabet } from 'nanoid'; + +const letter = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; +const customId = customAlphabet(letter, 16); +export class LiveCode { + wssManager: WSSManager; + app: App; + emitter: EventEmitter; + constructor(app: App) { + this.wssManager = new WSSManager({ heartbeatInterval: 5000 }); + this.app = app; + this.emitter = new EventEmitter(); + console.log('[LiveCode] 模块已初始化'); + } + async conn(req: WebSocketReq) { + const { ws, emitter, id } = req; + const that = this; + // @ts-ignore + let wid = ws.data?.wid; + if (!wid) { + const _id = this.wssManager.addConnection(req, { userId: id }); + // @ts-ignore + ws.data.wid = _id; + emitter.once('close--' + id, () => { + that.wssManager.closeConnection(_id); + this.deinitAppRoutes(_id); + }); + console.log('[LiveCode]新的 WebSocket 连接已打开', _id); + const res = await that.init(_id); + if (res.code === 200) { + console.log('[LiveCode]初始化路由列表完成'); + that.initAppRoutes(res.data?.list || [], _id); + } else { + console.error('[LiveCode]初始化路由列表失败:', res?.message); + } + return this; + } + that.onMessage(req); + return this; + } + getWss(id: string) { + return this.wssManager.getConnection(id) + } + async init(id: string): Promise<{ code: number, message?: string, data?: any }> { + return this.sendData({ path: 'router', key: 'list', }, id); + } + sendData(data: any, id: string): Promise<{ code: number, message?: string, data?: any }> { + const reqId = customId() + const wss = this.getWss(id); + if (!wss) { + return Promise.resolve({ code: 500, message: '连接不存在或已关闭' }); + } + const emitter = this.emitter; + const wsReq = wss.wsReq; + try { + wsReq.ws.send(JSON.stringify({ + type: 'router', + id: reqId, + data: data + })); + } catch (error) { + console.error('[LiveCode]发送数据失败:', error); + return Promise.resolve({ code: 500, message: '发送数据失败' }); + } + return new Promise((resolve) => { + const timeout = setTimeout(() => { + resolve({ code: 500, message: '请求超时' }); + emitter.off(reqId, listenOnce); + }, 5000); + const listenOnce = (resData: any) => { + clearTimeout(timeout); + resolve(resData); + emitter.off(reqId, listenOnce); + } + emitter.once(reqId, listenOnce); + }); + } + onMessage(req: WebSocketReq) { + const { data } = req; + if (data?.id) { + // console.log('LiveCode 收到消息:', data); + this.emitter.emit(data.id, data.data); + } else { + console.warn('[LiveCode] 未知的消息格式', data); + } + } + initAppRoutes(list: Route[], wid: string) { + for (const route of list) { + const path = route.path || ''; + const id = route.id || ''; + if (path.startsWith('router') || path.startsWith('auth') || path.startsWith('admin-autu') || path.startsWith('call')) { + continue; + } + // console.log('注册路由:', route.path, route.description, route.metadata, route.id); + this.app.route({ + path: route.id, + key: route.key, + description: route.description, + metadata: { + ...route.metadata, + liveCodeId: wid + }, + middleware: ['auth'], + }).define(async (ctx) => { + const { token, cookie, ...rest } = ctx.query; + const tokenUser = ctx.state.tokernUser; + const res = await this.sendData({ + id: route.id, + tokenUser, + payload: rest, + }, wid); + // console.log('路由响应数据:', res); + ctx.forward(res) + }).addTo(this.app, { + // override: false, + // // @ts-ignore + // overwrite: false + }); + } + } + deinitAppRoutes(wid: string) { + const routesToRemove = this.app.routes.filter(route => route.metadata?.liveCodeId === wid); + for (const route of routesToRemove) { + this.app.removeById(route.id); + } + } +} \ No newline at end of file diff --git a/assistant/src/module/livecode/sse.ts b/assistant/src/module/livecode/sse.ts new file mode 100644 index 0000000..24ceda6 --- /dev/null +++ b/assistant/src/module/livecode/sse.ts @@ -0,0 +1,134 @@ +import { nanoid } from "nanoid"; +type ConnectionInfo = { + id: string; + writer: WritableStreamDefaultWriter; + stream: ReadableStream; + connectedAt: Date; + heartbeatInterval: NodeJS.Timeout | null; + userId?: string; +}; +export class SSEManager { + private connections: Map = new Map(); + private userConnections: Map> = new Map(); // userId -> connectionIds + + constructor() { + // 初始化逻辑 + } + createConnection(info?: { userId?: string }): ConnectionInfo { + const connectionId = nanoid(16); + const { readable, writable } = new TransformStream(); + const writer = writable.getWriter(); + + // 存储连接信息 + const connectionInfo = { + id: connectionId, + writer, + stream: readable, + connectedAt: new Date(), + heartbeatInterval: null, + userId: info?.userId + }; + + this.connections.set(connectionId, connectionInfo); + + // 添加到用户索引 + if (info?.userId) { + const userSet = this.userConnections.get(info.userId) || new Set(); + userSet.add(connectionId); + this.userConnections.set(info.userId, userSet); + } + + return connectionInfo; + } + + sendToConnection(connectionId: string, data: any) { + const connection = this.connections.get(connectionId); + if (connection) { + const message = `data: ${JSON.stringify(data)}\n\n`; + return connection.writer.write(new TextEncoder().encode(message)); + } + throw new Error(`Connection ${connectionId} not found`); + } + + getConnection(connectionId: string) { + return this.connections.get(connectionId); + } + + broadcast(data: any, opts?: { userId?: string }) { + const message = `data: ${JSON.stringify(data)}\n\n`; + const promises = []; + + // 指定 userId:只发送给目标用户(通过索引快速查找) + if (opts?.userId) { + const userConnIds = this.userConnections.get(opts.userId); + if (userConnIds) { + for (const connId of userConnIds) { + const conn = this.connections.get(connId); + if (conn) { + promises.push( + conn.writer.write(new TextEncoder().encode(message)) + .catch(() => { + this.closeConnection(connId); + }) + ); + } + } + } + return Promise.all(promises); + } + + // 未指定 userId:广播给所有人 + for (const [id, connection] of this.connections) { + promises.push( + connection.writer.write(new TextEncoder().encode(message)) + .catch(() => { + this.closeConnection(id); + }) + ); + } + + return Promise.all(promises); + } + + closeConnection(connectionId: string) { + const connection = this.connections.get(connectionId); + if (connection) { + // 清理心跳定时器 + if (connection.heartbeatInterval) { + clearInterval(connection.heartbeatInterval); + } + + // 从用户索引中移除 + if (connection.userId) { + const userSet = this.userConnections.get(connection.userId); + if (userSet) { + userSet.delete(connectionId); + if (userSet.size === 0) { + this.userConnections.delete(connection.userId); + } + } + } + + // 关闭写入器 + connection.writer.close().catch(console.error); + + // 从管理器中移除 + this.connections.delete(connectionId); + + console.log(`Connection ${connectionId} closed`); + return true; + } + return false; + } + + closeAllConnections() { + for (const [connectionId, connection] of this.connections) { + this.closeConnection(connectionId); + } + } + + getActiveConnections() { + return Array.from(this.connections.keys()); + } +} + diff --git a/assistant/src/module/livecode/wss.ts b/assistant/src/module/livecode/wss.ts new file mode 100644 index 0000000..557278d --- /dev/null +++ b/assistant/src/module/livecode/wss.ts @@ -0,0 +1,213 @@ +import { nanoid } from "nanoid"; +import { WebSocketReq } from '@kevisual/router' +type ConnectionInfo = { + id: string; + wsReq: WebSocketReq; + connectedAt: Date; + heartbeatInterval: NodeJS.Timeout | null; + userId?: string; + lastHeartbeat: Date; +}; + +export class WSSManager { + private connections: Map = new Map(); + private userConnections: Map> = new Map(); + private heartbeatInterval: number = 30000; // 默认30秒 + + constructor(opts?: { heartbeatInterval?: number }) { + if (opts?.heartbeatInterval) { + this.heartbeatInterval = opts.heartbeatInterval; + } + } + + /** + * 添加 WebSocket 连接 + */ + addConnection(wsReq: WebSocketReq, info?: { userId?: string }): string { + const connectionId = nanoid(16); + const now = new Date(); + + const connectionInfo: ConnectionInfo = { + id: connectionId, + wsReq: wsReq, + connectedAt: now, + heartbeatInterval: null, + userId: info?.userId, + lastHeartbeat: now, + }; + + // 启动心跳 + this.startHeartbeat(connectionInfo); + + // 存储连接 + this.connections.set(connectionId, connectionInfo); + + // 添加到用户索引 + if (info?.userId) { + const userSet = this.userConnections.get(info.userId) || new Set(); + userSet.add(connectionId); + this.userConnections.set(info.userId, userSet); + } + return connectionId; + } + + /** + * 启动心跳 + */ + private startHeartbeat(connection: ConnectionInfo) { + connection.heartbeatInterval = setInterval(() => { + const ws = connection.wsReq.ws; + ws.send(JSON.stringify({ type: 'heartbeat', timestamp: new Date().toISOString() })); + connection.lastHeartbeat = new Date(); + console.log(`[LiveCode] 发送心跳给连接 ${connection.id}`); + }, this.heartbeatInterval); + } + + /** + * 发送消息到指定连接 + */ + sendToConnection(connectionId: string, data: any): boolean { + const connection = this.connections.get(connectionId); + if (connection) { + // 发送消息 + connection.wsReq.ws.send(JSON.stringify(data)); + return true; + } + return false; + } + + /** + * 发送消息到指定用户的所有连接 + */ + sendToUser(userId: string, data: any): number { + const userConnIds = this.userConnections.get(userId); + if (!userConnIds) return 0; + + let sentCount = 0; + for (const connId of userConnIds) { + if (this.sendToConnection(connId, data)) { + sentCount++; + } + } + return sentCount; + } + + /** + * 广播消息到所有连接 + */ + broadcast(data: any, opts?: { userId?: string; excludeConnectionId?: string }): number { + if (opts?.userId) { + // 发送给指定用户 + return this.sendToUser(opts.userId, data); + } + + let sentCount = 0; + for (const [connId, connection] of this.connections) { + // 跳过排除的连接 + if (opts?.excludeConnectionId && connId === opts.excludeConnectionId) { + continue; + } + + if (this.sendToConnection(connId, data)) { + sentCount++; + } + } + return sentCount; + } + + /** + * 获取连接信息 + */ + getConnection(connectionId: string): ConnectionInfo | undefined { + return this.connections.get(connectionId); + } + + /** + * 获取用户的所有连接 + */ + getUserConnections(userId: string): ConnectionInfo[] { + const userConnIds = this.userConnections.get(userId); + if (!userConnIds) return []; + + return Array.from(userConnIds) + .map((id) => this.connections.get(id)) + .filter((conn): conn is ConnectionInfo => conn !== undefined); + } + + /** + * 检查连接是否活跃(基于心跳) + */ + isConnectionAlive(connectionId: string, timeout: number = 60000): boolean { + const connection = this.connections.get(connectionId); + if (!connection) return false; + + const now = new Date(); + const timeSinceLastHeartbeat = now.getTime() - connection.lastHeartbeat.getTime(); + return timeSinceLastHeartbeat < timeout; + } + + /** + * 关闭指定连接 + */ + closeConnection(connectionId: string): boolean { + const connection = this.connections.get(connectionId); + if (connection) { + // 清理心跳定时器 + if (connection.heartbeatInterval) { + clearInterval(connection.heartbeatInterval); + } + + // 从用户索引中移除 + if (connection.userId) { + const userSet = this.userConnections.get(connection.userId); + if (userSet) { + userSet.delete(connectionId); + if (userSet.size === 0) { + this.userConnections.delete(connection.userId); + } + } + } + try { + connection.wsReq.ws.close(); + } catch (error) { + console.error(`Error closing WebSocket for connection ${connectionId}:`, error); + } + // 从管理器中移除 + this.connections.delete(connectionId); + + console.log(`WebSocket connection ${connectionId} closed`); + return true; + } + return false; + } + + /** + * 关闭所有连接 + */ + closeAllConnections(): void { + for (const [connectionId] of this.connections) { + this.closeConnection(connectionId); + } + } + + /** + * 获取活跃连接列表 + */ + getActiveConnections(): string[] { + return Array.from(this.connections.keys()); + } + + /** + * 获取连接数量 + */ + getConnectionCount(): number { + return this.connections.size; + } + + /** + * 获取用户连接数量 + */ + getUserConnectionCount(userId: string): number { + return this.userConnections.get(userId)?.size || 0; + } +} diff --git a/assistant/src/module/local-proxy/proxy.ts b/assistant/src/module/local-proxy/proxy.ts index b7ba16e..b055058 100644 --- a/assistant/src/module/local-proxy/proxy.ts +++ b/assistant/src/module/local-proxy/proxy.ts @@ -32,7 +32,7 @@ export const proxyRoute = async (req: http.IncomingMessage, res: http.ServerResp return fileProxy(req, res, { path: localProxyProxy.path, rootPath: localProxy.pagesDir, - indexPath: localProxyProxy.indexPath, + indexPath: localProxyProxy.file?.indexPath, }); } res.statusCode = 404; diff --git a/assistant/src/routes/hot-api/key-sender/index.ts b/assistant/src/routes/hot-api/key-sender/index.ts deleted file mode 100644 index 90ac701..0000000 --- a/assistant/src/routes/hot-api/key-sender/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { app } from '@/app.ts'; -// import { Hotkeys } from '@kevisual/hot-api'; -import { Hotkeys } from './lib.ts'; -import { useContextKey } from '@kevisual/context'; -app.route({ - path: 'key-sender', - // middleware: ['admin-auth'] -}).define(async (ctx) => { - let keys = ctx.query.keys; - if (keys.includes(' ')) { - keys = keys.replace(/\s+/g, '+'); - } - const hotKeys: Hotkeys = useContextKey('hotkeys', () => new Hotkeys()); - if (typeof keys === 'string') { - await hotKeys.pressHotkey({ - hotkey: keys, - }); - } - ctx.body = 'ok'; -}).addTo(app); diff --git a/assistant/src/routes/hot-api/key-sender/lib.ts b/assistant/src/routes/hot-api/key-sender/lib.ts deleted file mode 100644 index 7f0285b..0000000 --- a/assistant/src/routes/hot-api/key-sender/lib.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { keyboard, Key } from "@nut-tree-fork/nut-js"; - -/** - * 控制功能部分的案件映射 - */ -export const keyMap: Record = { - 'ctrl': Key.LeftControl, - 'leftctrl': Key.LeftControl, - 'rightctrl': Key.RightControl, - 'alt': Key.LeftAlt, - 'leftalt': Key.LeftAlt, - 'rightalt': Key.RightAlt, - 'shift': Key.LeftShift, - 'leftshift': Key.LeftShift, - 'rightshift': Key.RightShift, - 'meta': Key.LeftSuper, - 'cmd': Key.LeftCmd, - 'win': Key.LeftWin, - // 根据操作系统选择 Ctrl 或 Command 键 - 'ctrlorcommand': process.platform === 'darwin' ? Key.LeftCmd : Key.LeftControl, -}; - -/** - * 将快捷键字符串转换为 Key 枚举值 - * @param hotkey - * @returns - */ -export const parseHotkey = (hotkey: string): Key[] => { - return hotkey - .toLowerCase() - .split('+') - .map(key => { - const trimmed = key.trim().toLowerCase(); - // 如果是修饰键,从映射表中获取 - if (keyMap[trimmed]) { - return keyMap[trimmed]; - } - // 如果是字母,转换为大写并查找对应的 Key - if (trimmed.length === 1 && /[a-z]/.test(trimmed)) { - const upperKey = trimmed.toUpperCase(); - return Key[upperKey as keyof typeof Key] as Key; - } - // 其他情况直接查找 - return Key[trimmed as keyof typeof Key] as Key; - }) - .filter((key): key is Key => key !== undefined); -} - -type PressHostKeysOptions = { - hotkey: string; - durationMs?: number; -} -export const pressHotkey = async (opts: PressHostKeysOptions): Promise => { - const { hotkey, durationMs = 100 } = opts; - const keys = parseHotkey(hotkey); - - console.log('准备模拟按下快捷键:', hotkey); - // 同时按下所有键 - await keyboard.pressKey(...keys); - // 短暂延迟后释放 - await new Promise(resolve => setTimeout(resolve, durationMs)); - // 释放所有键 - await keyboard.releaseKey(...keys); - return true -} - -/** - * 模拟按下一组快捷键,支持逗号分隔的多个快捷键 - * @param opts - * @returns - */ -export const pressHotkeys = async (opts: PressHostKeysOptions): Promise => { - let { hotkey } = opts; - hotkey = hotkey.replace(/\s+/g, ''); // 去除所有空格 - const hotkeyList = hotkey.split(',').map(hk => hk.trim()); - if (hotkeyList.length === 0) { - return await pressHotkey({ ...opts, hotkey }); - } - for (const hk of hotkeyList) { - await pressHotkey({ ...opts, hotkey: hk }); - // 每个快捷键之间稍作延迟 - await new Promise(resolve => setTimeout(resolve, 200)); - } - return true; -} -export class Hotkeys { - pressHotkey = pressHotkey; - pressHotkeys = pressHotkeys; -} \ No newline at end of file diff --git a/assistant/src/routes/index.ts b/assistant/src/routes/index.ts index 2b8ac86..3ea3584 100644 --- a/assistant/src/routes/index.ts +++ b/assistant/src/routes/index.ts @@ -5,8 +5,6 @@ import './ai/index.ts'; import './user/index.ts'; import './call/index.ts' -// TODO: 移除 -// import './hot-api/key-sender/index.ts'; import './opencode/index.ts'; import './remote/index.ts'; import './kevisual/index.ts' diff --git a/assistant/src/server.ts b/assistant/src/server.ts index d375032..060705d 100644 --- a/assistant/src/server.ts +++ b/assistant/src/server.ts @@ -1,6 +1,6 @@ import { useContextKey } from '@kevisual/context'; import { app, assistantConfig, runtime } from './app.ts'; -import { proxyRoute, proxyWs } from './services/proxy/proxy-page-index.ts'; +import { proxyLivecodeWs, proxyRoute, proxyWs } from './services/proxy/proxy-page-index.ts'; import './routes/index.ts'; import './routes-simple/index.ts'; @@ -49,6 +49,7 @@ export const runServer = async (port: number = 51515, listenPath = '127.0.0.1') func: proxyRoute as any, }, ...proxyWs(), + ...proxyLivecodeWs(), qwenAsr, ]); const manager = useContextKey('manager', new AssistantApp(assistantConfig, app)); diff --git a/assistant/src/services/proxy/proxy-page-index.ts b/assistant/src/services/proxy/proxy-page-index.ts index e59cd04..e4b6027 100644 --- a/assistant/src/services/proxy/proxy-page-index.ts +++ b/assistant/src/services/proxy/proxy-page-index.ts @@ -1,13 +1,14 @@ import { createApiProxy, ProxyInfo, proxy } from '@/module/assistant/index.ts'; import http from 'node:http'; import { LocalProxy } from './local-proxy.ts'; -import { assistantConfig, simpleRouter } from '@/app.ts'; +import { assistantConfig, simpleRouter, app } from '@/app.ts'; import { log, logger } from '@/module/logger.ts'; import { getToken } from '@/module/http-token.ts'; import { getTokenUserCache } from '@/routes/index.ts'; import type { WebSocketListenerFun } from "@kevisual/router"; import WebSocket from 'ws'; import { renderNoAuthAndLogin } from '@/module/assistant/html/login.ts'; +import { LiveCode } from '@/module/livecode/index.ts'; const localProxy = new LocalProxy({}); localProxy.initFromAssistantConfig(assistantConfig); @@ -234,6 +235,27 @@ export const proxyWs = () => { } return proxyApi.map(createProxyInfo); }; +const liveCode = new LiveCode(app) +export const proxyLivecodeWs = () => { + const livecode = assistantConfig.getCacheAssistantConfig()?.router?.livecode ?? true; + if (!livecode) { + return []; + } + const fun: WebSocketListenerFun = async (req, res) => { + const { ws, emitter, id, data } = req; + // if (!id) { + // ws.send(JSON.stringify({ type: 'error', message: 'not found id' })); + // ws.close(); + // return; + // } + liveCode.conn(req) + } + return [{ + path: '/livecode/ws', + io: true, + func: fun + }] +} export const createProxyInfo = (proxyApiItem: ProxyInfo) => { const func: WebSocketListenerFun = async (req, res) => { const { ws, emitter, id, data } = req; diff --git a/assistant/src/test/live-app-origin.ts b/assistant/src/test/live-app-origin.ts new file mode 100644 index 0000000..8124616 --- /dev/null +++ b/assistant/src/test/live-app-origin.ts @@ -0,0 +1,215 @@ +import { App } from '@kevisual/router' +import { WebSocket } from 'ws' +import net from 'net'; + +type ReconnectConfig = { + maxRetries?: number; // 最大重试次数,默认无限 + retryDelay?: number; // 重试延迟(ms),默认1000 + maxDelay?: number; // 最大延迟(ms),默认30000 + backoffMultiplier?: number; // 退避倍数,默认2 +}; + +class ReconnectingWebSocket { + private ws: WebSocket | null = null; + private url: string; + private config: Required; + private retryCount: number = 0; + private reconnectTimer: NodeJS.Timeout | null = null; + private isManualClose: boolean = false; + private messageHandlers: Array<(data: any) => void> = []; + private openHandlers: Array<() => void> = []; + private closeHandlers: Array<(code: number, reason: Buffer) => void> = []; + private errorHandlers: Array<(error: Error) => void> = []; + + constructor(url: string, config: ReconnectConfig = {}) { + this.url = url; + this.config = { + maxRetries: config.maxRetries ?? Infinity, + retryDelay: config.retryDelay ?? 1000, + maxDelay: config.maxDelay ?? 30000, + backoffMultiplier: config.backoffMultiplier ?? 2, + }; + } + + connect(): void { + if (this.ws?.readyState === WebSocket.OPEN) { + return; + } + + console.log(`正在连接到 ${this.url}...`); + this.ws = new WebSocket(this.url); + + this.ws.on('open', () => { + console.log('WebSocket 连接已打开'); + this.retryCount = 0; + this.openHandlers.forEach(handler => handler()); + this.send({ type: 'heartbeat', timestamp: new Date().toISOString() }); + }); + + this.ws.on('message', (data: any) => { + this.messageHandlers.forEach(handler => { + try { + const message = JSON.parse(data.toString()); + handler(message); + } catch { + handler(data.toString()); + } + }); + }); + + this.ws.on('close', (code: number, reason: Buffer) => { + console.log(`WebSocket 连接已关闭: code=${code}, reason=${reason.toString()}`); + this.closeHandlers.forEach(handler => handler(code, reason)); + + if (!this.isManualClose) { + this.scheduleReconnect(); + } + }); + + this.ws.on('error', (error: Error) => { + console.error('WebSocket 错误:', error.message); + this.errorHandlers.forEach(handler => handler(error)); + }); + } + + private scheduleReconnect(): void { + if (this.reconnectTimer) { + return; + } + + if (this.retryCount >= this.config.maxRetries) { + console.error(`已达到最大重试次数 (${this.config.maxRetries}),停止重连`); + return; + } + + // 计算延迟(指数退避) + const delay = Math.min( + this.config.retryDelay * Math.pow(this.config.backoffMultiplier, this.retryCount), + this.config.maxDelay + ); + + this.retryCount++; + console.log(`将在 ${delay}ms 后进行第 ${this.retryCount} 次重连尝试...`); + + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null; + this.connect(); + }, delay); + } + + send(data: any): boolean { + if (this.ws?.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify(data)); + return true; + } + console.warn('WebSocket 未连接,无法发送消息'); + return false; + } + + onMessage(handler: (data: any) => void): void { + this.messageHandlers.push(handler); + } + + onOpen(handler: () => void): void { + this.openHandlers.push(handler); + } + + onClose(handler: (code: number, reason: Buffer) => void): void { + this.closeHandlers.push(handler); + } + + onError(handler: (error: Error) => void): void { + this.errorHandlers.push(handler); + } + + close(): void { + this.isManualClose = true; + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + if (this.ws) { + this.ws.close(); + this.ws = null; + } + } + + getReadyState(): number { + return this.ws?.readyState ?? WebSocket.CLOSED; + } + + getRetryCount(): number { + return this.retryCount; + } +} + +const app = new App(); + +app.route({ + path: 'livecode-status', + description: 'LiveCode 状态路由', + metadata: { + tags: ['livecode', 'status'], + }, +}).define(async (ctx) => { + ctx.body = { + status: 'LiveCode 模块运行正常', + timestamp: new Date().toISOString(), + }; +}).addTo(app) + +app.createRouteList(); +await new Promise((resolve) => setTimeout(resolve, 1000)); + +// 创建支持断开重连的 WebSocket 客户端 +const ws = new ReconnectingWebSocket('ws://localhost:51516/livecode/ws?id=test-live-app', { + maxRetries: Infinity, // 无限重试 + retryDelay: 1000, // 初始重试延迟 1 秒 + maxDelay: 30000, // 最大延迟 30 秒 + backoffMultiplier: 2, // 指数退避倍数 +}); + +ws.onMessage(async (message) => { + console.log('收到消息:', message); + if (message.type === 'router' && message.id) { + console.log('收到路由响应:', message); + const data = message?.data; + if (!data) { + ws.send({ + type: 'router', + id: message.id, + data: { code: 500, message: 'No data received' } + }); + return; + } + const res = await app.run(message.data); + console.log('路由处理结果:', res); + ws.send({ + type: 'router', + id: message.id, + data: res + }); + } +}); + +ws.onOpen(() => { + console.log('连接已建立,可以开始通信'); +}); + +ws.onError((error) => { + console.error('连接错误:', error.message); +}); + +ws.onClose((code, reason) => { + console.log(`连接关闭: ${code} - ${reason.toString()}`); +}); + +// 启动连接 +ws.connect(); + + +net.createServer((socket) => { + console.log('TCP 客户端已连接'); +}).listen(61616, () => { + console.log('TCP 服务器正在监听端口 61616'); +}); \ No newline at end of file diff --git a/assistant/src/test/live-app.ts b/assistant/src/test/live-app.ts new file mode 100644 index 0000000..b465549 --- /dev/null +++ b/assistant/src/test/live-app.ts @@ -0,0 +1,75 @@ + +import { App } from '@kevisual/router' +import { WebSocket } from 'ws' +import { ReconnectingWebSocket, handleCallApp } from '@kevisual/router/ws' +import net from 'net'; + +const app = new App(); + +app.route({ + path: 'livecode-status', + description: 'LiveCode 状态路由', + metadata: { + tags: ['livecode', 'status'], + }, +}).define(async (ctx) => { + ctx.body = { + status: 'LiveCode 模块运行正常', + timestamp: new Date().toISOString(), + }; +}).addTo(app) + +app.createRouteList(); +await new Promise((resolve) => setTimeout(resolve, 1000)); + +// 创建支持断开重连的 WebSocket 客户端 +const ws = new ReconnectingWebSocket('ws://localhost:51516/livecode/ws?id=test-live-app', { + maxRetries: Infinity, // 无限重试 + retryDelay: 1000, // 初始重试延迟 1 秒 + maxDelay: 30000, // 最大延迟 30 秒 + backoffMultiplier: 2, // 指数退避倍数 +}); +ws.onMessage(async (message) => { + console.log('收到消息:', message); + if (message.type === 'router' && message.id) { + console.log('收到路由响应:', message); + const data = message?.data; + if (!data) { + ws.send({ + type: 'router', + id: message.id, + data: { code: 500, message: 'No data received' } + }); + return; + } + const res = await app.run(message.data); + console.log('路由处理结果:', res); + ws.send({ + type: 'router', + id: message.id, + data: res + }); + } +}); + +ws.onOpen(() => { + console.log('连接已建立,可以开始通信'); +}); + +ws.onError((error) => { + console.error('连接错误:', error.message); +}); + +ws.onClose((code, reason) => { + console.log(`连接关闭: ${code} - ${reason.toString()}`); +}); + +// 启动连接 +ws.connect(); + + +net.createServer((socket) => { + console.log('TCP 客户端已连接'); +}).listen(61616, () => { + console.log('TCP 服务器正在监听端口 61616'); +}); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c12dda3..166a588 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -129,21 +129,18 @@ importers: '@aws-sdk/client-s3': specifier: ^3.980.0 version: 3.980.0 - '@kevisual/ha-api': - specifier: ^0.0.8 - version: 0.0.8 '@kevisual/js-filter': specifier: ^0.0.5 version: 0.0.5 '@kevisual/oss': - specifier: ^0.0.18 - version: 0.0.18 + specifier: ^0.0.19 + version: 0.0.19 '@kevisual/video-tools': specifier: ^0.0.13 version: 0.0.13(dotenv@17.2.3)(supports-color@10.2.2) '@opencode-ai/sdk': - specifier: ^1.1.47 - version: 1.1.47 + specifier: ^1.1.48 + version: 1.1.48 es-toolkit: specifier: ^1.44.0 version: 1.44.0 @@ -168,16 +165,13 @@ importers: devDependencies: '@inquirer/prompts': specifier: ^8.2.0 - version: 8.2.0(@types/node@25.1.0) + version: 8.2.0(@types/node@25.2.0) '@kevisual/ai': specifier: ^0.0.24 version: 0.0.24 '@kevisual/api': - specifier: ^0.0.35 - version: 0.0.35 - '@kevisual/cnb': - specifier: ^0.0.13 - version: 0.0.13(dotenv@17.2.3)(idb-keyval@6.2.2)(typescript@5.8.2) + specifier: ^0.0.42 + version: 0.0.42 '@kevisual/load': specifier: ^0.0.6 version: 0.0.6 @@ -188,29 +182,29 @@ importers: specifier: ^0.0.4 version: 0.0.4 '@kevisual/query': - specifier: 0.0.38 - version: 0.0.38 + specifier: 0.0.39 + version: 0.0.39 '@kevisual/query-login': specifier: 0.0.7 - version: 0.0.7(@kevisual/query@0.0.38) + version: 0.0.7(@kevisual/query@0.0.39) '@kevisual/router': - specifier: ^0.0.64 - version: 0.0.64(typescript@5.8.2) + specifier: ^0.0.67 + version: 0.0.67 '@kevisual/types': specifier: ^0.0.12 version: 0.0.12 '@kevisual/use-config': - specifier: ^1.0.28 - version: 1.0.28(dotenv@17.2.3) + specifier: ^1.0.30 + version: 1.0.30(dotenv@17.2.3) '@opencode-ai/plugin': - specifier: ^1.1.47 - version: 1.1.47 + specifier: ^1.1.48 + version: 1.1.48 '@types/bun': specifier: ^1.3.8 version: 1.3.8 '@types/node': - specifier: ^25.1.0 - version: 25.1.0 + specifier: ^25.2.0 + version: 25.2.0 '@types/send': specifier: ^1.2.1 version: 1.2.1 @@ -1306,9 +1300,6 @@ packages: '@kevisual/api@0.0.28': resolution: {integrity: sha512-WQluRlu2qGM1qktIhPLODie8x382a6jEMfFOcay/rnkCgXK0BRpnqOKwlX7IMLdMqka7GY/BD69kSMnK1Exf5g==} - '@kevisual/api@0.0.35': - resolution: {integrity: sha512-NbaOasecbG+O9Ju2/LWC2eWeqcPc5yZYXXyT4vHpU2W5SoPzBf7H3W7+i3py/JcEXF6adcHZVofftCYpecmGMQ==} - '@kevisual/api@0.0.42': resolution: {integrity: sha512-Bn5G+ZzGEPoJdvd5U3xWHGY0oidQj23gt1YAWvTqjm0frDJfJ4Q2WT9Xjb1ZdJ/YBcfaNe9yEoMCpFNdUls/mw==} @@ -1324,12 +1315,6 @@ packages: '@kevisual/cache@0.0.3': resolution: {integrity: sha512-BWEck69KYL96/ywjYVkML974RHjDJTj2ITQND1zFPR+hlBV1H1p55QZgSYRJCObg3EAV1S9Zic/fR2T4pfe8yg==} - '@kevisual/cache@0.0.5': - resolution: {integrity: sha512-fgtUYGUUq/DY0KFV4CkWszNqvQUaA8XvMTUjoR9ZXRpau5IIDolD/Wen2TFsZ7G3Rfy+lef5dnaiZVDkZwdVKg==} - - '@kevisual/cnb@0.0.13': - resolution: {integrity: sha512-n98lwnlVHz8YqceR/fcorYUaBzcvwwqehyOAGVrqCVwVLsltYmYuHUhzVy1bK3NJ6zwhVdoDrkq7+bv3ZqDT3g==} - '@kevisual/context@0.0.4': resolution: {integrity: sha512-HJeLeZQLU+7tCluSfOyvkgKLs0HjCZrdJlZgEgKRSa8XTwZfMAUt6J7qZTbrZAHBlPtX68EPu/PI8JMCeu3WAQ==} @@ -1337,9 +1322,6 @@ packages: resolution: {integrity: sha512-4T/m2LqhtwWEW+lWmg7jLxKFW7VtIAftsWFDDZvh10bZunqFf8iXxChHcVSQWikghJb4cq1IkWzPkvc2l+Asdw==} hasBin: true - '@kevisual/ha-api@0.0.8': - resolution: {integrity: sha512-RVBrHOXx471lwVmoP5lnAw4XAwyBN3BsJvwaJKLTpAaefvZ2slZEuJZY7lAX7OVEAZJLrfjH+QeErLQ+EvpdVA==} - '@kevisual/js-filter@0.0.5': resolution: {integrity: sha512-+S+Sf3K/aP6XtZI2s7TgKOr35UuvUvtpJ9YDW30a+mY0/N8gRuzyKhieBzQN7Ykayzz70uoMavBXut2rUlLgzw==} @@ -1355,8 +1337,8 @@ packages: '@kevisual/logger@0.0.4': resolution: {integrity: sha512-+fpr92eokSxoGOW1SIRl/27lPuO+zyY+feR5o2Q4YCNlAdt2x64NwC/w8r/3NEC5QenLgd4K0azyKTI2mHbARw==} - '@kevisual/oss@0.0.18': - resolution: {integrity: sha512-vTdXe41inq4oc+bfYIR3xMDm8GZyOAaWq3DBh+Eur9uNOJcIUdgZBVPOm2uSigmjl3PvqekUw8bE/vbWWJAY7w==} + '@kevisual/oss@0.0.19': + resolution: {integrity: sha512-4Y5krJTqLQOsEwJf7K7a/88t9YHm8PQNuZ5SJDTMopYDOflJlwVjvqiu0lapQ0UrpI+wG6FdfmdmnWpXdQsa1Q==} '@kevisual/permission@0.0.3': resolution: {integrity: sha512-8JsA/5O5Ax/z+M+MYpFYdlioHE6jNmWMuFSokBWYs9CCAHNiSKMR01YLkoVDoPvncfH/Y8F5K/IEXRCbptuMNA==} @@ -1390,17 +1372,12 @@ packages: '@kevisual/router@0.0.51': resolution: {integrity: sha512-i9qYBeS/um78oC912oWJD3iElB+5NTKyTrz1Hzf4DckiUFnjLL81UPwjIh5I2l9+ul0IZ/Pxx+sFSF99fJkzKg==} - '@kevisual/router@0.0.64': - resolution: {integrity: sha512-EYz1MZxrltgySUL0Y+/MtZf2FEmqC5U8GmFAqvHNjgtS5FJdHpxRjo6zab4+0wSUlVyCxCpZXFY5vHB/g+nQBw==} + '@kevisual/router@0.0.67': + resolution: {integrity: sha512-SKQDc9RUSUqpcVA4Y05rl525zmHcyl4JlHdFyBhatNRMBQdKCVd8rBAojnyz4gNmUU9bY+gxM87f30dHsQkRAw==} '@kevisual/types@0.0.12': resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==} - '@kevisual/use-config@1.0.28': - resolution: {integrity: sha512-ngF+LDbjxpXWrZNmnShIKF/jPpAa+ezV+DcgoZIIzHlRnIjE+rr9sLkN/B7WJbiH9C/j1tQXOILY8ujBqILrow==} - peerDependencies: - dotenv: ^17 - '@kevisual/use-config@1.0.30': resolution: {integrity: sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw==} peerDependencies: @@ -1416,10 +1393,6 @@ packages: resolution: {integrity: sha512-jlFxSlXUEz93cFW+UYT5BXv/rFVgiMQnIfqRYZ0gj1hSP8PMGRqMqUoHSLfKvfRRS4jseLSvTTeEKSQpZJtURg==} engines: {node: '>=10.0.0'} - '@kevisual/ws@8.19.0': - resolution: {integrity: sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw==} - engines: {node: '>=10.0.0'} - '@lezer/common@1.4.0': resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==} @@ -1466,11 +1439,8 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@opencode-ai/plugin@1.1.47': - resolution: {integrity: sha512-gNMPz72altieDfLhUw3VAT1xbduKi3w3wZ57GLeS7qU9W474HdvdIiLBnt2Xq3U7Ko0/0tvK3nzCker6IIDqmQ==} - - '@opencode-ai/sdk@1.1.47': - resolution: {integrity: sha512-s3PBHwk1sP6Zt/lJxIWSBWZ1TnrI1nFxSP97LCODUytouAQgbygZ1oDH7O2sGMBEuGdA8B1nNSPla0aRSN3IpA==} + '@opencode-ai/plugin@1.1.48': + resolution: {integrity: sha512-KkaSMevXmz7tOwYDMJeWiXE5N8LmRP18qWI5Xhv3+c+FdGPL+l1hQrjSgyv3k7Co7qpCyW3kAUESBB7BzIOl2w==} '@opencode-ai/sdk@1.1.48': resolution: {integrity: sha512-j5/79X45fUPWVD2Ffm/qvwLclDCdPeV+TYMDrm9to0p4pmzhmeKevCsyiRdLg0o0HE3AFRUnOo2rdO9NetN79A==} @@ -2402,9 +2372,6 @@ packages: '@types/node@17.0.45': resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} - '@types/node@25.1.0': - resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==} - '@types/node@25.2.0': resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} @@ -3427,10 +3394,6 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} - hono@4.11.7: - resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} - engines: {node: '>=16.9.0'} - hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} @@ -6314,15 +6277,6 @@ snapshots: '@inquirer/ansi@2.0.3': {} - '@inquirer/checkbox@5.0.4(@types/node@25.1.0)': - dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.1(@types/node@25.1.0) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@25.1.0) - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/checkbox@5.0.4(@types/node@25.2.0)': dependencies: '@inquirer/ansi': 2.0.3 @@ -6332,13 +6286,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/confirm@6.0.4(@types/node@25.1.0)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@25.1.0) - '@inquirer/type': 4.0.3(@types/node@25.1.0) - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/confirm@6.0.4(@types/node@25.2.0)': dependencies: '@inquirer/core': 11.1.1(@types/node@25.2.0) @@ -6346,18 +6293,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/core@11.1.1(@types/node@25.1.0)': - dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@25.1.0) - cli-width: 4.1.0 - mute-stream: 3.0.0 - signal-exit: 4.1.0 - wrap-ansi: 9.0.2 - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/core@11.1.1(@types/node@25.2.0)': dependencies: '@inquirer/ansi': 2.0.3 @@ -6370,14 +6305,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/editor@5.0.4(@types/node@25.1.0)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@25.1.0) - '@inquirer/external-editor': 2.0.3(@types/node@25.1.0) - '@inquirer/type': 4.0.3(@types/node@25.1.0) - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/editor@5.0.4(@types/node@25.2.0)': dependencies: '@inquirer/core': 11.1.1(@types/node@25.2.0) @@ -6386,13 +6313,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/expand@5.0.4(@types/node@25.1.0)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@25.1.0) - '@inquirer/type': 4.0.3(@types/node@25.1.0) - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/expand@5.0.4(@types/node@25.2.0)': dependencies: '@inquirer/core': 11.1.1(@types/node@25.2.0) @@ -6400,13 +6320,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/external-editor@2.0.3(@types/node@25.1.0)': - dependencies: - chardet: 2.1.1 - iconv-lite: 0.7.2 - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/external-editor@2.0.3(@types/node@25.2.0)': dependencies: chardet: 2.1.1 @@ -6416,13 +6329,6 @@ snapshots: '@inquirer/figures@2.0.3': {} - '@inquirer/input@5.0.4(@types/node@25.1.0)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@25.1.0) - '@inquirer/type': 4.0.3(@types/node@25.1.0) - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/input@5.0.4(@types/node@25.2.0)': dependencies: '@inquirer/core': 11.1.1(@types/node@25.2.0) @@ -6430,13 +6336,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/number@4.0.4(@types/node@25.1.0)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@25.1.0) - '@inquirer/type': 4.0.3(@types/node@25.1.0) - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/number@4.0.4(@types/node@25.2.0)': dependencies: '@inquirer/core': 11.1.1(@types/node@25.2.0) @@ -6444,14 +6343,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/password@5.0.4(@types/node@25.1.0)': - dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.1(@types/node@25.1.0) - '@inquirer/type': 4.0.3(@types/node@25.1.0) - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/password@5.0.4(@types/node@25.2.0)': dependencies: '@inquirer/ansi': 2.0.3 @@ -6460,21 +6351,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/prompts@8.2.0(@types/node@25.1.0)': - dependencies: - '@inquirer/checkbox': 5.0.4(@types/node@25.1.0) - '@inquirer/confirm': 6.0.4(@types/node@25.1.0) - '@inquirer/editor': 5.0.4(@types/node@25.1.0) - '@inquirer/expand': 5.0.4(@types/node@25.1.0) - '@inquirer/input': 5.0.4(@types/node@25.1.0) - '@inquirer/number': 4.0.4(@types/node@25.1.0) - '@inquirer/password': 5.0.4(@types/node@25.1.0) - '@inquirer/rawlist': 5.2.0(@types/node@25.1.0) - '@inquirer/search': 4.1.0(@types/node@25.1.0) - '@inquirer/select': 5.0.4(@types/node@25.1.0) - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/prompts@8.2.0(@types/node@25.2.0)': dependencies: '@inquirer/checkbox': 5.0.4(@types/node@25.2.0) @@ -6490,13 +6366,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/rawlist@5.2.0(@types/node@25.1.0)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@25.1.0) - '@inquirer/type': 4.0.3(@types/node@25.1.0) - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/rawlist@5.2.0(@types/node@25.2.0)': dependencies: '@inquirer/core': 11.1.1(@types/node@25.2.0) @@ -6504,14 +6373,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/search@4.1.0(@types/node@25.1.0)': - dependencies: - '@inquirer/core': 11.1.1(@types/node@25.1.0) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@25.1.0) - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/search@4.1.0(@types/node@25.2.0)': dependencies: '@inquirer/core': 11.1.1(@types/node@25.2.0) @@ -6520,15 +6381,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/select@5.0.4(@types/node@25.1.0)': - dependencies: - '@inquirer/ansi': 2.0.3 - '@inquirer/core': 11.1.1(@types/node@25.1.0) - '@inquirer/figures': 2.0.3 - '@inquirer/type': 4.0.3(@types/node@25.1.0) - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/select@5.0.4(@types/node@25.2.0)': dependencies: '@inquirer/ansi': 2.0.3 @@ -6538,10 +6390,6 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 - '@inquirer/type@4.0.3(@types/node@25.1.0)': - optionalDependencies: - '@types/node': 25.1.0 - '@inquirer/type@4.0.3(@types/node@25.2.0)': optionalDependencies: '@types/node': 25.2.0 @@ -6592,16 +6440,6 @@ snapshots: fuse.js: 7.1.0 nanoid: 5.1.6 - '@kevisual/api@0.0.35': - dependencies: - '@kevisual/js-filter': 0.0.5 - '@kevisual/load': 0.0.6 - es-toolkit: 1.44.0 - eventemitter3: 5.0.4 - fuse.js: 7.1.0 - nanoid: 5.1.6 - path-browserify-esm: 1.0.6 - '@kevisual/api@0.0.42': dependencies: '@kevisual/js-filter': 0.0.5 @@ -6645,45 +6483,6 @@ snapshots: dependencies: idb-keyval: 6.2.1 - '@kevisual/cache@0.0.5': - dependencies: - idb-keyval: 6.2.2 - lru-cache: 11.2.5 - nanoid: 5.1.6 - - '@kevisual/cnb@0.0.13(dotenv@17.2.3)(idb-keyval@6.2.2)(typescript@5.8.2)': - dependencies: - '@kevisual/query': 0.0.38 - '@kevisual/router': 0.0.64(typescript@5.8.2) - '@kevisual/use-config': 1.0.30(dotenv@17.2.3) - es-toolkit: 1.44.0 - nanoid: 5.1.6 - unstorage: 1.17.4(idb-keyval@6.2.2) - ws: '@kevisual/ws@8.19.0' - zod: 4.3.6 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - db0 - - dotenv - - idb-keyval - - ioredis - - typescript - - uploadthing - '@kevisual/context@0.0.4': {} '@kevisual/dts@0.0.3(typescript@5.8.2)': @@ -6697,12 +6496,6 @@ snapshots: transitivePeerDependencies: - typescript - '@kevisual/ha-api@0.0.8': - dependencies: - '@kevisual/cache': 0.0.5 - fuse.js: 7.1.0 - lru-cache: 11.2.5 - '@kevisual/js-filter@0.0.5': {} '@kevisual/kv-code@0.0.4(@types/react@19.2.10)(dotenv@17.2.3)': @@ -6748,7 +6541,7 @@ snapshots: '@kevisual/logger@0.0.4': {} - '@kevisual/oss@0.0.18': {} + '@kevisual/oss@0.0.19': {} '@kevisual/permission@0.0.3': {} @@ -6818,20 +6611,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@kevisual/router@0.0.64(typescript@5.8.2)': - dependencies: - '@kevisual/dts': 0.0.3(typescript@5.8.2) - hono: 4.11.7 - transitivePeerDependencies: - - typescript + '@kevisual/router@0.0.67': {} '@kevisual/types@0.0.12': {} - '@kevisual/use-config@1.0.28(dotenv@17.2.3)': - dependencies: - '@kevisual/load': 0.0.6 - dotenv: 17.2.3 - '@kevisual/use-config@1.0.30(dotenv@17.2.3)': dependencies: '@kevisual/load': 0.0.6 @@ -6856,8 +6639,6 @@ snapshots: '@kevisual/ws@8.0.0': {} - '@kevisual/ws@8.19.0': {} - '@lezer/common@1.4.0': {} '@lezer/css@1.3.0': @@ -6943,13 +6724,11 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - '@opencode-ai/plugin@1.1.47': + '@opencode-ai/plugin@1.1.48': dependencies: - '@opencode-ai/sdk': 1.1.47 + '@opencode-ai/sdk': 1.1.48 zod: 4.1.8 - '@opencode-ai/sdk@1.1.47': {} - '@opencode-ai/sdk@1.1.48': {} '@oslojs/encoding@1.1.0': {} @@ -8087,10 +7866,6 @@ snapshots: '@types/node@17.0.45': {} - '@types/node@25.1.0': - dependencies: - undici-types: 7.16.0 - '@types/node@25.2.0': dependencies: undici-types: 7.16.0 @@ -9390,8 +9165,6 @@ snapshots: highlight.js@11.11.1: {} - hono@4.11.7: {} - hookable@5.5.3: {} html-escaper@3.0.3: {} @@ -9456,7 +9229,8 @@ snapshots: idb-keyval@6.2.1: {} - idb-keyval@6.2.2: {} + idb-keyval@6.2.2: + optional: true ignore@7.0.5: {}