From d6e3f67ac3b9a778146bf1e17c34f87637c7f19e Mon Sep 17 00:00:00 2001 From: abearxiong Date: Mon, 23 Feb 2026 18:36:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20cnb-board=20?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E5=8F=8A=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E8=8E=B7=E5=8F=96=20live=20=E7=9A=84=20repo=E3=80=81?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E3=80=81PR=E3=80=81NPC=20=E5=92=8C=E8=AF=84?= =?UTF-8?q?=E8=AE=BA=E4=BF=A1=E6=81=AF=EF=BC=8C=E5=B9=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=96=87=E6=A1=A3=20fix:=20=E6=9B=B4=E6=96=B0=20SKILL.md=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=A0=BC=E5=BC=8F=EF=BC=8C=E8=B0=83=E6=95=B4?= =?UTF-8?q?=20metadata=20=E6=A0=87=E7=AD=BE=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.opencode/skills/kill-opencode/SKILL.md | 9 +- .../skills/opencode-skill-creator/SKILL.md | 77 ++++ .../.opencode/skills/router-creator/SKILL.md | 213 ++++++++++ assistant/package.json | 2 +- assistant/src/routes/call/index.ts | 2 +- assistant/src/routes/cnb-board/cnb-dev-env.ts | 388 ++++++++++++++++++ assistant/src/routes/cnb-board/index.ts | 23 ++ .../src/routes/cnb-board/live/live-content.ts | 262 ++++++++++++ assistant/src/routes/config/index.ts | 11 +- assistant/src/routes/index.ts | 12 +- docs/readme.md | 3 - keep.json | 5 - 12 files changed, 982 insertions(+), 25 deletions(-) create mode 100644 assistant/.opencode/skills/opencode-skill-creator/SKILL.md create mode 100644 assistant/.opencode/skills/router-creator/SKILL.md create mode 100644 assistant/src/routes/cnb-board/cnb-dev-env.ts create mode 100644 assistant/src/routes/cnb-board/index.ts create mode 100644 assistant/src/routes/cnb-board/live/live-content.ts delete mode 100644 docs/readme.md delete mode 100644 keep.json diff --git a/assistant/.opencode/skills/kill-opencode/SKILL.md b/assistant/.opencode/skills/kill-opencode/SKILL.md index 699835f..b106507 100644 --- a/assistant/.opencode/skills/kill-opencode/SKILL.md +++ b/assistant/.opencode/skills/kill-opencode/SKILL.md @@ -1,10 +1,11 @@ --- name: kill-opencode description: 自动查找并杀死所有opencode相关的进程,确保系统资源释放。 -tags: - - opencode - - process-management - - automation +metadata: + tags: + - opencode + - process-management + - automation --- ```bash #!/bin/bash diff --git a/assistant/.opencode/skills/opencode-skill-creator/SKILL.md b/assistant/.opencode/skills/opencode-skill-creator/SKILL.md new file mode 100644 index 0000000..2587285 --- /dev/null +++ b/assistant/.opencode/skills/opencode-skill-creator/SKILL.md @@ -0,0 +1,77 @@ +--- +name: opencode-skill-creator +description: 教你如何在opencode中创建自定义skill +--- + +## Skill 创建指南 + +### 目录结构 + +每个skill都是一个文件夹,包含以下文件: +``` +skills/ +└── your-skill-name/ + └── SKILL.md +``` + +### SKILL.md 文件格式 + +使用 YAML frontmatter 定义元数据: + +```yaml +--- +name: 技能名称 +description: 技能描述 +--- +``` + +### Frontmatter 字段说明 + +| 字段 | 必填 | 说明 | +|------|------|------| +| `name` | 是 | 技能名称,用于显示在技能列表中 | +| `description` | 是 | 技能简短描述 | + +### 技能内容规范 + +1. **描述要详细**:清楚地说明这个skill能做什么 +2. **提供示例**:包含实际使用的代码示例 +3. **说明参数**:列出所有可用参数及其作用 +4. **保持简洁**:避免冗长的说明 + +### 创建步骤 + +1. 在 `/workspace/.opencode/skills/` 目录下创建新文件夹 +2. 在文件夹中创建 `SKILL.md` 文件 +3. 编写frontmatter和内容 +4. 技能将自动被系统识别 + +### 示例 + +```yaml +--- +name: 示例技能 +description: 这是一个示例技能的描述 +--- + +这是一个详细的技能说明文档。 + +## 使用方法 + +```bash +# 示例命令 +echo "Hello World" +``` + +## 参数说明 + +- `param1`: 参数1说明 +- `param2`: 参数2说明 +``` + +### 注意事项 + +- 文件名必须是 `SKILL.md`(大写) +- frontmatter 必须使用 `---` 包裹 +- 内容支持 Markdown 语法 +- 可以包含代码块、表格、列表等 diff --git a/assistant/.opencode/skills/router-creator/SKILL.md b/assistant/.opencode/skills/router-creator/SKILL.md new file mode 100644 index 0000000..3616bbc --- /dev/null +++ b/assistant/.opencode/skills/router-creator/SKILL.md @@ -0,0 +1,213 @@ +--- +name: router-creator +description: 教你如何使用 @kevisual/router 创建和管理路由 +metadata: + tags: + - router + - api + - routes +--- + +# router + +一个轻量级的路由框架,支持链式调用、中间件、嵌套路由等功能。 + +## 快速开始 + +```ts +import { App } from '@kevisual/router'; + +const app = new App(); +app.listen(4002); + +app + .route({ path: 'demo', key: '02' }) + .define(async (ctx) => { + ctx.body = '02'; + }) + .addTo(app); + +app + .route({ path: 'demo', key: '03' }) + .define(async (ctx) => { + ctx.body = '03'; + }) + .addTo(app); +``` + +## 核心概念 + +### RouteContext 属性说明 + +在 route handler 中,你可以通过 `ctx` 访问以下属性: + +| 属性 | 类型 | 说明 | +| --------------- | ---------------------------- | ---------------------------- | +| `query` | `object` | 请求参数,会自动合并 payload | +| `body` | `number \| string \| Object` | 响应内容 | +| `code` | `number` | 响应状态码,默认为 200 | +| `message` | `string` | 响应消息 | +| `state` | `any` | 状态数据,可在路由间传递 | +| `appId` | `string` | 应用标识 | +| `currentId` | `string` | 当前路由ID | +| `currentPath` | `string` | 当前路由路径 | +| `currentKey` | `string` | 当前路由 key | +| `currentRoute` | `Route` | 当前 Route 实例 | +| `progress` | `[string, string][]` | 路由执行路径记录 | +| `nextQuery` | `object` | 传递给下一个路由的参数 | +| `end` | `boolean` | 是否提前结束路由执行 | +| `app` | `QueryRouter` | 路由实例引用 | +| `error` | `any` | 错误信息 | +| `index` | `number` | 当前路由执行深度 | +| `needSerialize` | `boolean` | 是否需要序列化响应数据 | + +### 上下文方法 + +| 方法 | 参数 | 说明 | +| ----------------------------------- | ----------------------------------------- | -------------------------------------------- | +| `ctx.call(msg, ctx?)` | `{ path, key?, payload?, ... } \| { id }` | 调用其他路由,返回完整 context | +| `ctx.run(msg, ctx?)` | `{ path, key?, payload? }` | 调用其他路由,返回 `{ code, data, message }` | +| `ctx.forward(res)` | `{ code, data?, message? }` | 设置响应结果 | +| `ctx.throw(code?, message?, tips?)` | - | 抛出自定义错误 | + +## 完整示例 + +```ts +import { App } from '@kevisual/router'; +import z from 'zod'; +const app = new App(); +app.listen(4002); + +// 基本路由 +app + .route({ path: 'user', key: 'info', id: 'user-info' }) + .define(async (ctx) => { + // ctx.query 包含请求参数 + const { id } = ctx.query; + // 使用 state 在路由间传递数据 + ctx.state.orderId = '12345'; + ctx.body = { id, name: '张三' }; + ctx.code = 200; + }) + .addTo(app); + +app + .route({ path: 'order', key: 'pay', middleware: ['user-info'] }) + .define(async (ctx) => { + // 可以获取前一个路由设置的 state + const { orderId } = ctx.state; + ctx.body = { orderId, status: 'paid' }; + }) + .addTo(app); + +// 调用其他路由 +app + .route({ path: 'dashboard', key: 'stats' }) + .define(async (ctx) => { + // 调用 user/info 路由 + const userRes = await ctx.run({ path: 'user', key: 'info', payload: { id: 1 } }); + // 调用 product/list 路由 + const productRes = await ctx.run({ path: 'product', key: 'list' }); + + ctx.body = { + user: userRes.data, + products: productRes.data, + }; + }) + .addTo(app); + +// 使用 throw 抛出错误 +app + .route({ path: 'admin', key: 'delete' }) + .define(async (ctx) => { + const { id } = ctx.query; + if (!id) { + ctx.throw(400, '缺少参数', 'id is required'); + } + ctx.body = { success: true }; + }) + .addTo(app); +``` + +## 中间件 + +```ts +import { App, Route } from '@kevisual/router'; + +const app = new App(); + +// 定义中间件 +app + .route({ + id: 'auth-example', + description: '权限校验中间件', + }) + .define(async (ctx) => { + const token = ctx.query.token; + if (!token) { + ctx.throw(401, '未登录', '需要 token'); + } + // 验证通过,设置用户信息到 state + ctx.state.tokenUser = { id: 1, name: '用户A' }; + }) + .addTo(app); + +// 使用中间件(通过 id 引用) +app + .route({ path: 'admin', key: 'panel', middleware: ['auth-example'] }) + .define(async (ctx) => { + // 可以访问中间件设置的 state + const { tokenUser } = ctx.state; + ctx.body = { tokenUser }; + }) + .addTo(app); +``` + +## 一个丰富的router示例 + +```ts +import { App } from '@kevisual/router'; +const app = new App(); + +app + .router({ + path: 'dog', + key: 'info', + description: '获取狗的信息', + metedata: { + args: { + owner: z.string().describe('狗主人姓名'), + age: z.number().describe('狗的年龄'), + }, + }, + }) + .define(async (ctx) => { + const { owner, age } = ctx.query; + ctx.body = { + content: `这是一只${age}岁的狗,主人是${owner}`, + }; + }) + .addTo(app); +``` + +## 注意事项 + +1. **path 和 key 的组合是路由的唯一标识**,同一个 path+key 只能添加一个路由,后添加的会覆盖之前的。 + +2. **ctx.call vs ctx.run**: + - `call` 返回完整 context,包含所有属性 + - `run` 返回 `{ code, data, message }` 格式,data 即 body + +3. **ctx.throw 会自动结束执行**,抛出自定义错误。 + +4. **payload 会自动合并到 query**,调用 `ctx.run({ path, key, payload })` 时,payload 会合并到 query。 + +5. **nextQuery 用于传递给 nextRoute**,在当前路由中设置 `ctx.nextQuery`,会在执行 nextRoute 时合并到 query。 + +6. **避免 nextRoute 循环调用**,默认最大深度为 40 次,超过会返回 500 错误。 + +7. **needSerialize 默认为 true**,会自动对 body 进行 JSON 序列化和反序列化。 + +8. **progress 记录执行路径**,可用于调试和追踪路由调用链。 + +9. **中间件找不到会返回 404**,错误信息中会包含找不到的中间件列表。 diff --git a/assistant/package.json b/assistant/package.json index 7333351..f2d8ca6 100644 --- a/assistant/package.json +++ b/assistant/package.json @@ -42,7 +42,7 @@ } }, "devDependencies": { - "@inquirer/prompts": "^8.2.1", + "@inquirer/prompts": "^8.3.0", "@kevisual/ai": "^0.0.24", "@kevisual/api": "^0.0.59", "@kevisual/load": "^0.0.6", diff --git a/assistant/src/routes/call/index.ts b/assistant/src/routes/call/index.ts index 9eb50c0..39a4529 100644 --- a/assistant/src/routes/call/index.ts +++ b/assistant/src/routes/call/index.ts @@ -6,7 +6,7 @@ app.route({ path: 'call', key: '', description: '调用', - middleware: ['auth'], + middleware: ['auth-admin'], metadata: { tags: ['opencode'], ...createSkill({ diff --git a/assistant/src/routes/cnb-board/cnb-dev-env.ts b/assistant/src/routes/cnb-board/cnb-dev-env.ts new file mode 100644 index 0000000..cef72ea --- /dev/null +++ b/assistant/src/routes/cnb-board/cnb-dev-env.ts @@ -0,0 +1,388 @@ +import { app } from '../../app.ts'; +import { useKey } from '@kevisual/context' + +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_URL_HTTPS') || ''; + // 从 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) => { + 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) => { + 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) => { + 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/assistant/src/routes/cnb-board/index.ts b/assistant/src/routes/cnb-board/index.ts new file mode 100644 index 0000000..dd57799 --- /dev/null +++ b/assistant/src/routes/cnb-board/index.ts @@ -0,0 +1,23 @@ +import { app } from '../../app.ts'; +import { getLiveMdContent } from './live/live-content.ts'; +import './cnb-dev-env.ts'; +import z from 'zod'; +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 + const list = getLiveMdContent({ more: more }); + ctx.body = { + title: '开发环境模式配置', + list, + }; +}).addTo(app); + diff --git a/assistant/src/routes/cnb-board/live/live-content.ts b/assistant/src/routes/cnb-board/live/live-content.ts new file mode 100644 index 0000000..4138d93 --- /dev/null +++ b/assistant/src/routes/cnb-board/live/live-content.ts @@ -0,0 +1,262 @@ + +import { useKey } from "@kevisual/context" +import os from 'node:os'; +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 vscodeWebUrl = useKey('CNB_VSCODE_WEB_URL') || '' + + const TEMPLATE = `# 开发环境模式配置 + +### 服务访问地址 +#### nginx 反向代理访问(推荐) +- OpenClaw: ${openclawUrl} +- OpenCode: ${opencodeUrl} + +### 直接访问 +- Kevisual: ${kevisualUrl} +- OpenCode: ${url?.replace('{{port}}', '4096')} +- VSCode Web: ${vscodeWebUrl} + +### 密码访问 +- OpenClaw: ${openclawUrlSecret} +- OpenCode: ${opencodeUrlSecret} + +### 环境变量 +- CNB_TOKEN: ${token} + +### 其他说明 + +使用插件访问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 + +` + const labels = [ + { + title: 'vscodeWebUrl', + value: vscodeWebUrl, + description: 'VSCode Web 的访问地址' + }, + { + title: 'kevisualUrl', + value: kevisualUrl, + description: 'Kevisual 的访问地址,可以通过该地址访问 Kevisual 服务' + }, + { + title: 'cnbTempToken', + value: token, + description: 'CNB 临时 Token,保持和环境变量 CNB_TOKEN 一致' + }, + { + title: 'openclawUrl', + value: openclawUrl, + description: 'OpenClaw 的访问地址,可以通过该地址访问 OpenClaw 服务' + }, + { + title: 'openclawUrlSecret', + value: openclawUrlSecret, + description: 'OpenClaw 的访问地址,包含 token 参数,可以直接访问 OpenClaw 服务' + }, + { + title: 'opencodeUrl', + value: opencodeUrl, + description: 'OpenCode 的访问地址,可以通过该地址访问 OpenCode 服务' + }, + { + title: 'opencodeUrlSecret', + value: opencodeUrlSecret, + description: 'OpenCode 的访问地址,包含 token 参数,可以直接访问 OpenCode 服务' + }, + { + title: 'docs', + value: TEMPLATE, + description: '开发环境模式配置说明文档' + } + ] + + const osInfoList = createOSInfo(more) + labels.push(...osInfoList) + return labels +} + +const createOSInfo = (more = false) => { + const labels: Array<{ title: string; value: string; description: string }> = [] + const startTimer = useKey('CNB_BUILD_START_TIME') || 0 + + // 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) + + // 内存使用情况 + const totalMem = os.totalmem() + const freeMem = os.freemem() + const usedMem = totalMem - freeMem + const memUsage = ((usedMem / totalMem) * 100).toFixed(2) + + // 格式化字节为人类可读格式 + 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 bootTime = os.uptime() + const bootTimeDate = new Date(Date.now() - bootTime * 1000) + const bootTimeStr = dayjs(bootTimeDate).format('YYYY-MM-DD HH:mm:ss') + + // 运行时间格式化 + 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) + return `${days}天 ${hours}小时 ${minutes}分钟 ${secs}秒` + } + + // 磁盘大小 (假设获取 / 目录) + // 注意: Node.js 原生不提供磁盘大小,需要通过 child_process 或假设值 + // 这里使用内存作为参考,实际磁盘需要额外处理 + const diskInfo = '可通过 df -h 命令获取' + + labels.push( + { + title: 'cpuUsage', + value: `${cpuUsage}%`, + description: 'CPU 使用率' + }, + { + title: 'cpuCores', + value: `${cpus.length}`, + description: 'CPU 核心数' + }, + { + title: 'memoryUsed', + value: formatBytes(usedMem), + description: '已使用内存' + }, + { + title: 'memoryTotal', + value: formatBytes(totalMem), + description: '总内存' + }, + { + title: 'memoryUsage', + value: `${memUsage}%`, + description: '内存使用率' + }, + { + title: 'diskInfo', + value: diskInfo, + description: '磁盘信息 (请使用 df -h 命令查看)' + }, + { + title: 'bootTime', + value: bootTimeStr, + description: '系统启动时间' + }, + { + title: 'uptime', + value: formatUptime(bootTime), + description: '系统运行时间' + } + ) + + // 如果有 CNB_BUILD_START_TIME,添加构建启动时间 + if (startTimer) { + const buildStartTime = dayjs(parseInt(startTimer as string)).format('YYYY-MM-DD HH:mm:ss') + const buildUptime = Date.now() - parseInt(startTimer as string) + const buildUptimeStr = formatUptime(Math.floor(buildUptime / 1000)) + labels.push( + { + title: 'buildStartTime', + value: buildStartTime, + description: '构建启动时间' + }, + { + title: 'buildUptime', + value: buildUptimeStr, + description: '构建已运行时间' + } + ) + } + + // more 为 true 时添加更多系统信息 + if (more) { + const loadavg = os.loadavg() + labels.push( + { + title: 'hostname', + value: os.hostname(), + description: '主机名' + }, + { + title: 'platform', + value: os.platform(), + description: '运行平台' + }, + { + title: 'arch', + value: os.arch(), + description: '系统架构' + }, + { + title: 'osType', + value: os.type(), + description: '操作系统类型' + }, + { + title: 'loadavg1m', + value: loadavg[0].toFixed(2), + description: '系统负载 (1分钟)' + }, + { + title: 'loadavg5m', + value: loadavg[1].toFixed(2), + description: '系统负载 (5分钟)' + }, + { + title: 'loadavg15m', + value: loadavg[2].toFixed(2), + description: '系统负载 (15分钟)' + } + ) + } + + return labels +} \ No newline at end of file diff --git a/assistant/src/routes/config/index.ts b/assistant/src/routes/config/index.ts index 2effc0c..5cf0dea 100644 --- a/assistant/src/routes/config/index.ts +++ b/assistant/src/routes/config/index.ts @@ -28,9 +28,14 @@ app app.route({ path: 'config', - key: 'getId' + key: 'getId', + description: '获取appId', + }).define(async (ctx) => { const config = assistantConfig.getCacheAssistantConfig(); - ctx.body = config?.app?.id || null; - + const appId = config?.app?.id || null; + ctx.body = { + id: appId, + } + }).addTo(app); \ No newline at end of file diff --git a/assistant/src/routes/index.ts b/assistant/src/routes/index.ts index cc83f00..548db5b 100644 --- a/assistant/src/routes/index.ts +++ b/assistant/src/routes/index.ts @@ -2,13 +2,14 @@ import { app, assistantConfig } from '../app.ts'; import './config/index.ts'; import './client/index.ts'; import './shop-install/index.ts'; -import './ai/index.ts'; +// import './ai/index.ts'; import './user/index.ts'; import './call/index.ts' import './opencode/index.ts'; import './remote/index.ts'; // import './kevisual/index.ts' +import './cnb-board/index.ts'; import { authCache } from '@/module/cache/auth.ts'; @@ -70,7 +71,7 @@ export const checkAuth = async (ctx: any, isAdmin = false) => { if (!auth.username) { // 初始管理员账号 auth.username = username; - assistantConfig.setConfig({ auth, token: token }); + assistantConfig.setConfig({ auth }); } if (isAdmin && auth.username) { const admins = config.auth?.admin || []; @@ -78,12 +79,6 @@ export const checkAuth = async (ctx: any, isAdmin = false) => { const admin = auth.username; if (admin === username) { isCheckAdmin = true; - const _token = config.token; - if (!_token) { - assistantConfig.setConfig({ token: token }); - } else if (_token && _token.startsWith('st-') && _token !== token) { - assistantConfig.setConfig({ token: token }); - } } if (!isCheckAdmin && admins.length > 0 && admins.includes(username)) { isCheckAdmin = true; @@ -127,6 +122,7 @@ app if (!ctx.query?.token && ctx.appId === app.appId) { return; } + ctx.state.isAdmin = true; const authResult = await checkAuth(ctx, true); if (authResult.code !== 200) { ctx.throw(authResult.code, authResult.message); diff --git a/docs/readme.md b/docs/readme.md deleted file mode 100644 index 83b122b..0000000 --- a/docs/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# envision-cli - -## 上传文件 \ No newline at end of file diff --git a/keep.json b/keep.json deleted file mode 100644 index e5cbc38..0000000 --- a/keep.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "wss": "wss://cnb-pf8-1jhgvbrkm-001.cnb.space:443/stable-3c0b449c6e6e37b44a8a7938c0d8a3049926a64c?reconnectionToken=3d90027f-b2b1-4c60-a3b4-f061b75ec073&reconnection=false&skipWebSocketFrames=false", - "cookie": "orange:workspace:cookie-session:cnb-pf8-1jhgvbrkm-001=9cc870da-d3d5-44ee-afdc-7498e1111186", - "url": "https://cnb-pf8-1jhgvbrkm-001.cnb.space/?folder=/workspace" -} \ No newline at end of file