@kevisual/cnb 0.0.10 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/agent/app.ts CHANGED
@@ -2,7 +2,6 @@ import { QueryRouterServer as App } from '@kevisual/router'
2
2
  import { useContextKey } from '@kevisual/context'
3
3
  import { useConfig, useKey } from '@kevisual/use-config'
4
4
  import { CNB } from '../src/index.ts';
5
- import { nanoid } from 'nanoid';
6
5
 
7
6
  export const config = useConfig()
8
7
  export const cnb = useContextKey<CNB>('cnb', () => {
@@ -13,9 +12,6 @@ export const cnb = useContextKey<CNB>('cnb', () => {
13
12
  const cookie = useKey('CNB_COOKIE') as string
14
13
  return new CNB({ token: token, cookie: cookie });
15
14
  })
16
- export const appId = nanoid();
17
15
  export const app = useContextKey<App>('app', () => {
18
- return new App({
19
- appId
20
- })
16
+ return new App({})
21
17
  })
package/agent/opencode.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { app} from './index.ts';
1
+ import { app } from './index.ts';
2
2
  import { createRouterAgentPluginFn } from '@kevisual/router/opencode'
3
3
 
4
4
  export const CnbPlugin = createRouterAgentPluginFn({
@@ -2,31 +2,33 @@ import { createSkill } from '@kevisual/router'
2
2
  import { app } from '../../app.ts'
3
3
  import { tool } from '@opencode-ai/plugin/tool'
4
4
 
5
- // "调用 path: cnb key: list-repos"
6
- app.route({
7
- path: 'call',
8
- key: '',
9
- description: '调用',
10
- middleware: ['auth'],
11
- metadata: {
12
- tags: ['opencode'],
13
- ...createSkill({
14
- skill: 'call-app',
15
- title: '调用app应用',
16
- summary: '调用router的应用, 参数path, key, payload',
17
- args: {
18
- path: tool.schema.string().describe('应用路径,例如 cnb'),
19
- key: tool.schema.string().optional().describe('应用key,例如 list-repos'),
20
- payload: tool.schema.object({}).optional().describe('调用参数'),
21
- }
22
- })
23
- },
24
- }).define(async (ctx) => {
25
- const { path, key } = ctx.query;
26
- console.log('call app', ctx.query);
27
- if (!path) {
28
- ctx.throw('路径path不能为空');
29
- }
30
- const res = await ctx.run({ path, key, payload: ctx.query.payload || {} });
31
- ctx.forward(res);
32
- }).addTo(app)
5
+ if (!app.hasRoute('call')) {
6
+ // "调用 path: cnb key: list-repos"
7
+ app.route({
8
+ path: 'call',
9
+ key: '',
10
+ description: '调用',
11
+ middleware: ['admin-auth'],
12
+ metadata: {
13
+ tags: ['opencode'],
14
+ ...createSkill({
15
+ skill: 'call-app',
16
+ title: '调用app应用',
17
+ summary: '调用router的应用, 参数path, key, payload',
18
+ args: {
19
+ path: tool.schema.string().describe('应用路径,例如 cnb'),
20
+ key: tool.schema.string().optional().describe('应用key,例如 list-repos'),
21
+ payload: tool.schema.object({}).optional().describe('调用参数'),
22
+ }
23
+ })
24
+ },
25
+ }).define(async (ctx) => {
26
+ const { path, key } = ctx.query;
27
+ console.log('call app', ctx.query);
28
+ if (!path) {
29
+ ctx.throw('路径path不能为空');
30
+ }
31
+ const res = await ctx.run({ path, key, payload: ctx.query.payload || {} });
32
+ ctx.forward(res);
33
+ }).addTo(app)
34
+ }
@@ -7,7 +7,7 @@ app.route({
7
7
  path: 'cnb',
8
8
  key: 'user-check',
9
9
  description: '检查用户登录状态,参数checkToken,default true; checkCookie, default false',
10
- middleware: ['auth'],
10
+ middleware: ['admin-auth'],
11
11
  metadata: {
12
12
  tags: ['opencode'],
13
13
  ...createSkill({
@@ -6,7 +6,7 @@ app.route({
6
6
  path: 'cnb',
7
7
  key: 'set-cnb-cookie',
8
8
  description: '设置当前cnb工作空间的cookie环境变量',
9
- middleware: ['auth'],
9
+ middleware: ['admin-auth'],
10
10
  metadata: {
11
11
  tags: ['opencode'],
12
12
  ...createSkill({
@@ -33,7 +33,7 @@ app.route({
33
33
  path: 'cnb',
34
34
  key: 'get-cnb-cookie',
35
35
  description: '获取当前cnb工作空间的cookie环境变量',
36
- middleware: ['auth'],
36
+ middleware: ['admin-auth'],
37
37
  metadata: {
38
38
  tags: ['opencode'],
39
39
  ...createSkill({
@@ -11,7 +11,7 @@ app.route({
11
11
  path: 'cnb',
12
12
  key: 'get-cnb-port-uri',
13
13
  description: '获取当前cnb工作空间的port代理uri',
14
- middleware: ['auth'],
14
+ middleware: ['admin-auth'],
15
15
  metadata: {
16
16
  tags: ['opencode'],
17
17
  ...createSkill({
@@ -40,7 +40,7 @@ app.route({
40
40
  path: 'cnb',
41
41
  key: 'get-cnb-vscode-uri',
42
42
  description: '获取当前cnb工作空间的vscode代理uri, 包括多种访问方式, 如web、vscode、codebuddy、cursor、ssh',
43
- middleware: ['auth'],
43
+ middleware: ['admin-auth'],
44
44
  metadata: {
45
45
  tags: ['opencode'],
46
46
  ...createSkill({
@@ -1,4 +1,4 @@
1
- import { app, appId } from '../app.ts';
1
+ import { app } from '../app.ts';
2
2
  import './cnb-env/check.ts'
3
3
  import './repo/index.ts'
4
4
  import './workspace/index.ts'
@@ -31,7 +31,7 @@ if (!app.hasRoute('auth')) {
31
31
  path: 'auth',
32
32
  }).define(async (ctx) => {
33
33
  // ctx.body = 'Auth Route';
34
- if (checkAppId(ctx, appId)) {
34
+ if (checkAppId(ctx, app.appId)) {
35
35
  return;
36
36
  }
37
37
  }).addTo(app);
@@ -42,7 +42,7 @@ if (!app.hasRoute('auth')) {
42
42
  middleware: ['auth'],
43
43
  }).define(async (ctx) => {
44
44
  // ctx.body = 'Admin Auth Route';
45
- if (checkAppId(ctx, appId)) {
45
+ if (checkAppId(ctx, app.appId)) {
46
46
  return;
47
47
  }
48
48
  }).addTo(app);
@@ -7,7 +7,7 @@ app.route({
7
7
  path: 'cnb',
8
8
  key: 'create-issue',
9
9
  description: '创建 Issue, 参数 repo, title, body, assignees, labels, priority',
10
- middleware: ['auth'],
10
+ middleware: ['admin-auth'],
11
11
  metadata: {
12
12
  tags: ['opencode'],
13
13
  ...createSkill({
@@ -51,7 +51,7 @@ app.route({
51
51
  path: 'cnb',
52
52
  key: 'complete-issue',
53
53
  description: '完成 Issue, 参数 repo, issueNumber',
54
- middleware: ['auth'],
54
+ middleware: ['admin-auth'],
55
55
  metadata: {
56
56
  tags: ['opencode'],
57
57
  ...createSkill({
@@ -6,7 +6,7 @@ app.route({
6
6
  path: 'cnb',
7
7
  key: 'list-issues',
8
8
  description: '查询 Issue 列表, 参数 repo, state, keyword, labels, page, page_size 等',
9
- middleware: ['auth'],
9
+ middleware: ['admin-auth'],
10
10
  metadata: {
11
11
  tags: ['opencode'],
12
12
  ...createSkill({
@@ -12,7 +12,7 @@ app.route({
12
12
  path: 'cnb',
13
13
  key: 'cnb-ai-chat',
14
14
  description: '调用cnb的知识库ai对话功能进行聊天',
15
- middleware: ['auth'],
15
+ middleware: ['admin-auth'],
16
16
  metadata: {
17
17
  tags: ['opencode'],
18
18
  ...createSkill({
@@ -88,7 +88,7 @@ app.route({
88
88
  path: 'cnb',
89
89
  key: 'cnb-rag-query',
90
90
  description: '调用cnb的知识库RAG查询功能进行问答',
91
- middleware: ['auth'],
91
+ middleware: ['admin-auth'],
92
92
  metadata: {
93
93
  tags: ['opencode'],
94
94
  ...createSkill({
@@ -8,7 +8,7 @@ app.route({
8
8
  path: 'cnb',
9
9
  key: 'list-repos',
10
10
  description: '列出我的代码仓库',
11
- middleware: ['auth'],
11
+ middleware: ['admin-auth'],
12
12
  metadata: {
13
13
  tags: ['opencode'],
14
14
  ...createSkill({
@@ -7,7 +7,7 @@ app.route({
7
7
  path: 'cnb',
8
8
  key: 'create-repo',
9
9
  description: '创建代码仓库, 参数name, visibility, description',
10
- middleware: ['auth'],
10
+ middleware: ['admin-auth'],
11
11
  metadata: {
12
12
  tags: ['opencode'],
13
13
  ...createSkill({
@@ -47,7 +47,7 @@ app.route({
47
47
  path: 'cnb',
48
48
  key: 'create-repo-file',
49
49
  description: '在代码仓库中创建文件, repoName, filePath, content, encoding',
50
- middleware: ['auth'],
50
+ middleware: ['admin-auth'],
51
51
  metadata: {
52
52
  tags: ['opencode'],
53
53
  ...createSkill({
@@ -86,7 +86,7 @@ app.route({
86
86
  path: 'cnb',
87
87
  key: 'delete-repo',
88
88
  description: '删除代码仓库, 参数name',
89
- middleware: ['auth'],
89
+ middleware: ['admin-auth'],
90
90
  metadata: {
91
91
  tags: ['opencode'],
92
92
  ...createSkill({
@@ -2,13 +2,14 @@ import { createSkill, tool } from '@kevisual/router';
2
2
  import { app, cnb } from '../../app.ts';
3
3
  import z from 'zod';
4
4
  import './skills.ts';
5
+ import './keep.ts';
5
6
 
6
7
  // 启动工作空间
7
8
  app.route({
8
9
  path: 'cnb',
9
10
  key: 'start-workspace',
10
11
  description: '启动开发工作空间, 参数 repo',
11
- middleware: ['auth'],
12
+ middleware: ['admin-auth'],
12
13
  metadata: {
13
14
  tags: ['opencode'],
14
15
  ...createSkill({
@@ -41,7 +42,7 @@ app.route({
41
42
  path: 'cnb',
42
43
  key: 'list-workspace',
43
44
  description: '获取cnb开发工作空间列表,可选参数 status=running 获取运行中的环境',
44
- middleware: ['auth'],
45
+ middleware: ['admin-auth'],
45
46
  metadata: {
46
47
  tags: ['opencode'],
47
48
  ...createSkill({
@@ -72,7 +73,7 @@ app.route({
72
73
  path: 'cnb',
73
74
  key: 'get-workspace',
74
75
  description: '获取工作空间详情,通过 repo 和 sn 获取',
75
- middleware: ['auth'],
76
+ middleware: ['admin-auth'],
76
77
  metadata: {
77
78
  tags: ['opencode'],
78
79
  ...createSkill({
@@ -103,7 +104,7 @@ app.route({
103
104
  path: 'cnb',
104
105
  key: 'delete-workspace',
105
106
  description: '删除工作空间,通过 pipelineId 或 sn',
106
- middleware: ['auth'],
107
+ middleware: ['admin-auth'],
107
108
  metadata: {
108
109
  tags: ['opencode'],
109
110
  ...createSkill({
@@ -113,7 +114,7 @@ app.route({
113
114
  args: {
114
115
  pipelineId: tool.schema.string().optional().describe('流水线 ID,优先使用'),
115
116
  sn: tool.schema.string().optional().describe('流水线构建号'),
116
- sns: tool.schema.array(z.string()).optional().describe('流水线构建号'),
117
+ sns: tool.schema.array(z.string()).optional().describe('批量流水线构建号'),
117
118
  },
118
119
  })
119
120
  }
@@ -142,7 +143,7 @@ app.route({
142
143
  path: 'cnb',
143
144
  key: 'stop-workspace',
144
145
  description: '停止工作空间,通过 pipelineId 或 sn',
145
- middleware: ['auth'],
146
+ middleware: ['admin-auth'],
146
147
  metadata: {
147
148
  tags: ['opencode'],
148
149
  ...createSkill({
@@ -0,0 +1,214 @@
1
+ import { createSkill, tool } from '@kevisual/router';
2
+ import { app, cnb } from '../../app.ts';
3
+ import { nanoid } from 'nanoid';
4
+ import dayjs from 'dayjs';
5
+ import { createKeepAlive } from '../../../src/keep.ts';
6
+
7
+ type AliveInfo = {
8
+ startTime: number;
9
+ updatedTime?: number;
10
+ KeepAlive: ReturnType<typeof createKeepAlive>;
11
+ id: string;// 6位唯一标识符
12
+ }
13
+
14
+ const keepAliveMap = new Map<string, AliveInfo>();
15
+
16
+ // 保持工作空间存活技能
17
+ app.route({
18
+ path: 'cnb',
19
+ key: 'keep-workspace-alive',
20
+ description: '保持工作空间存活技能,参数wsUrl:工作空间访问URL,cookie:访问工作空间所需的cookie',
21
+ middleware: ['admin-auth'],
22
+ metadata: {
23
+ tags: [],
24
+ ...({
25
+ args: {
26
+ wsUrl: tool.schema.string().describe('工作空间的访问URL'),
27
+ cookie: tool.schema.string().describe('访问工作空间所需的cookie')
28
+ }
29
+ })
30
+ }
31
+ }).define(async (ctx) => {
32
+ const wsUrl = ctx.query?.wsUrl as string;
33
+ const cookie = ctx.query?.cookie as string;
34
+ if (!wsUrl) {
35
+ ctx.throw(400, '缺少工作空间访问URL参数');
36
+ }
37
+ if (!cookie) {
38
+ ctx.throw(400, '缺少访问工作空间所需的cookie参数');
39
+ }
40
+
41
+ // 检测是否已在运行(通过 wsUrl 遍历检查)
42
+ const existing = Array.from(keepAliveMap.values()).find(info => (info as AliveInfo).id && (info as any).KeepAlive?.wsUrl === wsUrl);
43
+ if (existing) {
44
+ ctx.body = { message: `工作空间 ${wsUrl} 的保持存活任务已在运行中`, id: (existing as AliveInfo).id };
45
+ return;
46
+ }
47
+
48
+ console.log(`启动保持工作空间 ${wsUrl} 存活的任务`);
49
+ const keep = createKeepAlive({
50
+ wsUrl,
51
+ cookie,
52
+ onConnect: () => {
53
+ console.log(`工作空间 ${wsUrl} 保持存活任务已连接`);
54
+ },
55
+ onMessage: (data) => {
56
+ // 可选:处理收到的消息
57
+ // console.log(`工作空间 ${wsUrl} 收到消息: ${data}`);
58
+ // 通过 wsUrl 找到对应的 id 并更新时间
59
+ for (const info of keepAliveMap.values()) {
60
+ if ((info as any).KeepAlive?.wsUrl === wsUrl) {
61
+ info.updatedTime = Date.now();
62
+ break;
63
+ }
64
+ }
65
+ },
66
+ debug: true,
67
+ onExit: (code) => {
68
+ console.log(`工作空间 ${wsUrl} 保持存活任务已退出,退出码: ${code}`);
69
+ // 通过 wsUrl 找到对应的 id 并删除
70
+ for (const [id, info] of keepAliveMap.entries()) {
71
+ if ((info as any).KeepAlive?.wsUrl === wsUrl) {
72
+ keepAliveMap.delete(id);
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ });
78
+
79
+ const id = nanoid(6).toLowerCase();
80
+ keepAliveMap.set(id, { startTime: Date.now(), updatedTime: Date.now(), KeepAlive: keep, id });
81
+
82
+ ctx.body = { content: `已启动保持工作空间 ${wsUrl} 存活的任务`, id };
83
+ }).addTo(app);
84
+
85
+ // 获取保持工作空间存活任务列表技能
86
+ app.route({
87
+ path: 'cnb',
88
+ key: 'list-keep-alive-tasks',
89
+ description: '获取保持工作空间存活任务列表技能',
90
+ middleware: ['admin-auth'],
91
+ metadata: {
92
+ tags: [],
93
+ }
94
+ }).define(async (ctx) => {
95
+ const list = Array.from(keepAliveMap.entries()).map(([id, info]) => {
96
+ const now = Date.now();
97
+ const duration = Math.floor((now - info.startTime) / 60000); // 分钟
98
+ return {
99
+ id,
100
+ wsUrl: (info as any).KeepAlive?.wsUrl,
101
+ startTime: info.startTime,
102
+ startTimeStr: dayjs(info.startTime).format('YYYY-MM-DD HH:mm'),
103
+ updatedTime: info.updatedTime,
104
+ updatedTimeStr: dayjs(info.updatedTime).format('YYYY-MM-DD HH:mm'),
105
+ duration,
106
+ }
107
+ });
108
+ ctx.body = { list };
109
+ }).addTo(app);
110
+
111
+ // 停止保持工作空间存活技能
112
+ app.route({
113
+ path: 'cnb',
114
+ key: 'stop-keep-workspace-alive',
115
+ description: '停止保持工作空间存活技能, 参数wsUrl:工作空间访问URL或者id',
116
+ middleware: ['admin-auth'],
117
+ metadata: {
118
+ tags: [],
119
+ ...({
120
+ args: {
121
+ wsUrl: tool.schema.string().optional().describe('工作空间的访问URL'),
122
+ id: tool.schema.string().optional().describe('保持存活任务的唯一标识符'),
123
+ }
124
+ })
125
+ }
126
+ }).define(async (ctx) => {
127
+ const wsUrl = ctx.query?.wsUrl as string;
128
+ const id = ctx.query?.id as string;
129
+ if (!wsUrl && !id) {
130
+ ctx.throw(400, '缺少工作空间访问URL参数或唯一标识符');
131
+ }
132
+
133
+ let targetId: string | undefined;
134
+ let wsUrlFound: string | undefined;
135
+
136
+ if (id) {
137
+ const info = keepAliveMap.get(id);
138
+ if (info) {
139
+ targetId = id;
140
+ wsUrlFound = (info as any).KeepAlive?.wsUrl;
141
+ }
142
+ } else if (wsUrl) {
143
+ for (const [key, info] of keepAliveMap.entries()) {
144
+ if ((info as any).KeepAlive?.wsUrl === wsUrl) {
145
+ targetId = key;
146
+ wsUrlFound = wsUrl;
147
+ break;
148
+ }
149
+ }
150
+ }
151
+
152
+ if (targetId) {
153
+ const keepAlive = keepAliveMap.get(targetId);
154
+ const endTime = Date.now();
155
+ const duration = endTime - keepAlive!.startTime;
156
+ keepAlive?.KeepAlive?.disconnect();
157
+ keepAliveMap.delete(targetId);
158
+ ctx.body = { content: `已停止保持工作空间 ${wsUrlFound} 存活的任务,持续时间: ${duration}ms`, id: targetId };
159
+ } else {
160
+ ctx.body = { content: `没有找到对应的工作空间保持存活任务` };
161
+ }
162
+ }).addTo(app);
163
+
164
+ app.route({
165
+ path: 'cnb',
166
+ key: 'reset-keep-workspace-alive',
167
+ description: '对存活的工作空间,startTime进行重置',
168
+ middleware: ['admin-auth'],
169
+ metadata: {
170
+ tags: [],
171
+ }
172
+ }).define(async (ctx) => {
173
+ const now = Date.now();
174
+ for (const info of keepAliveMap.values()) {
175
+ info.startTime = now;
176
+ }
177
+ ctx.body = { content: `已重置所有存活工作空间的开始时间` };
178
+ }).addTo(app);
179
+
180
+ app.route({
181
+ path: 'cnb',
182
+ key: 'clear-keep-workspace-alive',
183
+ description: '对存活的工作空间,超过5小时的进行清理',
184
+ middleware: ['admin-auth'],
185
+ metadata: {
186
+ tags: [],
187
+ }
188
+ }).define(async (ctx) => {
189
+ const res = clearKeepAlive();
190
+ ctx.body = {
191
+ content: `已清理所有存活工作空间中超过5小时的任务` + (res.length ? `,清理项:${res.map(i => i.wsUrl).join(', ')}` : ''),
192
+ list: res
193
+ };
194
+ }).addTo(app);
195
+
196
+ const clearKeepAlive = () => {
197
+ const now = Date.now();
198
+ let clearedArr: { id: string; wsUrl: string }[] = [];
199
+ for (const [id, info] of keepAliveMap.entries()) {
200
+ if (now - info.startTime > FIVE_HOURS) {
201
+ console.log(`工作空间 ${(info as any).KeepAlive?.wsUrl} 超过5小时,自动停止`);
202
+ info.KeepAlive?.disconnect?.();
203
+ keepAliveMap.delete(id);
204
+ clearedArr.push({ id, wsUrl: (info as any).KeepAlive?.wsUrl });
205
+ }
206
+ }
207
+ return clearedArr;
208
+ }
209
+
210
+ // 每5小时自动清理超时的keepAlive任务
211
+ const FIVE_HOURS = 5 * 60 * 60 * 1000;
212
+ setInterval(() => {
213
+ clearKeepAlive();
214
+ }, FIVE_HOURS);
@@ -35,7 +35,7 @@ app.route({
35
35
  path: 'cnb',
36
36
  key: 'clean-closed-workspace',
37
37
  description: '批量删除已停止的cnb工作空间',
38
- middleware: ['auth'],
38
+ middleware: ['admin-auth'],
39
39
  metadata: {
40
40
  tags: ['opencode'],
41
41
  ...createSkill({
package/dist/keep.d.ts CHANGED
@@ -13,6 +13,7 @@ interface KeepAliveConfig {
13
13
  data: string;
14
14
  signedData: string;
15
15
  }) => void;
16
+ onExit?: (code?: number) => void;
16
17
  debug?: boolean;
17
18
  }
18
19
  interface ParsedMessage {
package/dist/keep.js CHANGED
@@ -20,6 +20,7 @@ class WSKeepAlive {
20
20
  onDisconnect: config.onDisconnect ?? (() => {}),
21
21
  onError: config.onError ?? (() => {}),
22
22
  onSign: config.onSign ?? (() => {}),
23
+ onExit: config.onExit ?? (() => {}),
23
24
  debug: config.debug ?? false
24
25
  };
25
26
  this.url = new URL(this.config.wsUrl);
@@ -119,6 +120,7 @@ class WSKeepAlive {
119
120
  handleReconnect() {
120
121
  if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
121
122
  this.log(`Max reconnect attempts (${this.config.maxReconnectAttempts}) reached. Giving up.`);
123
+ this.config.onExit(1);
122
124
  return;
123
125
  }
124
126
  this.reconnectAttempts++;
@@ -133,6 +135,7 @@ class WSKeepAlive {
133
135
  this.stopPing();
134
136
  if (this.ws) {
135
137
  this.ws.close();
138
+ this.config.onExit(0);
136
139
  this.ws = null;
137
140
  }
138
141
  }