feat: 添加 cnb-board 路由及相关功能,获取 live 的 repo、构建、PR、NPC 和评论信息,并更新文档
fix: 更新 SKILL.md 文件格式,调整 metadata 标签位置
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
---
|
---
|
||||||
name: kill-opencode
|
name: kill-opencode
|
||||||
description: 自动查找并杀死所有opencode相关的进程,确保系统资源释放。
|
description: 自动查找并杀死所有opencode相关的进程,确保系统资源释放。
|
||||||
tags:
|
metadata:
|
||||||
- opencode
|
tags:
|
||||||
- process-management
|
- opencode
|
||||||
- automation
|
- process-management
|
||||||
|
- automation
|
||||||
---
|
---
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|||||||
77
assistant/.opencode/skills/opencode-skill-creator/SKILL.md
Normal file
77
assistant/.opencode/skills/opencode-skill-creator/SKILL.md
Normal file
@@ -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 语法
|
||||||
|
- 可以包含代码块、表格、列表等
|
||||||
213
assistant/.opencode/skills/router-creator/SKILL.md
Normal file
213
assistant/.opencode/skills/router-creator/SKILL.md
Normal file
@@ -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**,错误信息中会包含找不到的中间件列表。
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@inquirer/prompts": "^8.2.1",
|
"@inquirer/prompts": "^8.3.0",
|
||||||
"@kevisual/ai": "^0.0.24",
|
"@kevisual/ai": "^0.0.24",
|
||||||
"@kevisual/api": "^0.0.59",
|
"@kevisual/api": "^0.0.59",
|
||||||
"@kevisual/load": "^0.0.6",
|
"@kevisual/load": "^0.0.6",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ app.route({
|
|||||||
path: 'call',
|
path: 'call',
|
||||||
key: '',
|
key: '',
|
||||||
description: '调用',
|
description: '调用',
|
||||||
middleware: ['auth'],
|
middleware: ['auth-admin'],
|
||||||
metadata: {
|
metadata: {
|
||||||
tags: ['opencode'],
|
tags: ['opencode'],
|
||||||
...createSkill({
|
...createSkill({
|
||||||
|
|||||||
388
assistant/src/routes/cnb-board/cnb-dev-env.ts
Normal file
388
assistant/src/routes/cnb-board/cnb-dev-env.ts
Normal file
@@ -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);
|
||||||
23
assistant/src/routes/cnb-board/index.ts
Normal file
23
assistant/src/routes/cnb-board/index.ts
Normal file
@@ -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);
|
||||||
|
|
||||||
262
assistant/src/routes/cnb-board/live/live-content.ts
Normal file
262
assistant/src/routes/cnb-board/live/live-content.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -28,9 +28,14 @@ app
|
|||||||
|
|
||||||
app.route({
|
app.route({
|
||||||
path: 'config',
|
path: 'config',
|
||||||
key: 'getId'
|
key: 'getId',
|
||||||
|
description: '获取appId',
|
||||||
|
|
||||||
}).define(async (ctx) => {
|
}).define(async (ctx) => {
|
||||||
const config = assistantConfig.getCacheAssistantConfig();
|
const config = assistantConfig.getCacheAssistantConfig();
|
||||||
ctx.body = config?.app?.id || null;
|
const appId = config?.app?.id || null;
|
||||||
|
ctx.body = {
|
||||||
|
id: appId,
|
||||||
|
}
|
||||||
|
|
||||||
}).addTo(app);
|
}).addTo(app);
|
||||||
@@ -2,13 +2,14 @@ import { app, assistantConfig } from '../app.ts';
|
|||||||
import './config/index.ts';
|
import './config/index.ts';
|
||||||
import './client/index.ts';
|
import './client/index.ts';
|
||||||
import './shop-install/index.ts';
|
import './shop-install/index.ts';
|
||||||
import './ai/index.ts';
|
// import './ai/index.ts';
|
||||||
import './user/index.ts';
|
import './user/index.ts';
|
||||||
import './call/index.ts'
|
import './call/index.ts'
|
||||||
|
|
||||||
import './opencode/index.ts';
|
import './opencode/index.ts';
|
||||||
import './remote/index.ts';
|
import './remote/index.ts';
|
||||||
// import './kevisual/index.ts'
|
// import './kevisual/index.ts'
|
||||||
|
import './cnb-board/index.ts';
|
||||||
|
|
||||||
import { authCache } from '@/module/cache/auth.ts';
|
import { authCache } from '@/module/cache/auth.ts';
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ export const checkAuth = async (ctx: any, isAdmin = false) => {
|
|||||||
if (!auth.username) {
|
if (!auth.username) {
|
||||||
// 初始管理员账号
|
// 初始管理员账号
|
||||||
auth.username = username;
|
auth.username = username;
|
||||||
assistantConfig.setConfig({ auth, token: token });
|
assistantConfig.setConfig({ auth });
|
||||||
}
|
}
|
||||||
if (isAdmin && auth.username) {
|
if (isAdmin && auth.username) {
|
||||||
const admins = config.auth?.admin || [];
|
const admins = config.auth?.admin || [];
|
||||||
@@ -78,12 +79,6 @@ export const checkAuth = async (ctx: any, isAdmin = false) => {
|
|||||||
const admin = auth.username;
|
const admin = auth.username;
|
||||||
if (admin === username) {
|
if (admin === username) {
|
||||||
isCheckAdmin = true;
|
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)) {
|
if (!isCheckAdmin && admins.length > 0 && admins.includes(username)) {
|
||||||
isCheckAdmin = true;
|
isCheckAdmin = true;
|
||||||
@@ -127,6 +122,7 @@ app
|
|||||||
if (!ctx.query?.token && ctx.appId === app.appId) {
|
if (!ctx.query?.token && ctx.appId === app.appId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ctx.state.isAdmin = true;
|
||||||
const authResult = await checkAuth(ctx, true);
|
const authResult = await checkAuth(ctx, true);
|
||||||
if (authResult.code !== 200) {
|
if (authResult.code !== 200) {
|
||||||
ctx.throw(authResult.code, authResult.message);
|
ctx.throw(authResult.code, authResult.message);
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# envision-cli
|
|
||||||
|
|
||||||
## 上传文件
|
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user