diff --git a/agent/routes/call/index.ts b/agent/routes/call/index.ts index e1a446d..c8d28d9 100644 --- a/agent/routes/call/index.ts +++ b/agent/routes/call/index.ts @@ -1,33 +1,31 @@ import { createSkill, tool } from '@kevisual/router' import { app } from '../../app.ts' -if (!app.hasRoute('call')) { - // "调用 path: cnb key: list-repos" - app.route({ - path: 'call', - key: '', - description: '调用', - middleware: ['admin-auth'], - metadata: { - tags: ['opencode'], - ...createSkill({ - skill: 'call-app', - title: '调用app应用', - summary: '调用router的应用, 参数path, key, payload', - args: { - path: tool.schema.string().describe('应用路径,例如 cnb'), - key: tool.schema.string().optional().describe('应用key,例如 list-repos'), - payload: tool.schema.object({}).optional().describe('调用参数'), - } - }) - }, - }).define(async (ctx) => { - const { path, key } = ctx.query; - console.log('call app', ctx.query); - if (!path) { - ctx.throw('路径path不能为空'); - } - const res = await ctx.run({ path, key, payload: ctx.query.payload || {} }); - ctx.forward(res); - }).addTo(app) -} \ No newline at end of file +// "调用 path: cnb key: list-repos" +app.route({ + path: 'call', + key: '', + description: '调用', + middleware: ['admin-auth'], + metadata: { + tags: ['opencode'], + ...createSkill({ + skill: 'call-app', + title: '调用app应用', + summary: '调用router的应用, 参数path, key, payload', + args: { + path: tool.schema.string().describe('应用路径,例如 cnb'), + key: tool.schema.string().optional().describe('应用key,例如 list-repos'), + payload: tool.schema.object({}).optional().describe('调用参数'), + } + }) + }, +}).define(async (ctx) => { + const { path, key } = ctx.query; + console.log('call app', ctx.query); + if (!path) { + ctx.throw('路径path不能为空'); + } + const res = await ctx.run({ path, key, payload: ctx.query.payload || {} }); + ctx.forward(res); +}).addTo(app, { overwrite: false }) \ No newline at end of file diff --git a/agent/routes/cnb-board/cnb-dev-env.ts b/agent/routes/cnb-board/cnb-dev-env.ts new file mode 100644 index 0000000..1617706 --- /dev/null +++ b/agent/routes/cnb-board/cnb-dev-env.ts @@ -0,0 +1,424 @@ +import { app } from '../../app.ts'; +import { useKey } from '@kevisual/context' +import { getLiveMdContent } from './live/live-content.ts'; +import z from 'zod'; +const notCNBCheck = (ctx: any) => { + const isCNB = useKey('CNB'); + if (!isCNB) { + ctx.body = { + title: '非 cnb-board 环境', + list: [] + } + return true; + } + return false; +} +app.route({ + path: 'cnb_board', + key: 'live', + description: '获取cnb-board live的mdContent内容', + middleware: ['auth-admin'], + metadata: { + args: { + more: z.boolean().optional().describe('是否获取更多系统信息,默认false'), + } + } +}).define(async (ctx) => { + const more = ctx.query?.more ?? false + if (notCNBCheck(ctx)) return; + const list = getLiveMdContent({ more: more }); + ctx.body = { + title: '开发环境模式配置', + list, + }; +}).addTo(app); + +app.route({ + path: 'cnb_board', + key: 'live_repo_info', + description: '获取cnb-board live的repo信息', + middleware: ['auth-admin'] +}).define(async (ctx) => { + const repoSlug = useKey('CNB_REPO_SLUG') || ''; + const repoName = useKey('CNB_REPO_NAME') || ''; + const repoId = useKey('CNB_REPO_ID') || ''; + const repoUrlHttps = useKey('CNB_REPO_UR if (notCNBCheck(ctx)) return;L_HTTPS') || ''; + if (notCNBCheck(ctx)) return; + // 从 repoSlug 提取仓库名称 + const repoNameFromSlug = repoSlug.split('/').pop() || ''; + + const labels = [ + { + title: 'CNB_REPO_SLUG', + value: repoSlug, + description: '目标仓库路径,格式为 group_slug / repo_name,group_slug / sub_gourp_slug /.../repo_name' + }, + { + title: 'CNB_REPO_SLUG_LOWERCASE', + value: repoSlug.toLowerCase(), + description: '目标仓库路径小写格式' + }, + { + title: 'CNB_REPO_NAME', + value: repoName || repoNameFromSlug, + description: '目标仓库名称' + }, + { + title: 'CNB_REPO_NAME_LOWERCASE', + value: (repoName || repoNameFromSlug).toLowerCase(), + description: '目标仓库名称小写格式' + }, + { + title: 'CNB_REPO_ID', + value: repoId, + description: '目标仓库的 id' + }, + { + title: 'CNB_REPO_URL_HTTPS', + value: repoUrlHttps, + description: '目标仓库 https 地址' + } + ] + ctx.body = { + title: 'CNB_BOARD_LIVE_REPO_INFO', + list: labels + }; +}).addTo(app); + +// 构建类变量 +app.route({ + path: 'cnb_board', + key: 'live_build_info', + description: '获取cnb-board live的构建信息', + middleware: ['auth-admin'] +}).define(async (ctx) => { + if (notCNBCheck(ctx)) return; + const labels = [ + { + title: 'CNB_BUILD_ID', + value: useKey('CNB_BUILD_ID') || '', + description: '当前构建的流水号,全局唯一' + }, + { + title: 'CNB_BUILD_WEB_URL', + value: useKey('CNB_BUILD_WEB_URL') || '', + description: '当前构建的日志地址' + }, + { + title: 'CNB_BUILD_START_TIME', + value: useKey('CNB_BUILD_START_TIME') || '', + description: '当前构建的开始时间,UTC 格式,示例 2025-08-21T09:13:45.803Z' + }, + { + title: 'CNB_BUILD_USER', + value: useKey('CNB_BUILD_USER') || '', + description: '当前构建的触发者用户名' + }, + { + title: 'CNB_BUILD_USER_NICKNAME', + value: useKey('CNB_BUILD_USER_NICKNAME') || '', + description: '当前构建的触发者昵称' + }, + { + title: 'CNB_BUILD_USER_EMAIL', + value: useKey('CNB_BUILD_USER_EMAIL') || '', + description: '当前构建的触发者邮箱' + }, + { + title: 'CNB_BUILD_USER_ID', + value: useKey('CNB_BUILD_USER_ID') || '', + description: '当前构建的触发者 id' + }, + { + title: 'CNB_BUILD_USER_NPC_SLUG', + value: useKey('CNB_BUILD_USER_NPC_SLUG') || '', + description: '当前构建若为 NPC 触发,则为 NPC 所属仓库的路径' + }, + { + title: 'CNB_BUILD_USER_NPC_NAME', + value: useKey('CNB_BUILD_USER_NPC_NAME') || '', + description: '当前构建若为 NPC 触发,则为 NPC 角色名' + }, + { + title: 'CNB_BUILD_STAGE_NAME', + value: useKey('CNB_BUILD_STAGE_NAME') || '', + description: '当前构建的 stage 名称' + }, + { + title: 'CNB_BUILD_JOB_NAME', + value: useKey('CNB_BUILD_JOB_NAME') || '', + description: '当前构建的 job 名称' + }, + { + title: 'CNB_BUILD_JOB_KEY', + value: useKey('CNB_BUILD_JOB_KEY') || '', + description: '当前构建的 job key,同 stage 下唯一' + }, + { + title: 'CNB_BUILD_WORKSPACE', + value: useKey('CNB_BUILD_WORKSPACE') || '', + description: '自定义 shell 脚本执行的工作空间根目录' + }, + { + title: 'CNB_BUILD_FAILED_MSG', + value: useKey('CNB_BUILD_FAILED_MSG') || '', + description: '流水线构建失败的错误信息,可在 failStages 中使用' + }, + { + title: 'CNB_BUILD_FAILED_STAGE_NAME', + value: useKey('CNB_BUILD_FAILED_STAGE_NAME') || '', + description: '流水线构建失败的 stage 的名称,可在 failStages 中使用' + }, + { + title: 'CNB_PIPELINE_NAME', + value: useKey('CNB_PIPELINE_NAME') || '', + description: '当前 pipeline 的 name,没声明时为空' + }, + { + title: 'CNB_PIPELINE_KEY', + value: useKey('CNB_PIPELINE_KEY') || '', + description: '当前 pipeline 的索引 key,例如 pipeline-0' + }, + { + title: 'CNB_PIPELINE_ID', + value: useKey('CNB_PIPELINE_ID') || '', + description: '当前 pipeline 的 id,全局唯一字符串' + }, + { + title: 'CNB_PIPELINE_DOCKER_IMAGE', + value: useKey('CNB_PIPELINE_DOCKER_IMAGE') || '', + description: '当前 pipeline 所使用的 docker image,如:alpine:latest' + }, + { + title: 'CNB_PIPELINE_STATUS', + value: useKey('CNB_PIPELINE_STATUS') || '', + description: '当前流水线的构建状态,可在 endStages 中查看,其可能的值包括:success、error、cancel' + }, + { + title: 'CNB_PIPELINE_MAX_RUN_TIME', + value: useKey('CNB_PIPELINE_MAX_RUN_TIME') || '', + description: '流水线最大运行时间,单位为毫秒' + }, + { + title: 'CNB_RUNNER_IP', + value: useKey('CNB_RUNNER_IP') || '', + description: '当前 pipeline 所在 Runner 的 ip' + }, + { + title: 'CNB_CPUS', + value: useKey('CNB_CPUS') || '', + description: '当前构建流水线可以使用的最大 CPU 核数' + }, + { + title: 'CNB_MEMORY', + value: useKey('CNB_MEMORY') || '', + description: '当前构建流水线可以使用的最大内存大小,单位为 GiB' + }, + { + title: 'CNB_IS_RETRY', + value: useKey('CNB_IS_RETRY') || '', + description: '当前构建是否由 rebuild 触发' + }, + { + title: 'HUSKY_SKIP_INSTALL', + value: useKey('HUSKY_SKIP_INSTALL') || '', + description: '兼容 ci 环境下 husky' + } + ] + ctx.body = { + title: 'CNB_BOARD_LIVE_BUILD_INFO', + list: labels + }; +}).addTo(app); + +// PR/合并类变量 +app.route({ + path: 'cnb_board', + key: 'live_pull_info', + description: '获取cnb-board live的PR信息', + middleware: ['auth-admin'] +}).define(async (ctx) => { + const labels = [ + { + title: 'CNB_PULL_REQUEST', + value: useKey('CNB_PULL_REQUEST') || '', + description: '对于由 pull_request、pull_request.update、pull_request.target 触发的构建,值为 true,否则为 false' + }, + { + title: 'CNB_PULL_REQUEST_LIKE', + value: useKey('CNB_PULL_REQUEST_LIKE') || '', + description: '对于由 合并类事件 触发的构建,值为 true,否则为 false' + }, + { + title: 'CNB_PULL_REQUEST_PROPOSER', + value: useKey('CNB_PULL_REQUEST_PROPOSER') || '', + description: '对于由 合并类事件 触发的构建,值为提出 PR 者名称,否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_TITLE', + value: useKey('CNB_PULL_REQUEST_TITLE') || '', + description: '对于由 合并类事件 触发的构建,值为提 PR 时候填写的标题,否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_BRANCH', + value: useKey('CNB_PULL_REQUEST_BRANCH') || '', + description: '对于由 合并类事件 触发的构建,值为发起 PR 的源分支名称,否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_SHA', + value: useKey('CNB_PULL_REQUEST_SHA') || '', + description: '对于由 合并类事件 触发的构建,值为当前 PR 源分支最新的提交 sha,否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_TARGET_SHA', + value: useKey('CNB_PULL_REQUEST_TARGET_SHA') || '', + description: '对于由 合并类事件 触发的构建,值为当前 PR 目标分支最新的提交 sha,否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_MERGE_SHA', + value: useKey('CNB_PULL_REQUEST_MERGE_SHA') || '', + description: '对于由 pull_request.merged 触发的构建,值为合并后的 sha;对于 pull_request 等触发的构建,值为预合并后的 sha,否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_SLUG', + value: useKey('CNB_PULL_REQUEST_SLUG') || '', + description: '对于由 合并类事件 触发的构建,值为源仓库的仓库 slug,如 group_slug/repo_name,否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_ACTION', + value: useKey('CNB_PULL_REQUEST_ACTION') || '', + description: '对于由 合并类事件 触发的构建,可能的值有:created(新建PR)、code_update(源分支push)、status_update(评审通过或CI状态变更),否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_ID', + value: useKey('CNB_PULL_REQUEST_ID') || '', + description: '对于由 合并类事件 触发的构建,值为当前或者关联 PR 的全局唯一 id,否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_IID', + value: useKey('CNB_PULL_REQUEST_IID') || '', + description: '对于由 合并类事件 触发的构建,值为当前或者关联 PR 在仓库中的编号 iid,否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_REVIEWERS', + value: useKey('CNB_PULL_REQUEST_REVIEWERS') || '', + description: '对于由 合并类事件 触发的构建,值为评审人列表,多个以 , 分隔,否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_REVIEW_STATE', + value: useKey('CNB_PULL_REQUEST_REVIEW_STATE') || '', + description: '对于由 合并类事件 触发的构建,有评审者且有人通过评审为 approve,有评审者但无人通过评审为 unapprove,否则为空字符串' + }, + { + title: 'CNB_REVIEW_REVIEWED_BY', + value: useKey('CNB_REVIEW_REVIEWED_BY') || '', + description: '对于由 合并类事件 触发的构建,值为同意评审的评审人列表,多个以 , 分隔,否则为空字符串' + }, + { + title: 'CNB_REVIEW_LAST_REVIEWED_BY', + value: useKey('CNB_REVIEW_LAST_REVIEWED_BY') || '', + description: '对于由 合并类事件 触发的构建,值为最后一个同意评审的评审人,否则为空字符串' + }, + { + title: 'CNB_PULL_REQUEST_IS_WIP', + value: useKey('CNB_PULL_REQUEST_IS_WIP') || '', + description: '对于由 合并类事件 触发的构建,值为 true、false,表示 PR 是否被设置为 [WIP],否则为空字符串' + } + ] + ctx.body = { + title: 'CNB_BOARD_LIVE_PULL_INFO', + list: labels + }; +}).addTo(app); + +// NPC 类变量 +app.route({ + path: 'cnb_board', + key: 'live_npc_info', + description: '获取cnb-board live的NPC信息', + middleware: ['auth-admin'] +}).define(async (ctx) => { + if (notCNBCheck(ctx)) return; + const labels = [ + { + title: 'CNB_NPC_SLUG', + value: useKey('CNB_NPC_SLUG') || '', + description: '对于 @ 知识库角色触发的 NPC 事件,值为 NPC 所属仓库路径,否则为空字符串' + }, + { + title: 'CNB_NPC_NAME', + value: useKey('CNB_NPC_NAME') || '', + description: '对于 NPC 事件触发的构建,值为 NPC 角色名,否则为空字符串' + }, + { + title: 'CNB_NPC_SHA', + value: useKey('CNB_NPC_SHA') || '', + description: '对于 @ 知识库角色触发的 NPC 事件,值为 NPC 所属仓库默认分支最新提交的 sha,否则为空字符串' + }, + { + title: 'CNB_NPC_PROMPT', + value: useKey('CNB_NPC_PROMPT') || '', + description: '对于 @ 知识库角色触发的 NPC 事件,值为 NPC 角色 Prompt,否则为空字符串' + }, + { + title: 'CNB_NPC_AVATAR', + value: useKey('CNB_NPC_AVATAR') || '', + description: '对于 @ 知识库角色触发的 NPC 事件,值为 NPC 角色头像,否则为空字符串' + }, + { + title: 'CNB_NPC_ENABLE_THINKING', + value: useKey('CNB_NPC_ENABLE_THINKING') || '', + description: '对于 @npc 事件触发的构建,值为 NPC 角色是否开启思考,否则为空字符串' + } + ] + ctx.body = { + title: 'CNB_BOARD_LIVE_NPC_INFO', + list: labels + }; +}).addTo(app); + +// 评论类变量 +app.route({ + path: 'cnb_board', + key: 'live_comment_info', + description: '获取cnb-board live的评论信息', + middleware: ['auth-admin'] +}).define(async (ctx) => { + if (notCNBCheck(ctx)) return; + const labels = [ + { + title: 'CNB_COMMENT_ID', + value: useKey('CNB_COMMENT_ID') || '', + description: '对于评论事件触发的构建,值为评论全局唯一 ID,否则为空字符串' + }, + { + title: 'CNB_COMMENT_BODY', + value: useKey('CNB_COMMENT_BODY') || '', + description: '对于评论事件触发的构建,值为评论内容,否则为空字符串' + }, + { + title: 'CNB_COMMENT_TYPE', + value: useKey('CNB_COMMENT_TYPE') || '', + description: '对于 PR 代码评审评论,值为 diff_note;对于 PR 非代码评审评论以及 Issue 评论,值为 note;否则为空字符串' + }, + { + title: 'CNB_COMMENT_FILE_PATH', + value: useKey('CNB_COMMENT_FILE_PATH') || '', + description: '对于 PR 代码评审评论,值为评论所在文件,否则为空字符串' + }, + { + title: 'CNB_COMMENT_RANGE', + value: useKey('CNB_COMMENT_RANGE') || '', + description: '对于 PR 代码评审评论,值为评论所在代码行。如,单行为 L12,多行为 L13-L16,否则为空字符串' + }, + { + title: 'CNB_REVIEW_ID', + value: useKey('CNB_REVIEW_ID') || '', + description: '对于 PR 代码评审,值为评审 ID,否则为空字符串' + } + ] + ctx.body = { + title: 'CNB_BOARD_LIVE_COMMENT_INFO', + list: labels + }; +}).addTo(app); \ No newline at end of file diff --git a/agent/routes/cnb-board/common.ts b/agent/routes/cnb-board/common.ts new file mode 100644 index 0000000..e69de29 diff --git a/agent/routes/cnb-board/index.ts b/agent/routes/cnb-board/index.ts new file mode 100644 index 0000000..61826de --- /dev/null +++ b/agent/routes/cnb-board/index.ts @@ -0,0 +1,38 @@ +import { app } from '../../app.ts'; +import './cnb-dev-env.ts'; +import { useKey } from '@kevisual/context'; +import { spawnSync } from 'node:child_process'; + +export const execCommand = (command: string, options: { cwd?: string } = {}) => { + const { cwd } = options; + return spawnSync(command, { + stdio: 'inherit', + shell: true, + cwd: cwd, + env: process.env, + }); +}; +app.route({ + path: 'cnb-board', + key: 'is-cnb-board', + description: '检查是否是 cnb-board 环境', + middleware: ['auth-admin'] +}).define(async (ctx) => { + const isCNB = useKey('CNB'); + ctx.body = { + isCNB: !!isCNB, + }; +}).addTo(app); + + + + +app.route({ + path: 'cnb-board', + key: 'exit', + description: 'cnb的工作环境退出程序', + middleware: ['auth-admin'], +}).define(async (ctx) => { + const cmd = 'kill 1'; + execCommand(cmd); +}).addTo(app); \ No newline at end of file diff --git a/agent/routes/cnb-board/live/live-content.ts b/agent/routes/cnb-board/live/live-content.ts new file mode 100644 index 0000000..f273373 --- /dev/null +++ b/agent/routes/cnb-board/live/live-content.ts @@ -0,0 +1,341 @@ + +import { useKey } from "@kevisual/context" +import os from 'node:os'; +import { execSync } from 'node:child_process'; +import dayjs from 'dayjs'; + +export const getLiveMdContent = (opts?: { more?: boolean }) => { + const more = opts?.more ?? false + const url = useKey('CNB_VSCODE_PROXY_URI') || '' + const token = useKey('CNB_TOKEN') || '' + const openclawPort = useKey('OPENCLAW_PORT') || '80' + const openclawUrl = url.replace('{{port}}', openclawPort) + const openclawUrlSecret = openclawUrl + '/openclaw?token=' + token + + const opencodePort = useKey('OPENCODE_PORT') || '100' + const opencodeUrl = url.replace('{{port}}', opencodePort) + // btoa('root:password'); // + const _opencodeURL = new URL(opencodeUrl) + _opencodeURL.username = 'root' + _opencodeURL.password = token + const opencodeUrlSecret = _opencodeURL.toString() + + // console.log('btoa opencode auth: ', Buffer.from(`root:${token}`).toString('base64')) + const kevisualUrl = url.replace('{{port}}', '51515') + + const openWebUrl = url.replace('{{port}}', '200') + + const vscodeWebUrl = useKey('CNB_VSCODE_WEB_URL') || '' + + const TEMPLATE = `# 开发环境模式配置 + +### 服务访问地址 +#### nginx 反向代理访问(推荐) +- OpenClaw: ${openclawUrl + '/openclaw'} +- OpenCode: ${opencodeUrl} +- VSCode Web: ${vscodeWebUrl} +- OpenWebUI: ${openWebUrl} +- Kevisual: ${kevisualUrl} + +### 密码访问 +- OpenClaw: ${openclawUrlSecret} +- OpenCode: ${opencodeUrlSecret} + +### 环境变量 +- CNB_TOKEN: ${token} + +### 其他说明 + +1. 保活说明 +使用插件访问vscode web获取wss进行保活,避免长时间不操作导致的自动断开连接。 + +方法1: 使用插件访问vscode web获取wss进行保活,避免长时间不操作导致的自动断开连接。 + +1. 安装插件[CNB LIVE](https://chromewebstore.google.com/detail/cnb-live/iajpiophkcdghonpijkcgpjafbcjhkko?pli=1) +2. 打开vscode web获取,点击插件,获取json数据,替换keep.json中的数据,保持在线状态。 +3. keep.json中的数据结构说明: +- wss: vscode web的websocket地址 +- cookie: vscode web的cookie,保持和浏览器一致 +- url: vscode web的访问地址,可以直接访问vscode web +4. 运行cli命令,ev cnb live -c /workspace/live/keep.json.(直接对话opencode或者openclaw调用cnb-live技能即可) + +方法2:环境变量设置CNB_COOKIE,直接opencode或者openclaw的ui界面对话说,cnb-keep-live保活,他会自动调用保活,同时不需要点cnb-lie插件获取配置。 + +2. Opencode web访问说明 +Opencode打开web地址,需要在浏览器输入用户名和密码,用户名固定为root,密码为CNB_TOKEN的值. 纯连接打开包含账号密码,第一次点击后,需要把账号密码清理掉才能访问,opencode的bug导致的。 +` + const labels = [ + { + key: 'vscodeWebUrl', + title: 'VSCode Web 地址', + value: vscodeWebUrl, + description: 'VSCode Web 的访问地址' + }, + { + key: 'kevisualUrl', + title: 'Kevisual 地址', + value: kevisualUrl, + description: 'Kevisual 的访问地址,可以通过该地址访问 Kevisual 服务' + }, + { + key: 'cnbTempToken', + title: 'CNB Token', + value: token, + description: 'CNB 临时 Token,保持和环境变量 CNB_TOKEN 一致' + }, + { + key: 'openWebUrl', + title: 'OpenWebUI 地址', + value: openWebUrl, + description: 'OpenWebUI 的访问地址,可以通过该地址访问 OpenWebUI 服务' + }, + { + key: 'openclawUrl', + title: 'OpenClaw 地址', + value: openclawUrl + '/openclaw', + description: 'OpenClaw 的访问地址,可以通过该地址访问 OpenClaw 服务' + }, + { + key: 'openclawUrlSecret', + title: 'OpenClaw 访问地址(含 Token)', + value: openclawUrlSecret, + description: 'OpenClaw 的访问地址,包含 token 参数,可以直接访问 OpenClaw 服务' + }, + { + key: 'opencodeUrl', + title: 'OpenCode 地址', + value: opencodeUrl, + description: 'OpenCode 的访问地址,可以通过该地址访问 OpenCode 服务' + }, + { + key: 'opencodeUrlSecret', + title: 'OpenCode 访问地址(含 Token)', + value: opencodeUrlSecret, + description: 'OpenCode 的访问地址,包含 token 参数,可以直接访问 OpenCode 服务' + }, + { + key: 'docs', + title: '配置说明文档', + value: TEMPLATE, + description: '开发环境模式配置说明文档' + } + ] + + const osInfoList = createOSInfo(more) + labels.push(...osInfoList) + return labels +} + +const createOSInfo = (more = false) => { + const labels: Array<{ key: string; title: string; value: string | number; description: string }> = [] + const startTimer = useKey('CNB_BUILD_START_TIME') || '' + + // CPU 使用率 + const cpus = os.cpus() + let totalIdle = 0 + let totalTick = 0 + cpus.forEach((cpu) => { + for (const type in cpu.times) { + totalTick += cpu.times[type as keyof typeof cpu.times] + } + totalIdle += cpu.times.idle + }) + const cpuUsage = ((1 - totalIdle / totalTick) * 100).toFixed(2) + + // 内存使用情况 (使用 free 命令) + let memUsed = 0 + let memTotal = 0 + let memFree = 0 + try { + const freeOutput = execSync('free -b', { encoding: 'utf-8' }) + const lines = freeOutput.trim().split('\n') + const memLine = lines.find(line => line.startsWith('Mem:')) + if (memLine) { + const parts = memLine.split(/\s+/) + memTotal = parseInt(parts[1]) + memUsed = parseInt(parts[2]) + memFree = parseInt(parts[3]) + } + } catch (e) { + // 如果 free 命令失败,使用 os 模块 + memTotal = os.totalmem() + memFree = os.freemem() + memUsed = memTotal - memFree + } + const memUsage = memTotal > 0 ? ((memUsed / memTotal) * 100).toFixed(2) : '0.00' + + // 格式化字节为人类可读格式 + const formatBytes = (bytes: number) => { + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + if (bytes === 0) return '0 B' + const i = Math.floor(Math.log(bytes) / Math.log(1024)) + return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i] + } + + // 运行时间格式化 + const formatUptime = (seconds: number) => { + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + const secs = Math.floor(seconds % 60) + let uptimeStr = '' + if (days > 0) uptimeStr += `${days}天 ` + if (hours > 0) uptimeStr += `${hours}小时 ` + if (minutes > 0) uptimeStr += `${minutes}分钟 ` + return `${uptimeStr}${secs}秒` + } + + // 磁盘使用情况 (使用 du 命令,获取当前目录) + let diskUsage = '' + try { + const duOutput = execSync('du -sh .', { encoding: 'utf-8' }) + diskUsage = duOutput.trim().split('\t')[0] + } catch (e) { + diskUsage = '获取失败' + } + + labels.push( + { + key: 'cpuUsage', + title: 'CPU 使用率', + value: `${cpuUsage}%`, + description: 'CPU 使用率' + }, + { + key: 'cpuCores', + title: 'CPU 核心数', + value: cpus.length, + description: 'CPU 核心数' + }, + { + key: 'memoryUsed', + title: '已使用内存', + value: formatBytes(memUsed), + description: '已使用内存' + }, + { + key: 'memoryTotal', + title: '总内存', + value: formatBytes(memTotal), + description: '总内存' + }, + { + key: 'memoryFree', + title: '空闲内存', + value: formatBytes(memFree), + description: '空闲内存' + }, + { + key: 'memoryUsage', + title: '内存使用率', + value: `${memUsage}%`, + description: '内存使用率' + }, + { + key: 'diskUsage', + title: '磁盘使用', + value: diskUsage, + description: '当前目录磁盘使用情况' + }, + ) + + // 如果有 CNB_BUILD_START_TIME,添加构建启动时间 + if (startTimer) { + // startTimer 是日期字符串格式 + const buildStartTime = dayjs(startTimer as string).format('YYYY-MM-DD HH:mm:ss') + const buildStartTimestamp = dayjs(startTimer as string).valueOf() + const buildUptime = Date.now() - buildStartTimestamp + const buildUptimeStr = formatUptime(Math.floor(buildUptime / 1000)) + const maxRunTime = useKey('CNB_PIPELINE_MAX_RUN_TIME') || 0 // 毫秒 + + labels.push( + { + key: 'buildStartTime', + title: '构建启动时间', + value: buildStartTime, + description: '构建启动时间' + }, + { + key: 'buildUptime', + title: '构建已运行时间', + value: buildUptime, + description: `构建已运行时间: ${buildUptimeStr}` + } + ) + if (maxRunTime > 0) { + // 计算到达4点的倒计时 + const now = dayjs() + const today4am = now.hour(4).minute(0).second(0).millisecond(0) + let timeTo4 = today4am.valueOf() - now.valueOf() + if (timeTo4 < 0) { + // 如果已经过了4点,计算到明天4点 + timeTo4 = today4am.add(1, 'day').valueOf() - now.valueOf() + } + const timeTo4Str = `[距离晚上4点重启时间: ${formatUptime(Math.floor(timeTo4 / 1000))}]` + + labels.push({ + key: 'buildMaxRunTime', + title: '最大运行时间', + value: formatUptime(Math.floor(maxRunTime / 1000)), + description: '构建最大运行时间(限制时间)' + }) + labels.unshift({ + key: 'remainingTime', + title: '剩余时间', + value: maxRunTime - buildUptime, + description: '构建剩余时间' + formatUptime(Math.floor((maxRunTime - buildUptime) / 1000)) + ' ' + timeTo4Str + }) + } + } + + // more 为 true 时添加更多系统信息 + if (more) { + const loadavg = os.loadavg() + labels.push( + { + key: 'hostname', + title: '主机名', + value: os.hostname(), + description: '主机名' + }, + { + key: 'platform', + title: '运行平台', + value: os.platform(), + description: '运行平台' + }, + { + key: 'arch', + title: '系统架构', + value: os.arch(), + description: '系统架构' + }, + { + key: 'osType', + title: '操作系统类型', + value: os.type(), + description: '操作系统类型' + }, + { + key: 'loadavg1m', + title: '系统负载 (1分钟)', + value: loadavg[0].toFixed(2), + description: '系统负载 (1分钟)' + }, + { + key: 'loadavg5m', + title: '系统负载 (5分钟)', + value: loadavg[1].toFixed(2), + description: '系统负载 (5分钟)' + }, + { + key: 'loadavg15m', + title: '系统负载 (15分钟)', + value: loadavg[2].toFixed(2), + description: '系统负载 (15分钟)' + } + ) + } + + return labels +} \ No newline at end of file diff --git a/agent/routes/cnb-board/modules/index.ts b/agent/routes/cnb-board/modules/index.ts new file mode 100644 index 0000000..75241e4 --- /dev/null +++ b/agent/routes/cnb-board/modules/index.ts @@ -0,0 +1 @@ +export * from './is-cnb.ts'; \ No newline at end of file diff --git a/agent/routes/cnb-board/modules/is-cnb.ts b/agent/routes/cnb-board/modules/is-cnb.ts new file mode 100644 index 0000000..19b5d2f --- /dev/null +++ b/agent/routes/cnb-board/modules/is-cnb.ts @@ -0,0 +1,6 @@ +import { useKey } from "@kevisual/context"; + +export const isCnb = () => { + const CNB = useKey('CNB'); + return !!CNB; +} \ No newline at end of file diff --git a/agent/routes/index.ts b/agent/routes/index.ts index 3d08e01..366e84b 100644 --- a/agent/routes/index.ts +++ b/agent/routes/index.ts @@ -6,6 +6,7 @@ import './call/index.ts' import './cnb-env/index.ts' import './knowledge/index.ts' import './issues/index.ts' +import './cnb-board/index.ts'; /** * 验证上下文中的 App ID 是否与指定的 App ID 匹配 @@ -25,25 +26,23 @@ const checkAppId = (ctx: any, appId: string) => { return false; } -if (!app.hasRoute('auth')) { - app.route({ - id: 'auth', - path: 'auth', - }).define(async (ctx) => { - // ctx.body = 'Auth Route'; - if (checkAppId(ctx, app.appId)) { - return; - } - }).addTo(app); +app.route({ + id: 'auth', + path: 'auth', +}).define(async (ctx) => { + // ctx.body = 'Auth Route'; + if (checkAppId(ctx, app.appId)) { + return; + } +}).addTo(app, { overwrite: false }); - app.route({ - id: 'admin-auth', - path: 'admin-auth', - middleware: ['auth'], - }).define(async (ctx) => { - // ctx.body = 'Admin Auth Route'; - if (checkAppId(ctx, app.appId)) { - return; - } - }).addTo(app); -} \ No newline at end of file +app.route({ + id: 'admin-auth', + path: 'admin-auth', + middleware: ['auth'], +}).define(async (ctx) => { + // ctx.body = 'Admin Auth Route'; + if (checkAppId(ctx, app.appId)) { + return; + } +}).addTo(app, { overwrite: false }); \ No newline at end of file diff --git a/agent/routes/workspace/keep-file-live.ts b/agent/routes/workspace/keep-file-live.ts deleted file mode 100644 index 439a47d..0000000 --- a/agent/routes/workspace/keep-file-live.ts +++ /dev/null @@ -1,114 +0,0 @@ -import path from 'node:path'; -import fs from 'node:fs'; -import os from 'node:os'; -import { execSync } from 'node:child_process'; - -export type KeepAliveData = { - wsUrl: string; - cookie: string; - repo: string; - pipelineId: string; - createdTime: number; - filePath: string; - pm2Name: string; -} - -type KeepAliveCache = { - data: KeepAliveData[]; -} - -const keepAliveFilePath = path.join(os.homedir(), '.cnb/keepAliveCache.json'); - -export const runLive = (filePath: string, pm2Name: string) => { - // 使用 npx 运行命令 - const cmdArgs = `cnb live -c ${filePath}`; - - // 先停止已存在的同名 pm2 进程 - const stopCmd = `pm2 delete ${pm2Name} 2>/dev/null || true`; - console.log('停止已存在的进程:', stopCmd); - try { - execSync(stopCmd, { stdio: 'inherit' }); - } catch (error) { - console.log('停止进程失败或进程不存在:', error); - } - - // 使用pm2启动 - const pm2Cmd = `pm2 start ev --name ${pm2Name} --no-autorestart -- ${cmdArgs}`; - console.log('执行命令:', pm2Cmd); - try { - const result = execSync(pm2Cmd, { stdio: 'pipe', encoding: 'utf8' }); - console.log(result); - } catch (error) { - console.error("状态码:", error.status); - console.error("错误详情:", error.stderr.toString()); // 这里会显示 ev 命令报的具体错误 - } -} - -export const stopLive = (pm2Name: string): boolean => { - const stopCmd = `pm2 delete ${pm2Name} 2>/dev/null || true`; - console.log('停止进程:', stopCmd); - try { - execSync(stopCmd, { stdio: 'inherit' }); - console.log(`已停止 ${pm2Name} 的保持存活任务`); - return true; - } catch (error) { - console.error('停止进程失败:', error); - } - return false; -} - -export function getKeepAliveCache(): KeepAliveCache { - try { - if (fs.existsSync(keepAliveFilePath)) { - const data = fs.readFileSync(keepAliveFilePath, 'utf-8'); - const cache = JSON.parse(data) as KeepAliveCache; - return cache; - } else { - return { data: [] }; - } - } catch (error) { - console.error('读取保持存活缓存文件失败:', error); - return { data: [] }; - } -} - -export function addKeepAliveData(data: KeepAliveData): KeepAliveCache { - const cache = getKeepAliveCache(); - cache.data.push(data); - runLive(data.filePath, data.pm2Name); - try { - if (!fs.existsSync(path.dirname(keepAliveFilePath))) { - fs.mkdirSync(path.dirname(keepAliveFilePath), { recursive: true }); - } - fs.writeFileSync(keepAliveFilePath, JSON.stringify(cache, null, 2), 'utf-8'); - return cache; - } catch (error) { - console.error('写入保持存活缓存文件失败:', error); - return { data: [] }; - } -} - -export function removeKeepAliveData(repo: string, pipelineId: string): KeepAliveCache { - const cache = getKeepAliveCache(); - cache.data = cache.data.filter(item => item.repo !== repo || item.pipelineId !== pipelineId); - try { - fs.writeFileSync(keepAliveFilePath, JSON.stringify(cache, null, 2), 'utf-8'); - return cache; - } catch (error) { - console.error('写入保持存活缓存文件失败:', error); - return { data: [] }; - } -} - -export const createLiveData = (data: { wsUrl: string, cookie: string, repo: string, pipelineId: string }): KeepAliveData => { - const { wsUrl, cookie, repo, pipelineId } = data; - const createdTime = Date.now(); - const pm2Name = `${repo}__${pipelineId}`.replace(/\//g, '__'); - const filePath = path.join(os.homedir(), '.cnb', `${pm2Name}.json`); - const _newData = { wss: wsUrl, wsUrl, cookie, repo, pipelineId, createdTime, filePath, pm2Name }; - if (!fs.existsSync(path.dirname(filePath))) { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - } - fs.writeFileSync(filePath, JSON.stringify(_newData, null, 2), 'utf-8'); - return _newData; -} \ No newline at end of file diff --git a/agent/routes/workspace/keep.ts b/agent/routes/workspace/keep.ts index 797df80..a47b3d5 100644 --- a/agent/routes/workspace/keep.ts +++ b/agent/routes/workspace/keep.ts @@ -1,6 +1,7 @@ import { tool } from '@kevisual/router'; import { app, cnb } from '../../app.ts'; import { addKeepAliveData, KeepAliveData, removeKeepAliveData, createLiveData } from '../../../src/workspace/keep-file-live.ts'; +import { useKey } from '@kevisual/context'; // 保持工作空间存活技能 app.route({ @@ -75,3 +76,23 @@ app.route({ +app.route({ + path: 'cnb', + key: 'keep-alive-current-workspace', + description: '保持当前工作空间存活技能', + middleware: ['admin-auth'], + metadata: { + tags: ['opencode'], + skill: 'keep-alive-current-workspace', + title: '保持当前工作空间存活', + summary: '保持当前工作空间存活,防止被关闭或释放资源', + } +}).define(async (ctx) => { + const pipelineId = useKey('CNB_PIPELINE_ID'); + const repo = useKey('CNB_REPO_SLUG_LOWERCASE'); + if (!pipelineId || !repo) { + ctx.throw(400, '当前环境缺少 CNB_PIPELINE_ID 或 CNB_REPO_SLUG_LOWERCASE 环境变量,无法保持工作空间存活'); + } + const res = await app.run({ path: 'cnb', key: 'keep-workspace-alive', payload: { repo, pipelineId } }, ctx); + ctx.forward(res); +}).addTo(app); \ No newline at end of file diff --git a/package.json b/package.json index f7f7720..e320d70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevisual/cnb", - "version": "0.0.33", + "version": "0.0.34", "description": "", "main": "index.js", "scripts": { @@ -19,14 +19,14 @@ "packageManager": "pnpm@10.30.3", "type": "module", "devDependencies": { - "@kevisual/ai": "^0.0.24", + "@kevisual/ai": "^0.0.26", "@kevisual/code-builder": "^0.0.6", "@kevisual/dts": "^0.0.4", "@kevisual/context": "^0.0.8", "@kevisual/types": "^0.0.12", - "@opencode-ai/plugin": "^1.2.15", - "@types/bun": "^1.3.9", - "@types/node": "^25.3.2", + "@opencode-ai/plugin": "^1.2.16", + "@types/bun": "^1.3.10", + "@types/node": "^25.3.3", "@types/ws": "^8.18.1", "dayjs": "^1.11.19", "dotenv": "^17.3.1" @@ -39,9 +39,9 @@ }, "dependencies": { "@kevisual/query": "^0.0.52", - "@kevisual/router": "^0.0.84", + "@kevisual/router": "^0.0.85", "@kevisual/use-config": "^1.0.30", - "es-toolkit": "^1.44.0", + "es-toolkit": "^1.45.1", "nanoid": "^5.1.6", "unstorage": "^1.17.4", "ws": "npm:@kevisual/ws", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca032d6..4203d39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,14 +15,14 @@ importers: specifier: ^0.0.52 version: 0.0.52 '@kevisual/router': - specifier: ^0.0.84 - version: 0.0.84 + specifier: ^0.0.85 + version: 0.0.85 '@kevisual/use-config': specifier: ^1.0.30 version: 1.0.30(dotenv@17.3.1) es-toolkit: - specifier: ^1.44.0 - version: 1.44.0 + specifier: ^1.45.1 + version: 1.45.1 nanoid: specifier: ^5.1.6 version: 5.1.6 @@ -37,8 +37,8 @@ importers: version: 4.3.6 devDependencies: '@kevisual/ai': - specifier: ^0.0.24 - version: 0.0.24 + specifier: ^0.0.26 + version: 0.0.26 '@kevisual/code-builder': specifier: ^0.0.6 version: 0.0.6 @@ -52,14 +52,14 @@ importers: specifier: ^0.0.12 version: 0.0.12 '@opencode-ai/plugin': - specifier: ^1.2.15 - version: 1.2.15 + specifier: ^1.2.16 + version: 1.2.16 '@types/bun': - specifier: ^1.3.9 - version: 1.3.9 + specifier: ^1.3.10 + version: 1.3.10 '@types/node': - specifier: ^25.3.2 - version: 25.3.2 + specifier: ^25.3.3 + version: 25.3.3 '@types/ws': specifier: ^8.18.1 version: 8.18.1 @@ -83,8 +83,8 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@kevisual/ai@0.0.24': - resolution: {integrity: sha512-7jvZk1/L//VIClK7usuNgN4ZA9Etgbooka1Sj5quE/0UywR+NNnwqXVZ89Y1fBhI1TkhauDsdJBAtcQ7r/vbVw==} + '@kevisual/ai@0.0.26': + resolution: {integrity: sha512-lhaMpxi+vgqPdyBKiuNbSil4hy13tNLbDiqCtG0qUXKtvoowK6xMx269pSSYkYBivczM8g8I0XEouuJceUpJPg==} '@kevisual/code-builder@0.0.6': resolution: {integrity: sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw==} @@ -103,17 +103,14 @@ packages: '@kevisual/logger@0.0.4': resolution: {integrity: sha512-+fpr92eokSxoGOW1SIRl/27lPuO+zyY+feR5o2Q4YCNlAdt2x64NwC/w8r/3NEC5QenLgd4K0azyKTI2mHbARw==} - '@kevisual/permission@0.0.3': - resolution: {integrity: sha512-8JsA/5O5Ax/z+M+MYpFYdlioHE6jNmWMuFSokBWYs9CCAHNiSKMR01YLkoVDoPvncfH/Y8F5K/IEXRCbptuMNA==} - - '@kevisual/query@0.0.38': - resolution: {integrity: sha512-bfvbSodsZyMfwY+1T2SvDeOCKsT/AaIxlVe0+B1R/fNhlg2MDq2CP0L9HKiFkEm+OXrvXcYDMKPUituVUM5J6Q==} + '@kevisual/permission@0.0.4': + resolution: {integrity: sha512-zwBYPnT/z21W4q2wkklJrxvoYBYWG/+a3iXFDKqXQAnDOcxm/SU1f1N6FQb9KxGKl36/fclVlhxlxqszvKCenQ==} '@kevisual/query@0.0.52': resolution: {integrity: sha512-m1UbyDTIxtfAQXM+EqhXA4ytE2V8rV8mXTZVBwzfW9O6+gtvAcRY7K1YYxfewTSXLVh9nwvfHe0KQ8MDL5ukyw==} - '@kevisual/router@0.0.84': - resolution: {integrity: sha512-l/TUFuqTJegB/S3FZQRBMUoz0Spvg8EzV3C/kBi/VO9KKCzjqZDVvhZJJbTQh9879CBY6vUy1ajo9WcLYnwbNA==} + '@kevisual/router@0.0.85': + resolution: {integrity: sha512-ihSzPXHOMSOnZD/+Eso4yZMt4MoUXyLdfRHhXJGg90+sJBr/BjsmgAokit4pI9gWU+Rs/3JqQ2/aqA43FHtGoA==} '@kevisual/types@0.0.12': resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==} @@ -127,11 +124,11 @@ packages: resolution: {integrity: sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw==} engines: {node: '>=10.0.0'} - '@opencode-ai/plugin@1.2.15': - resolution: {integrity: sha512-mh9S05W+CZZmo6q3uIEBubS66QVgiev7fRafX7vemrCfz+3pEIkSwipLjU/sxIewC9yLiDWLqS73DH/iEQzVDw==} + '@opencode-ai/plugin@1.2.16': + resolution: {integrity: sha512-9Kb7BQIC2P3oKCvI8K3thP5YP0vE7yLvcmBmgyACUIqc3e5UL6U+4umLpTvgQa2eQdjxtOXznuGTNwgcGMHUHg==} - '@opencode-ai/sdk@1.2.15': - resolution: {integrity: sha512-NUJNlyBCdZ4R0EBLjJziEQOp2XbRPJosaMcTcWSWO5XJPKGUpz0u8ql+5cR8K+v2RJ+hp2NobtNwpjEYfe6BRQ==} + '@opencode-ai/sdk@1.2.16': + resolution: {integrity: sha512-y9ae9VnCcuog0GaI4DveX1HB6DBoZgGN3EuJVlRFbBCPwhzkls6fCfHSb5+VnTS6Fy0OWFUL28VBCmixL/D+/Q==} '@rollup/plugin-commonjs@29.0.0': resolution: {integrity: sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==} @@ -311,14 +308,14 @@ packages: cpu: [x64] os: [win32] - '@types/bun@1.3.9': - resolution: {integrity: sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw==} + '@types/bun@1.3.10': + resolution: {integrity: sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/node@25.3.2': - resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==} + '@types/node@25.3.3': + resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -330,8 +327,8 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - bun-types@1.3.9: - resolution: {integrity: sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg==} + bun-types@1.3.10: + resolution: {integrity: sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg==} chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} @@ -363,8 +360,8 @@ packages: resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} - es-toolkit@1.44.0: - resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -575,11 +572,11 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@kevisual/ai@0.0.24': + '@kevisual/ai@0.0.26': dependencies: '@kevisual/logger': 0.0.4 - '@kevisual/permission': 0.0.3 - '@kevisual/query': 0.0.38 + '@kevisual/permission': 0.0.4 + '@kevisual/query': 0.0.52 '@kevisual/code-builder@0.0.6': {} @@ -602,17 +599,13 @@ snapshots: '@kevisual/logger@0.0.4': {} - '@kevisual/permission@0.0.3': {} - - '@kevisual/query@0.0.38': - dependencies: - tslib: 2.8.1 + '@kevisual/permission@0.0.4': {} '@kevisual/query@0.0.52': {} - '@kevisual/router@0.0.84': + '@kevisual/router@0.0.85': dependencies: - es-toolkit: 1.44.0 + es-toolkit: 1.45.1 '@kevisual/types@0.0.12': {} @@ -623,12 +616,12 @@ snapshots: '@kevisual/ws@8.19.0': {} - '@opencode-ai/plugin@1.2.15': + '@opencode-ai/plugin@1.2.16': dependencies: - '@opencode-ai/sdk': 1.2.15 + '@opencode-ai/sdk': 1.2.16 zod: 4.3.6 - '@opencode-ai/sdk@1.2.15': {} + '@opencode-ai/sdk@1.2.16': {} '@rollup/plugin-commonjs@29.0.0(rollup@4.57.1)': dependencies: @@ -744,13 +737,13 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.57.1': optional: true - '@types/bun@1.3.9': + '@types/bun@1.3.10': dependencies: - bun-types: 1.3.9 + bun-types: 1.3.10 '@types/estree@1.0.8': {} - '@types/node@25.3.2': + '@types/node@25.3.3': dependencies: undici-types: 7.18.2 @@ -758,16 +751,16 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.3.2 + '@types/node': 25.3.3 anymatch@3.1.3: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 - bun-types@1.3.9: + bun-types@1.3.10: dependencies: - '@types/node': 25.3.2 + '@types/node': 25.3.3 chokidar@5.0.0: dependencies: @@ -791,7 +784,7 @@ snapshots: dotenv@17.3.1: {} - es-toolkit@1.44.0: {} + es-toolkit@1.45.1: {} estree-walker@2.0.2: {}