@kevisual/cnb 0.0.54 → 0.0.56

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/npc.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { app } from './index.ts';
2
2
  import { parse } from '@kevisual/router/src/commander.ts';
3
3
 
4
- import { useIssueEnv, useCommentEnv, useRepoInfoEnv, IssueLabelItem } from '../src/index.ts'
4
+ import { useIssueEnv, useCommentEnv, useRepoInfoEnv, IssueLabelItem, IssueComment } from '../src/index.ts'
5
5
  import { pick } from 'es-toolkit';
6
6
  import z from 'zod';
7
7
  import { useKey } from '@kevisual/context';
@@ -16,9 +16,9 @@ const writeToProcess = (message: string) => {
16
16
  const getIssuesLabels = async () => {
17
17
  const issueEnv = useIssueEnv();
18
18
  const repoInfoEnv = useRepoInfoEnv();
19
- const issueId = issueEnv.issueId;
19
+ const issueIid = issueEnv.issueIid;
20
20
  const repoSlug = repoInfoEnv.repoSlug;
21
- if (!issueId || !repoSlug) {
21
+ if (!issueIid || !repoSlug) {
22
22
  return [];
23
23
  }
24
24
  const res = await app.run({
@@ -26,7 +26,7 @@ const getIssuesLabels = async () => {
26
26
  key: 'getIssue',
27
27
  payload: {
28
28
  repo: repoSlug,
29
- issueNumber: issueId
29
+ issueNumber: issueIid
30
30
  }
31
31
  });
32
32
  if (res.code === 200) {
@@ -38,13 +38,33 @@ const getIssuesLabels = async () => {
38
38
  return []
39
39
 
40
40
  }
41
+ const getIssueComment = async (opts: { repo: string, issueNumber: number }) => {
42
+ const res = await app.run({
43
+ path: 'cnb',
44
+ key: 'list-issue-comments',
45
+ payload: {
46
+ repo: opts.repo,
47
+ issueNumber: opts.issueNumber,
48
+ }
49
+ })
50
+ let comments: { commentId: string, body: string, author: string }[] = [];
51
+ if (res.code === 200) {
52
+ const data = res.data as IssueComment[];
53
+ comments = data.map(item => ({
54
+ commentId: item.id,
55
+ body: item.body,
56
+ author: item.author?.nickname || item.author?.username || 'unknown',
57
+ }));
58
+ }
59
+ return comments;
60
+ }
41
61
 
42
- const main = async ({ exit, question }: { exit: (code: number) => void, question?: string }) => {
62
+ const main = async ({ exit, question, admins }: { exit: (code: number) => void, question?: string, admins: string[] }) => {
43
63
  const repoInfoEnv = useRepoInfoEnv();
44
64
  const commentEnv = useCommentEnv();
45
65
  const issueEnv = useIssueEnv();
46
66
  const pickCommentEnv = pick(commentEnv, ['commentId', 'commentIdLabel']);
47
- const pickIssueEnv = pick(issueEnv, ['issueId', 'issueIdLabel', 'issueIid', 'issueIidLabel', 'issueTitle', 'issueTitleLabel', 'issueDescription', 'issueDescriptionLabel']);
67
+ const pickIssueEnv = pick(issueEnv, ['issueIid', 'issueIidLabel', 'issueTitle', 'issueTitleLabel', 'issueDescription', 'issueDescriptionLabel']);
48
68
  const pickRepoInfoEnv = pick(repoInfoEnv, ['repoId', 'repoIdLabel', 'repoName', 'repoNameLabel', 'repoSlug', 'repoSlugLabel']);
49
69
  // const issueLabels = issueEnv.issueLabels || [];
50
70
  const isComment = !!commentEnv.commentId;
@@ -61,15 +81,40 @@ const main = async ({ exit, question }: { exit: (code: number) => void, question
61
81
  writeToProcess('当前 Issue 不包含 Run 标签,跳过执行');
62
82
  return exit(0);
63
83
  }
84
+ const issueComments = await getIssueComment({ repo: repoInfoEnv.repoSlug || '', issueNumber: Number(issueEnv.issueIid) });
85
+ if (issueComments.length > 0) {
86
+ const lastComment = issueComments[issueComments.length - 1];
87
+ if (admins.length > 0 && !admins.includes(lastComment.author)) {
88
+ writeToProcess(`当前 Issue 最新的评论由 ${lastComment.author} 创建,不是管理员 ${admins.join(', ')},跳过执行`);
89
+ const helperKey = ['帮', '做', '执行', '处理', '解决', '回复', '评论'];
90
+ const hasHelper = helperKey.some(key => question?.includes(key));
91
+ if (!hasHelper) {
92
+ await app.run({
93
+ path: 'cnb', key: 'create-issue-comment', payload: {
94
+ repo: repoInfoEnv.repoSlug || '',
95
+ issueNumber: Number(issueEnv.issueIid),
96
+ body: `当前 Issue 最新的评论由 ${lastComment.author} 创建,不是管理员,不允许执行喵~`
97
+ }
98
+ })
99
+ }
100
+ return exit(0);
101
+ }
102
+ }
103
+ const botName = 'kevisual/cnb(router) '
64
104
  const messages = [
65
105
  {
66
106
  role: 'system',
67
- content: `你是一个智能的代码助手, 根据用户提供的上下文信息,提供有用的建议和帮助, 如果用户的要求和执行工具不一致,请说出你不能这么做。并把最后的结果提交一个评论到对应的issue中,提交的内容必须不能包含 @ 提及。用户提供的上下文信息如下:`
107
+ content: `你是一个智能的代码助手叫${botName}, 根据用户提供的上下文信息,提供有用的建议和帮助, 如果用户的要求和执行工具不一致,请说出你不能这么做。并把最后的结果提交一个评论到对应的issue中,提交的内容必须不能包含 @ 提及。如果你有些决定不能决定,需要咨询,需要任何交互的,也把对应的需求评论到对应的issue。用户提供的上下文信息如下:`
68
108
  },
69
109
  {
70
110
  role: 'system',
71
111
  content: `相关变量:${JSON.stringify({ ...pickCommentEnv, ...pickIssueEnv, ...pickRepoInfoEnv })}`
72
- }, {
112
+ },
113
+ {
114
+ role: "assistant",
115
+ content: '历史评论:' + JSON.stringify(issueComments)
116
+ },
117
+ {
73
118
  role: 'user',
74
119
  content: question || commentEnv.commentBody || pickIssueEnv.issueDescription || '无'
75
120
  }
@@ -116,9 +161,9 @@ app.route({
116
161
  const buildUserNickName = useKey('CNB_BUILD_USER_NICKNAME')
117
162
  let admins = owner.split(',').map(item => item.trim());
118
163
  if (owner && admins.includes(buildUserNickName)) {
119
- await main({ exit });
164
+ await main({ exit, admins });
120
165
  } else {
121
- await main({ exit, question: `你是${owner}的专属助手,请生成一条评论,说明你不具备其他用户能访问的能力。同时你需要提示说明,fork当前仓库后,即可成为你的专属助手` });
166
+ await main({ exit, admins, question: `你是${owner}的专属助手,请生成一条评论,说明你不具备其他用户能访问的能力。同时你需要提示说明,fork当前仓库后,即可成为你的专属助手` });
122
167
  }
123
168
  }).addTo(app)
124
169
 
@@ -12,6 +12,7 @@ import './cnb-manager/index.ts';
12
12
  import './build/index.ts';
13
13
  import './chat/chat.ts';
14
14
  import './missions/index.ts';
15
+ import './labels/index.ts';
15
16
 
16
17
  /**
17
18
  * 验证上下文中的 App ID 是否与指定的 App ID 匹配
@@ -26,8 +26,8 @@ app.route({
26
26
  const cnb = await cnbManager.getContext(ctx);
27
27
  let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
28
28
  const issueNumber = ctx.query?.issueNumber;
29
- const page = ctx.query?.page ? Number(ctx.query.page) : undefined;
30
- const page_size = ctx.query?.page_size ? Number(ctx.query.page_size) : undefined;
29
+ const page = ctx.query?.page ?? 1;
30
+ const page_size = ctx.query?.page_size ?? 100;
31
31
 
32
32
  if (!repo) {
33
33
  ctx.throw(400, '缺少参数 repo');
@@ -0,0 +1 @@
1
+ import './issue-label.ts'
@@ -0,0 +1,195 @@
1
+ import { createSkill, tool } from '@kevisual/router';
2
+ import { app, cnbManager } from '../../app.ts';
3
+ import { useKey } from '@kevisual/context';
4
+
5
+ // 查询 Issue 标签列表
6
+ app.route({
7
+ path: 'cnb',
8
+ key: 'list-issue-labels',
9
+ description: '查询 Issue 的标签列表',
10
+ middleware: ['auth'],
11
+ metadata: {
12
+ tags: ['opencode'],
13
+ ...createSkill({
14
+ skill: 'list-issue-labels',
15
+ title: '查询 Issue 标签列表',
16
+ summary: '查询 Issue 的标签列表',
17
+ args: {
18
+ repo: tool.schema.string().optional().describe('仓库路径, 如 my-user/my-repo'),
19
+ issueNumber: tool.schema.number().describe('Issue 编号'),
20
+ page: tool.schema.number().optional().describe('分页页码,默认 1'),
21
+ pageSize: tool.schema.number().optional().describe('分页每页大小,默认 30'),
22
+ },
23
+ })
24
+ }
25
+ }).define(async (ctx) => {
26
+ const cnb = await cnbManager.getContext(ctx);
27
+ let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
28
+ const issueNumber = ctx.query?.issueNumber;
29
+ const page = ctx.query?.page;
30
+ const pageSize = ctx.query?.pageSize;
31
+
32
+ if (!repo) {
33
+ ctx.throw(400, '缺少参数 repo');
34
+ }
35
+ if (!issueNumber) {
36
+ ctx.throw(400, '缺少参数 issueNumber');
37
+ }
38
+
39
+ const res = await cnb.labels.issueLabel.list(repo, issueNumber, {
40
+ page,
41
+ page_size: pageSize,
42
+ });
43
+ ctx.forward(res);
44
+ }).addTo(app);
45
+
46
+ // 设置 Issue 标签(完全替换)
47
+ app.route({
48
+ path: 'cnb',
49
+ key: 'set-issue-labels',
50
+ description: '设置 Issue 标签(完全替换现有标签)',
51
+ middleware: ['auth'],
52
+ metadata: {
53
+ tags: ['opencode'],
54
+ ...createSkill({
55
+ skill: 'set-issue-labels',
56
+ title: '设置 Issue 标签',
57
+ summary: '设置 Issue 标签(完全替换现有标签)',
58
+ args: {
59
+ repo: tool.schema.string().optional().describe('仓库路径, 如 my-user/my-repo'),
60
+ issueNumber: tool.schema.number().describe('Issue 编号'),
61
+ labels: tool.schema.array(tool.schema.string()).describe('标签名称数组'),
62
+ },
63
+ })
64
+ }
65
+ }).define(async (ctx) => {
66
+ const cnb = await cnbManager.getContext(ctx);
67
+ let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
68
+ const issueNumber = ctx.query?.issueNumber;
69
+ const labels = ctx.query?.labels;
70
+
71
+ if (!repo) {
72
+ ctx.throw(400, '缺少参数 repo');
73
+ }
74
+ if (!issueNumber) {
75
+ ctx.throw(400, '缺少参数 issueNumber');
76
+ }
77
+ if (!labels || !Array.isArray(labels)) {
78
+ ctx.throw(400, '缺少参数 labels');
79
+ }
80
+
81
+ const res = await cnb.labels.issueLabel.set(repo, issueNumber, { labels });
82
+ ctx.forward(res);
83
+ }).addTo(app);
84
+
85
+ // 新增 Issue 标签(追加)
86
+ app.route({
87
+ path: 'cnb',
88
+ key: 'add-issue-labels',
89
+ description: '新增 Issue 标签(追加到现有标签)',
90
+ middleware: ['auth'],
91
+ metadata: {
92
+ tags: ['opencode'],
93
+ ...createSkill({
94
+ skill: 'add-issue-labels',
95
+ title: '新增 Issue 标签',
96
+ summary: '新增 Issue 标签(追加到现有标签)',
97
+ args: {
98
+ repo: tool.schema.string().optional().describe('仓库路径, 如 my-user/my-repo'),
99
+ issueNumber: tool.schema.number().describe('Issue 编号'),
100
+ labels: tool.schema.array(tool.schema.string()).describe('标签名称数组'),
101
+ },
102
+ })
103
+ }
104
+ }).define(async (ctx) => {
105
+ const cnb = await cnbManager.getContext(ctx);
106
+ let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
107
+ const issueNumber = ctx.query?.issueNumber;
108
+ const labels = ctx.query?.labels;
109
+
110
+ if (!repo) {
111
+ ctx.throw(400, '缺少参数 repo');
112
+ }
113
+ if (!issueNumber) {
114
+ ctx.throw(400, '缺少参数 issueNumber');
115
+ }
116
+ if (!labels || !Array.isArray(labels)) {
117
+ ctx.throw(400, '缺少参数 labels');
118
+ }
119
+
120
+ const res = await cnb.labels.issueLabel.add(repo, issueNumber, { labels });
121
+ ctx.forward(res);
122
+ }).addTo(app);
123
+
124
+ // 清空 Issue 标签
125
+ app.route({
126
+ path: 'cnb',
127
+ key: 'clear-issue-labels',
128
+ description: '清空 Issue 标签(移除所有标签)',
129
+ middleware: ['auth'],
130
+ metadata: {
131
+ tags: ['opencode'],
132
+ ...createSkill({
133
+ skill: 'clear-issue-labels',
134
+ title: '清空 Issue 标签',
135
+ summary: '清空 Issue 标签(移除所有标签)',
136
+ args: {
137
+ repo: tool.schema.string().optional().describe('仓库路径, 如 my-user/my-repo'),
138
+ issueNumber: tool.schema.number().describe('Issue 编号'),
139
+ },
140
+ })
141
+ }
142
+ }).define(async (ctx) => {
143
+ const cnb = await cnbManager.getContext(ctx);
144
+ let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
145
+ const issueNumber = ctx.query?.issueNumber;
146
+
147
+ if (!repo) {
148
+ ctx.throw(400, '缺少参数 repo');
149
+ }
150
+ if (!issueNumber) {
151
+ ctx.throw(400, '缺少参数 issueNumber');
152
+ }
153
+
154
+ const res = await cnb.labels.issueLabel.clear(repo, issueNumber);
155
+ ctx.forward(res);
156
+ }).addTo(app);
157
+
158
+ // 删除 Issue 指定标签
159
+ app.route({
160
+ path: 'cnb',
161
+ key: 'remove-issue-label',
162
+ description: '删除 Issue 指定标签',
163
+ middleware: ['auth'],
164
+ metadata: {
165
+ tags: ['opencode'],
166
+ ...createSkill({
167
+ skill: 'remove-issue-label',
168
+ title: '删除 Issue 标签',
169
+ summary: '删除 Issue 指定标签',
170
+ args: {
171
+ repo: tool.schema.string().optional().describe('仓库路径, 如 my-user/my-repo'),
172
+ issueNumber: tool.schema.number().describe('Issue 编号'),
173
+ name: tool.schema.string().describe('标签名称'),
174
+ },
175
+ })
176
+ }
177
+ }).define(async (ctx) => {
178
+ const cnb = await cnbManager.getContext(ctx);
179
+ let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
180
+ const issueNumber = ctx.query?.issueNumber;
181
+ const name = ctx.query?.name;
182
+
183
+ if (!repo) {
184
+ ctx.throw(400, '缺少参数 repo');
185
+ }
186
+ if (!issueNumber) {
187
+ ctx.throw(400, '缺少参数 issueNumber');
188
+ }
189
+ if (!name) {
190
+ ctx.throw(400, '缺少参数 name');
191
+ }
192
+
193
+ const res = await cnb.labels.issueLabel.remove(repo, issueNumber, name);
194
+ ctx.forward(res);
195
+ }).addTo(app);
@@ -16,7 +16,8 @@ app.route({
16
16
  summary: '列出cnb代码仓库, 可选flags参数,如 KnowledgeBase',
17
17
  args: {
18
18
  search: tool.schema.string().optional().describe('搜索关键词'),
19
- pageSize: tool.schema.number().optional().describe('每页数量,默认999'),
19
+ page: tool.schema.number().optional().describe('分页页码,默认 1'),
20
+ pageSize: tool.schema.number().optional().describe('每页数量,默认99'),
20
21
  flags: tool.schema.string().optional().describe('仓库标记,如果是知识库则填写 KnowledgeBase'),
21
22
  },
22
23
  })
@@ -24,13 +25,17 @@ app.route({
24
25
  }).define(async (ctx) => {
25
26
  const cnb = await cnbManager.getContext(ctx);
26
27
  const search = ctx.query?.search;
27
- const pageSize = ctx.query?.pageSize || 9999;
28
+ const page = ctx.query?.page || 1;
29
+ let pageSize = ctx.query?.pageSize || 99;
28
30
  const flags = ctx.query?.flags;
29
31
  const params: any = {};
30
32
  if (flags) {
31
33
  params.flags = flags;
32
34
  }
33
- const res = await cnb.repo.getRepoList({ search, page_size: pageSize, role: 'developer', ...params });
35
+ if (pageSize > 99) {
36
+ pageSize = 99;
37
+ }
38
+ const res = await cnb.repo.getRepoList({ search, page, page_size: pageSize + 1, role: 'developer', ...params });
34
39
  if (res.code === 200) {
35
40
  const repos = res.data.map((item) => ({
36
41
  name: item.name,
@@ -38,7 +43,9 @@ app.route({
38
43
  description: item.description,
39
44
  web_url: item.web_url,
40
45
  }));
41
- ctx.body = { content: JSON.stringify(repos), list: res.data };
46
+ const list = repos.slice(0, pageSize);
47
+ const hasMore = repos.length > pageSize;
48
+ ctx.body = { content: JSON.stringify(repos), list, hasMore, page, pageSize };
42
49
  } else {
43
50
  ctx.throw(500, '获取仓库列表失败');
44
51
  }
@@ -4,6 +4,7 @@ import z from 'zod';
4
4
  import './skills.ts';
5
5
  import './keep.ts';
6
6
  import './build.ts';
7
+ import './rerun.ts';
7
8
 
8
9
  // 启动工作空间
9
10
  app.route({
@@ -0,0 +1,55 @@
1
+ import z from 'zod';
2
+ import { app, cnbManager } from '../../app.ts';
3
+
4
+ app.route({
5
+ path: 'cnb',
6
+ key: 'rerun',
7
+ description: '重新启动工作区,定时任务',
8
+ middleware: ['auth'],
9
+ metadata: {
10
+ args: {
11
+ repo: z.string().optional().describe('仓库名称,例如:owner/repo'),
12
+ config: z.string().optional().describe('工作区配置'),
13
+ event: z.string().optional().describe('触发事件来源,api_trigger_event'),
14
+ }
15
+ }
16
+ }).define(async (ctx) => {
17
+ const cnb = await cnbManager.getContext(ctx);
18
+ const repo = ctx.args.repo;
19
+ const config = ctx.args.config;
20
+ const event = ctx.args.event || 'api_trigger_event';
21
+ const res = await cnb.workspace.list({ status: "running" })
22
+ if (res.code !== 200) {
23
+ ctx.throw(500, res.message || 'Failed to list workspaces');
24
+ }
25
+ const list = res.data?.list || []
26
+ const _list = list.filter(item => {
27
+ // 如果指定了 repo 参数,则只重启该仓库相关的工作区;如果未指定 repo 参数,则重启所有包含 '/dev' 的工作区
28
+ if (repo) {
29
+ if (item.slug === repo) {
30
+ return true;
31
+ }
32
+ } else {
33
+ if (item.slug.includes('/dev')) {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ })
39
+ for (const item of _list) {
40
+ const branch = item.branch || 'main';
41
+ const repo = item.slug;
42
+ const sn = item.sn;
43
+ // 先停止工作区
44
+ await cnb.workspace.stopWorkspace({ sn });
45
+ if (config) {
46
+ await cnb.build.startBuild(repo, { branch, config, event });
47
+ } else {
48
+ await cnb.workspace.startWorkspace(repo, { branch });
49
+ }
50
+
51
+ }
52
+ ctx.body = {
53
+ content: '工作区重新启动中',
54
+ }
55
+ }).addTo(app)