@kevisual/cnb 0.0.42 → 0.0.43
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 +5 -4
- package/agent/modules/cnb-manager.ts +20 -2
- package/agent/npc.ts +94 -0
- package/agent/routes/chat/chat.ts +41 -0
- package/agent/routes/index.ts +1 -0
- package/agent/routes/issues/comments.ts +14 -4
- package/agent/routes/issues/list.ts +33 -0
- package/agent/routes/opencode/index.ts +12 -0
- package/dist/cli.js +30510 -21932
- package/dist/keep.js +14 -14
- package/dist/opencode.js +56459 -47891
- package/dist/routes.d.ts +47 -4
- package/dist/routes.js +30383 -21816
- package/package.json +16 -11
- package/src/issue/index.ts +14 -3
- package/src/issue/npc/build-env.ts +217 -0
- package/src/issue/npc/env.ts +73 -296
- package/src/issue/npc/pr-env.ts +95 -0
- package/src/issue/npc/repo-env.ts +56 -0
package/agent/app.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { useContextKey } from '@kevisual/context'
|
|
|
3
3
|
import { useKey } from '@kevisual/use-config'
|
|
4
4
|
import { CNB } from '../src/index.ts';
|
|
5
5
|
import { CNBManager } from './modules/cnb-manager.ts'
|
|
6
|
+
|
|
6
7
|
export const cnbManager = new CNBManager()
|
|
7
8
|
|
|
8
9
|
// CNB_TOKEN是降级兼容变量,推荐使用CNB_API_KEY
|
|
@@ -18,9 +19,9 @@ try {
|
|
|
18
19
|
cnb: new CNB({ token: token, cookie: cookie })
|
|
19
20
|
})
|
|
20
21
|
} catch (error) {
|
|
21
|
-
|
|
22
|
+
process.exit(1)
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
24
25
|
export const app = await useContextKey<App>('app', () => {
|
|
25
26
|
return new App({})
|
|
26
27
|
})
|
|
@@ -28,7 +29,7 @@ export const app = await useContextKey<App>('app', () => {
|
|
|
28
29
|
export const notCNBCheck = (ctx: any) => {
|
|
29
30
|
const isCNB = useKey('CNB');
|
|
30
31
|
if (!isCNB) {
|
|
31
|
-
ctx.throw(400, '当前环境非 cnb-board
|
|
32
|
+
ctx.throw(400, '当前环境非 cnb-board 环境,无法获取内容');
|
|
32
33
|
}
|
|
33
34
|
return false;
|
|
34
|
-
}
|
|
35
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Result } from '@kevisual/query';
|
|
2
2
|
import { CNB } from '../../src/index.ts';
|
|
3
3
|
import { useKey } from '@kevisual/context';
|
|
4
|
+
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
4
5
|
export const getConfig = async (opts: { token?: string }) => {
|
|
5
6
|
const kevisualEnv = useKey('KEVISUAL_ENV')
|
|
6
7
|
const baseUrl = kevisualEnv === 'production' ? 'https://kevisual.cn/api/router' : 'https://kevisual.xiongxiao.me/api/router';
|
|
@@ -32,7 +33,14 @@ type CNBItem = {
|
|
|
32
33
|
runAt?: number
|
|
33
34
|
owner?: boolean
|
|
34
35
|
cnb: CNB
|
|
36
|
+
cnbAi: ReturnType<typeof createOpenAICompatible>
|
|
35
37
|
}
|
|
38
|
+
// const repo = useKey('CNB_REPO_SLUG_LOWERCASE') as string || 'kevision/kevision';
|
|
39
|
+
// export const cnbAi = createOpenAICompatible({
|
|
40
|
+
// baseURL: `https://api.cnb.cool/${repo}/-/ai/`,
|
|
41
|
+
// name: 'custom-cnb',
|
|
42
|
+
// apiKey: token,
|
|
43
|
+
// });
|
|
36
44
|
export class CNBManager {
|
|
37
45
|
cnbMap: Map<string, CNBItem> = new Map()
|
|
38
46
|
constructor() {
|
|
@@ -71,20 +79,24 @@ export class CNBManager {
|
|
|
71
79
|
* @returns CNB 实例
|
|
72
80
|
*/
|
|
73
81
|
async getContext(ctx: any) {
|
|
82
|
+
const item = await this.getCNBItem(ctx)
|
|
83
|
+
return item.cnb
|
|
84
|
+
}
|
|
85
|
+
async getCNBItem(ctx: any) {
|
|
74
86
|
const tokenUser = ctx?.state?.tokenUser
|
|
75
87
|
const username = tokenUser?.username
|
|
76
88
|
if (!username) {
|
|
77
89
|
ctx.throw(403, 'Unauthorized')
|
|
78
90
|
}
|
|
79
91
|
if (username === 'default') {
|
|
80
|
-
return this.getDefaultCNB()
|
|
92
|
+
return this.getDefaultCNB()
|
|
81
93
|
}
|
|
82
94
|
const kevisualToken = ctx.query?.token;
|
|
83
95
|
const item = await this.getCNB({ username, kevisualToken });
|
|
84
96
|
if (!item) {
|
|
85
97
|
ctx.throw(400, '不存在的 CNB 配置项,请检查 登录 Token 是否正确,或添加 CNB 配置')
|
|
86
98
|
}
|
|
87
|
-
return item
|
|
99
|
+
return item;
|
|
88
100
|
}
|
|
89
101
|
addCNB(opts: Partial<CNBItem>) {
|
|
90
102
|
if (!opts.username || !opts.token) {
|
|
@@ -98,6 +110,12 @@ export class CNBManager {
|
|
|
98
110
|
const cnb = opts?.cnb || new CNB({ token: opts.token, cookie: opts.cookie });
|
|
99
111
|
opts.cnb = cnb;
|
|
100
112
|
opts.runAt = Date.now()
|
|
113
|
+
const repoSlug = useKey('CNB_REPO_SLUG_LOWERCASE') as string || 'kevision/kevision';
|
|
114
|
+
opts.cnbAi = createOpenAICompatible({
|
|
115
|
+
baseURL: `https://api.cnb.cool/${repoSlug}/-/ai/`,
|
|
116
|
+
name: `custom-cnb-${opts.username}`,
|
|
117
|
+
apiKey: opts.token,
|
|
118
|
+
})
|
|
101
119
|
this.cnbMap.set(opts.username, opts as CNBItem)
|
|
102
120
|
return opts as CNBItem
|
|
103
121
|
}
|
package/agent/npc.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { app } from './index.ts';
|
|
2
|
+
|
|
3
|
+
import { useIssueEnv, useCommentEnv, useRepoInfoEnv, IssueLabel } from '../src/index.ts'
|
|
4
|
+
import { pick } from 'es-toolkit';
|
|
5
|
+
|
|
6
|
+
const writeToProcess = (message: string) => {
|
|
7
|
+
if (process.send) {
|
|
8
|
+
process.send(message);
|
|
9
|
+
} else {
|
|
10
|
+
console.log(message);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const getIssuesLabels = async () => {
|
|
14
|
+
const issueEnv = useIssueEnv();
|
|
15
|
+
const repoInfoEnv = useRepoInfoEnv();
|
|
16
|
+
const issueId = issueEnv.issueId;
|
|
17
|
+
const repoSlug = repoInfoEnv.repoSlug;
|
|
18
|
+
if (!issueId || !repoSlug) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const res = await app.run({
|
|
22
|
+
path: 'cnb',
|
|
23
|
+
key: 'getIssue',
|
|
24
|
+
payload: {
|
|
25
|
+
repo: repoSlug,
|
|
26
|
+
issueNumber: issueId
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
if (res.code === 200) {
|
|
30
|
+
const issueData = res.data as any;
|
|
31
|
+
const labels = issueData.labels || [];
|
|
32
|
+
return labels as IssueLabel[];
|
|
33
|
+
}
|
|
34
|
+
console.error('获取 Issue 详情失败', res);
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const main = async () => {
|
|
40
|
+
const repoInfoEnv = useRepoInfoEnv();
|
|
41
|
+
const commentEnv = useCommentEnv();
|
|
42
|
+
const issueEnv = useIssueEnv();
|
|
43
|
+
const pickCommentEnv = pick(commentEnv, ['commentId', 'commentIdLabel']);
|
|
44
|
+
const pickIssueEnv = pick(issueEnv, ['issueId', 'issueIdLabel', 'issueIid', 'issueIidLabel', 'issueTitle', 'issueTitleLabel', 'issueDescription', 'issueDescriptionLabel']);
|
|
45
|
+
const pickRepoInfoEnv = pick(repoInfoEnv, ['repoId', 'repoIdLabel', 'repoName', 'repoNameLabel', 'repoSlug', 'repoSlugLabel']);
|
|
46
|
+
// const issueLabels = issueEnv.issueLabels || [];
|
|
47
|
+
const isComment = !!commentEnv.commentId;
|
|
48
|
+
const envList = [
|
|
49
|
+
...Object.entries(pickRepoInfoEnv).map(([key, value]) => `${key}: ${value}`),
|
|
50
|
+
...Object.entries(issueEnv).map(([key, value]) => `${key}: ${value}`),
|
|
51
|
+
...Object.entries(pickCommentEnv).map(([key, value]) => `${key}: ${value}`),
|
|
52
|
+
]
|
|
53
|
+
writeToProcess('当前环境变量:');
|
|
54
|
+
const issueLabels = await getIssuesLabels();
|
|
55
|
+
const issueLabelsNames = issueLabels.map(label => label.name) || [];
|
|
56
|
+
envList.forEach(item => writeToProcess(item));
|
|
57
|
+
if (!isComment && !issueLabelsNames.includes('Run')) {
|
|
58
|
+
writeToProcess('当前 Issue 不包含 Run 标签,跳过执行');
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
const messages = [
|
|
62
|
+
{
|
|
63
|
+
role: 'system',
|
|
64
|
+
content: `你是一个智能的代码助手, 根据用户提供的上下文信息,提供有用的建议和帮助, 如果用户的要求和执行工具不一致,请说出你不能这么做。并把最后的结果提交一个评论到对应的issue中,提交的内容必须不能包含 @ 提及。用户提供的上下文信息如下:`
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
role: 'system',
|
|
68
|
+
content: `相关变量:${JSON.stringify({ ...pickCommentEnv, ...pickIssueEnv, ...pickRepoInfoEnv })}`
|
|
69
|
+
}, {
|
|
70
|
+
role: 'user',
|
|
71
|
+
content: commentEnv.commentBody || pickIssueEnv.issueDescription || '无'
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
writeToProcess('输入消息:');
|
|
75
|
+
writeToProcess(JSON.stringify(messages, null, 2));
|
|
76
|
+
const result = await app.run({
|
|
77
|
+
path: 'cnb',
|
|
78
|
+
key: 'chat',
|
|
79
|
+
payload: {
|
|
80
|
+
messages
|
|
81
|
+
}
|
|
82
|
+
}, { appId: app.appId })
|
|
83
|
+
if (result.code === 200) {
|
|
84
|
+
let _message = result.data.message || []
|
|
85
|
+
writeToProcess('执行完成')
|
|
86
|
+
writeToProcess(JSON.stringify(_message, null, 2))
|
|
87
|
+
process.exit(0)
|
|
88
|
+
} else {
|
|
89
|
+
writeToProcess(result.message || '执行错误')
|
|
90
|
+
process.exit(1)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { runAgent } from '@kevisual/ai/agent'
|
|
2
|
+
import { app, cnbManager } from '../../app.ts';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
|
|
5
|
+
app.route({
|
|
6
|
+
path: 'cnb',
|
|
7
|
+
key: 'chat',
|
|
8
|
+
description: 'cnb智能对话接口',
|
|
9
|
+
middleware: ['auth'],
|
|
10
|
+
metadata: {
|
|
11
|
+
args: {
|
|
12
|
+
question: z.string().describe('用户输入的问题'),
|
|
13
|
+
messages: z.array(z.object({
|
|
14
|
+
role: z.enum(['user', 'assistant']).describe('消息角色,user表示用户输入,assistant表示助手回复'),
|
|
15
|
+
content: z.string().describe('消息内容')
|
|
16
|
+
})).describe('对话消息列表,按照时间顺序排列,包含用户和助手的历史消息'),
|
|
17
|
+
model: z.string().optional().describe('默认auto')
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}).define(async (ctx) => {
|
|
21
|
+
// notCNBCheck(ctx);
|
|
22
|
+
if (!ctx.args.question && !ctx.args.messages) {
|
|
23
|
+
ctx.throw(400, '缺少必要参数,必须提供question或messages');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const model = ctx.args?.model || 'auto'
|
|
27
|
+
const item = await cnbManager.getCNBItem(ctx);
|
|
28
|
+
const cnbAi = item.cnbAi;
|
|
29
|
+
const messages = ctx.args.messages || [{
|
|
30
|
+
role: 'user',
|
|
31
|
+
content: ctx.args.question
|
|
32
|
+
}]
|
|
33
|
+
const result = await runAgent({
|
|
34
|
+
app,
|
|
35
|
+
messages: messages,
|
|
36
|
+
languageModel: cnbAi(model),
|
|
37
|
+
token: '',
|
|
38
|
+
// token: ctx.query.token as string,
|
|
39
|
+
});
|
|
40
|
+
ctx.body = result;
|
|
41
|
+
}).addTo(app);
|
package/agent/routes/index.ts
CHANGED
|
@@ -59,6 +59,7 @@ app.route({
|
|
|
59
59
|
repo: tool.schema.string().optional().describe('代码仓库名称, 如 my-user/my-repo'),
|
|
60
60
|
issueNumber: tool.schema.number().describe('Issue 编号'),
|
|
61
61
|
body: tool.schema.string().describe('评论内容'),
|
|
62
|
+
clearAt: tool.schema.boolean().optional().describe('是否清除评论内容中的 @ 提及,默认: true'),
|
|
62
63
|
},
|
|
63
64
|
summary: '创建 Issue 评论',
|
|
64
65
|
})
|
|
@@ -67,7 +68,8 @@ app.route({
|
|
|
67
68
|
const cnb = await cnbManager.getContext(ctx);
|
|
68
69
|
let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
|
|
69
70
|
const issueNumber = ctx.query?.issueNumber;
|
|
70
|
-
|
|
71
|
+
let body = ctx.query?.body;
|
|
72
|
+
const clearAt = ctx.query?.clearAt ?? true;
|
|
71
73
|
|
|
72
74
|
if (!repo) {
|
|
73
75
|
ctx.throw(400, '缺少参数 repo');
|
|
@@ -78,6 +80,10 @@ app.route({
|
|
|
78
80
|
if (!body) {
|
|
79
81
|
ctx.throw(400, '缺少参数 body');
|
|
80
82
|
}
|
|
83
|
+
if (clearAt && body) {
|
|
84
|
+
// 清除评论内容中的 @ 提及
|
|
85
|
+
body = body.replace(/@/g, '');
|
|
86
|
+
}
|
|
81
87
|
|
|
82
88
|
const res = await cnb.issue.createComment(repo, issueNumber, body);
|
|
83
89
|
ctx.forward(res);
|
|
@@ -138,6 +144,7 @@ app.route({
|
|
|
138
144
|
issueNumber: tool.schema.number().describe('Issue 编号'),
|
|
139
145
|
commentId: tool.schema.number().describe('评论 ID'),
|
|
140
146
|
body: tool.schema.string().describe('评论内容'),
|
|
147
|
+
clearAt: tool.schema.boolean().optional().describe('是否清除评论内容中的 @ 提及,默认: true'),
|
|
141
148
|
},
|
|
142
149
|
summary: '修改 Issue 评论',
|
|
143
150
|
})
|
|
@@ -147,8 +154,8 @@ app.route({
|
|
|
147
154
|
let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
|
|
148
155
|
const issueNumber = ctx.query?.issueNumber;
|
|
149
156
|
const commentId = ctx.query?.commentId;
|
|
150
|
-
|
|
151
|
-
|
|
157
|
+
let body = ctx.query?.body;
|
|
158
|
+
const clearAt = ctx.query?.clearAt ?? true;
|
|
152
159
|
if (!repo) {
|
|
153
160
|
ctx.throw(400, '缺少参数 repo');
|
|
154
161
|
}
|
|
@@ -161,7 +168,10 @@ app.route({
|
|
|
161
168
|
if (!body) {
|
|
162
169
|
ctx.throw(400, '缺少参数 body');
|
|
163
170
|
}
|
|
164
|
-
|
|
171
|
+
if (clearAt && body) {
|
|
172
|
+
// 清除评论内容中的 @ 提及
|
|
173
|
+
body = body.replace(/@/g, '');
|
|
174
|
+
}
|
|
165
175
|
const res = await cnb.issue.updateComment(repo, issueNumber, commentId, body);
|
|
166
176
|
ctx.forward(res);
|
|
167
177
|
}).addTo(app);
|
|
@@ -49,4 +49,37 @@ app.route({
|
|
|
49
49
|
|
|
50
50
|
const res = await cnb.issue.getList(repo, params);
|
|
51
51
|
ctx.forward(res);
|
|
52
|
+
}).addTo(app);
|
|
53
|
+
|
|
54
|
+
app.route({
|
|
55
|
+
path: 'cnb',
|
|
56
|
+
key: 'getIssue',
|
|
57
|
+
description: '获取 单个 Issue',
|
|
58
|
+
middleware: ['auth'],
|
|
59
|
+
metadata: {
|
|
60
|
+
tags: ['opencode'],
|
|
61
|
+
...createSkill({
|
|
62
|
+
skill: 'getIssue',
|
|
63
|
+
title: '获取 单个 Issue',
|
|
64
|
+
args: {
|
|
65
|
+
repo: tool.schema.string().optional().describe('代码仓库名称, 如 my-user/my-repo'),
|
|
66
|
+
issueNumber: tool.schema.union([tool.schema.string(), tool.schema.number()]).describe('Issue 编号'),
|
|
67
|
+
},
|
|
68
|
+
summary: '获取 单个 Issue',
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}).define(async (ctx) => {
|
|
72
|
+
const cnb = await cnbManager.getContext(ctx);
|
|
73
|
+
let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
|
|
74
|
+
const issueNumber = ctx.query?.issueNumber;
|
|
75
|
+
|
|
76
|
+
if (!repo) {
|
|
77
|
+
ctx.throw(400, '缺少参数 repo');
|
|
78
|
+
}
|
|
79
|
+
if (!issueNumber) {
|
|
80
|
+
ctx.throw(400, '缺少参数 issueNumber');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const res = await cnb.issue.getItem(repo, issueNumber);
|
|
84
|
+
ctx.forward(res);
|
|
52
85
|
}).addTo(app);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createOpencodeClient } from "@opencode-ai/sdk"
|
|
2
|
+
|
|
3
|
+
const client = await createOpencodeClient({
|
|
4
|
+
// baseUrl: "https://yccb64t1z-100.cnb.run",
|
|
5
|
+
// auth: async () => {
|
|
6
|
+
// return 'cm9vdDozR0I2MDg5ZGpYOE5oMDFjM1FteE5DWDd0ZkI='
|
|
7
|
+
// }
|
|
8
|
+
baseUrl: "http://localhost:4096",
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const sessionList = await client.session.list()
|
|
12
|
+
console.log(sessionList.data)
|