@kevisual/cnb 0.0.2 → 0.0.3
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 +10 -3
- package/agent/opencode.ts +6 -0
- package/agent/routes/call/index.ts +32 -0
- package/agent/routes/cnb-env/env.ts +48 -0
- package/agent/routes/cnb-env/index.ts +3 -1
- package/agent/routes/cnb-env/vscode.ts +94 -0
- package/agent/routes/index.ts +15 -2
- package/agent/routes/knowledge/ai.ts +142 -0
- package/agent/routes/knowledge/index.ts +1 -0
- package/agent/routes/repo/index.ts +2 -53
- package/agent/routes/repo/list.ts +45 -0
- package/agent/routes/repo/repo.ts +110 -0
- package/agent/routes/repo/skills.ts +0 -0
- package/agent/routes/user/check.ts +19 -8
- package/agent/routes/workspace/index.ts +146 -2
- package/agent/routes/workspace/skills.ts +64 -0
- package/dist/opencode.d.ts +5 -0
- package/dist/opencode.js +80920 -0
- package/package.json +22 -27
- package/src/cnb-core.ts +3 -3
- package/src/common/cnb-env.ts +57 -0
- package/src/index.ts +2 -9
- package/src/knowledge/index.ts +14 -1
- package/src/repo/index.ts +22 -5
- package/src/{workspace.ts → workspace/index.ts} +2 -2
- package/agent/opencode-plugin.ts +0 -35
package/agent/app.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { QueryRouterServer as App } from '@kevisual/router'
|
|
2
2
|
import { useContextKey } from '@kevisual/context'
|
|
3
|
-
import { useConfig } from '@kevisual/use-config'
|
|
3
|
+
import { useConfig, useKey } from '@kevisual/use-config'
|
|
4
4
|
import { CNB } from '../src/index.ts';
|
|
5
5
|
import { nanoid } from 'nanoid';
|
|
6
6
|
|
|
7
7
|
export const config = useConfig()
|
|
8
8
|
export const cnb = useContextKey<CNB>('cnb', () => {
|
|
9
|
-
|
|
9
|
+
// CNB_TOKEN是降级兼容变量,推荐使用CNB_API_KEY
|
|
10
|
+
// CNB_TOKEN 是流水线自己就有的变量,但是权限比较小
|
|
11
|
+
const token = useKey('CNB_API_KEY') as string || useKey('CNB_TOKEN') as string
|
|
12
|
+
// cookie 变量是可选的
|
|
13
|
+
const cookie = useKey('CNB_COOKIE') as string
|
|
14
|
+
return new CNB({ token: token, cookie: cookie });
|
|
10
15
|
})
|
|
11
16
|
export const appId = nanoid();
|
|
12
17
|
export const app = useContextKey<App>('app', () => {
|
|
13
|
-
return new App(
|
|
18
|
+
return new App({
|
|
19
|
+
appId
|
|
20
|
+
})
|
|
14
21
|
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createSkill } from '@kevisual/router'
|
|
2
|
+
import { app } from '../../app.ts'
|
|
3
|
+
import { tool } from '@opencode-ai/plugin/tool'
|
|
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)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createSkill, tool } from '@kevisual/router';
|
|
2
|
+
import { app, cnb } from '../../app.ts';
|
|
3
|
+
|
|
4
|
+
// 设置 CNB_COOKIE环境变量和获取环境变量,用于界面操作定制模块功能
|
|
5
|
+
app.route({
|
|
6
|
+
path: 'cnb',
|
|
7
|
+
key: 'set-cnb-cookie',
|
|
8
|
+
description: '设置当前cnb工作空间的cookie环境变量',
|
|
9
|
+
middleware: ['auth'],
|
|
10
|
+
metadata: {
|
|
11
|
+
tags: ['opencode'],
|
|
12
|
+
...createSkill({
|
|
13
|
+
skill: 'set-cnb-cookie',
|
|
14
|
+
title: '设置当前cnb工作空间的cookie环境变量',
|
|
15
|
+
summary: '设置当前cnb工作空间的cookie环境变量,用于界面操作定制模块功能,例子:CNBSESSION=xxxx;csrfkey=2222xxxx;',
|
|
16
|
+
args: {
|
|
17
|
+
cookie: tool.schema.string().describe('cnb的cookie值'),
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
}).define(async (ctx) => {
|
|
22
|
+
const cookie = ctx.query?.cookie;
|
|
23
|
+
if (!cookie) {
|
|
24
|
+
ctx.body = { content: '请提供有效的cookie值' };
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
cnb.cookie = cookie;
|
|
28
|
+
ctx.body = { content: '已成功设置cnb的cookie环境变量' };
|
|
29
|
+
}).addTo(app);
|
|
30
|
+
|
|
31
|
+
// 获取 CNB_COOKIE环境变量
|
|
32
|
+
app.route({
|
|
33
|
+
path: 'cnb',
|
|
34
|
+
key: 'get-cnb-cookie',
|
|
35
|
+
description: '获取当前cnb工作空间的cookie环境变量',
|
|
36
|
+
middleware: ['auth'],
|
|
37
|
+
metadata: {
|
|
38
|
+
tags: ['opencode'],
|
|
39
|
+
...createSkill({
|
|
40
|
+
skill: 'get-cnb-cookie',
|
|
41
|
+
title: '获取当前cnb工作空间的cookie环境变量',
|
|
42
|
+
summary: '获取当前cnb工作空间的cookie环境变量,用于界面操作定制模块功能',
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}).define(async (ctx) => {
|
|
46
|
+
const cookie = cnb.cookie || '未设置cookie环境变量';
|
|
47
|
+
ctx.body = { content: `当前cnb工作空间的cookie环境变量为:${cookie}` };
|
|
48
|
+
}).addTo(app);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { createSkill, tool } from '@kevisual/router';
|
|
2
|
+
import { app, cnb } from '../../app.ts';
|
|
3
|
+
|
|
4
|
+
import { CNB_ENV } from "@/common/cnb-env.ts";
|
|
5
|
+
|
|
6
|
+
// 执行技能 get-cnb-port-uri,端口为4096
|
|
7
|
+
// 执行技能 get-cnb-port-uri,端口为51515
|
|
8
|
+
app.route({
|
|
9
|
+
path: 'cnb',
|
|
10
|
+
key: 'get-cnb-port-uri',
|
|
11
|
+
description: '获取当前cnb工作空间的port代理uri',
|
|
12
|
+
middleware: ['auth'],
|
|
13
|
+
metadata: {
|
|
14
|
+
tags: ['opencode'],
|
|
15
|
+
...createSkill({
|
|
16
|
+
skill: 'get-cnb-port-uri',
|
|
17
|
+
title: '获取当前cnb工作空间的port代理uri',
|
|
18
|
+
summary: '获取当前cnb工作空间的port代理uri,用于端口转发',
|
|
19
|
+
args: {
|
|
20
|
+
port: tool.schema.number().optional().describe('端口号,默认为4096'),
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}).define(async (ctx) => {
|
|
25
|
+
const port = ctx.query?.port || 4096;
|
|
26
|
+
const uri = CNB_ENV?.CNB_VSCODE_PROXY_URI as string || '';
|
|
27
|
+
const finalUri = uri.replace('{{port}}', port.toString());
|
|
28
|
+
let content = `
|
|
29
|
+
cnb工作空间的访问uri为:${finalUri}
|
|
30
|
+
`
|
|
31
|
+
ctx.body = { content };
|
|
32
|
+
}).addTo(app);
|
|
33
|
+
|
|
34
|
+
// 获取当前cnb工作空间的vscode代理uri,执行技能 get-cnb-vscode-uri
|
|
35
|
+
// 包括 web 访问uri,vscode 访问uri,codebuddy 访问uri,cursor 访问uri,ssh 连接字符串
|
|
36
|
+
|
|
37
|
+
app.route({
|
|
38
|
+
path: 'cnb',
|
|
39
|
+
key: 'get-cnb-vscode-uri',
|
|
40
|
+
description: '获取当前cnb工作空间的vscode代理uri, 包括多种访问方式, 如web、vscode、codebuddy、cursor、ssh',
|
|
41
|
+
middleware: ['auth'],
|
|
42
|
+
metadata: {
|
|
43
|
+
tags: ['opencode'],
|
|
44
|
+
...createSkill({
|
|
45
|
+
skill: 'get-cnb-vscode-uri',
|
|
46
|
+
title: '获取当前cnb工作空间的编辑器访问地址',
|
|
47
|
+
summary: '获取当前cnb工作空间的vscode代理uri,用于在浏览器中访问vscode,包含多种访问方式,如web、vscode、codebuddy、cursor、ssh',
|
|
48
|
+
args: {
|
|
49
|
+
web: tool.schema.boolean().optional().describe('是否获取vscode web的访问uri,默认为false'),
|
|
50
|
+
vscode: tool.schema.boolean().optional().describe('是否获取vscode的代理uri,默认为true'),
|
|
51
|
+
codebuddy: tool.schema.boolean().optional().describe('是否获取codebuddy的代理uri,默认为false'),
|
|
52
|
+
cursor: tool.schema.boolean().optional().describe('是否获取cursor的代理uri,默认为false'),
|
|
53
|
+
// trae: tool.schema.boolean().optional().describe('是否获取trae的代理uri,默认为false'),
|
|
54
|
+
ssh: tool.schema.boolean().optional().describe('是否获取vscode remote ssh的连接字符串,默认为false'),
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}).define(async (ctx) => {
|
|
59
|
+
const web = ctx.query?.web ?? false;
|
|
60
|
+
const vscode = ctx.query?.vscode ?? true; // 默认true
|
|
61
|
+
const codebuddy = ctx.query?.codebuddy ?? false;
|
|
62
|
+
const cursor = ctx.query?.cursor ?? false;
|
|
63
|
+
// const trae = ctx.query?.trae ?? false;
|
|
64
|
+
const ssh = ctx.query?.ssh ?? false;
|
|
65
|
+
|
|
66
|
+
const webUri = CNB_ENV?.CNB_VSCODE_WEB_URL as string || '';
|
|
67
|
+
const vscodeSchma = CNB_ENV?.CNB_VSCODE_REMOTE_SSH_SCHEMA as string || '';
|
|
68
|
+
let content = `
|
|
69
|
+
当前的cnb 仓库为:${CNB_ENV?.CNB_REPO_SLUG}
|
|
70
|
+
|
|
71
|
+
`
|
|
72
|
+
if (web) {
|
|
73
|
+
content += `VS Code Web 访问 URI:${webUri}\n\n`;
|
|
74
|
+
}
|
|
75
|
+
if (vscode) {
|
|
76
|
+
content += `VS Code 访问 URI:${vscodeSchma}\n\n`;
|
|
77
|
+
}
|
|
78
|
+
if (codebuddy) {
|
|
79
|
+
const codebuddyUri = vscodeSchma.replace('vscode://vscode-remote/ssh-remote+', 'codebuddycn://vscode-remote/codebuddy-remote');
|
|
80
|
+
content += `CodeBuddy 访问 URI:${codebuddyUri}\n\n`;
|
|
81
|
+
}
|
|
82
|
+
if (cursor) {
|
|
83
|
+
const cursorUri = vscodeSchma.replace('vscode://', 'cursor://');
|
|
84
|
+
content += `Cursor 访问 URI:${cursorUri}\n\n`;
|
|
85
|
+
}
|
|
86
|
+
// if (trae) {
|
|
87
|
+
// const traeUri = vscodeSchma.replace('vscode://vscode-remote/ssh-remote+', 'traecn://ssh-remote+');
|
|
88
|
+
// content += `Trae 访问 URI:${traeUri}\n\n`;
|
|
89
|
+
// }
|
|
90
|
+
if (ssh) {
|
|
91
|
+
content += `VS Code Remote SSH 连接字符串:ssh ${CNB_ENV.CNB_PIPELINE_ID}.${CNB_ENV.CNB_VSCODE_SSH_TOKEN}@cnb.space`;
|
|
92
|
+
}
|
|
93
|
+
ctx.body = { content };
|
|
94
|
+
}).addTo(app);
|
package/agent/routes/index.ts
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
import { app, appId } from '
|
|
1
|
+
import { app, appId } from '../app.ts';
|
|
2
2
|
import './user/check.ts'
|
|
3
|
+
import './repo/index.ts'
|
|
4
|
+
import './workspace/index.ts'
|
|
5
|
+
import './call/index.ts'
|
|
6
|
+
import './cnb-env/index.ts'
|
|
7
|
+
import './knowledge/index.ts'
|
|
3
8
|
|
|
9
|
+
import { isEqual } from 'es-toolkit'
|
|
10
|
+
/**
|
|
11
|
+
* 验证上下文中的 App ID 是否与指定的 App ID 匹配
|
|
12
|
+
* @param {any} ctx - 上下文对象,可能包含 appId 属性
|
|
13
|
+
* @param {string} appId - 需要验证的目标 App ID
|
|
14
|
+
* @returns {boolean} 如果 ctx 中包含 appId 且匹配则返回 true,否则返回 false
|
|
15
|
+
* @throws {Error} 如果 ctx 中包含 appId 但不匹配,则抛出 403 错误
|
|
16
|
+
*/
|
|
4
17
|
const checkAppId = (ctx: any, appId: string) => {
|
|
5
|
-
const _appId = ctx?.appId;
|
|
18
|
+
const _appId = ctx?.app?.appId;
|
|
6
19
|
if (_appId) {
|
|
7
20
|
if (_appId !== appId) {
|
|
8
21
|
ctx.throw(403, 'Invalid App ID');
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createSkill, tool } from '@kevisual/router';
|
|
2
|
+
import { app, cnb } from '../../app.ts';
|
|
3
|
+
import { CNBChat } from '@kevisual/ai/browser'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
|
|
7
|
+
调用cnb-ai-chat技能, repo为kevisual/starred-auto.
|
|
8
|
+
问题是:用户提供的问题是OpenListTeam/OpenList是什么,有多少star
|
|
9
|
+
|
|
10
|
+
*/
|
|
11
|
+
app.route({
|
|
12
|
+
path: 'cnb',
|
|
13
|
+
key: 'cnb-ai-chat',
|
|
14
|
+
description: '调用cnb的知识库ai对话功能进行聊天',
|
|
15
|
+
middleware: ['auth'],
|
|
16
|
+
metadata: {
|
|
17
|
+
tags: ['opencode'],
|
|
18
|
+
...createSkill({
|
|
19
|
+
skill: 'cnb-ai-chat',
|
|
20
|
+
title: '调用cnb的知识库ai对话功能进行聊天',
|
|
21
|
+
summary: '调用cnb的知识库ai对话功能进行聊天,基于cnb提供的ai能力',
|
|
22
|
+
args: {
|
|
23
|
+
question: tool.schema.string().describe('用户输入的消息内容'),
|
|
24
|
+
repo: tool.schema.string().optional().describe('知识库仓库ID,默认为空表示使用默认知识库'),
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}).define(async (ctx) => {
|
|
29
|
+
const question = ctx.query?.question;
|
|
30
|
+
if (!question) {
|
|
31
|
+
ctx.body = { content: '请提供有效的消息内容' };
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
let repo = ctx.query?.repo;
|
|
35
|
+
if (!repo) {
|
|
36
|
+
// 如果未指定知识库仓库ID,则使用默认知识库
|
|
37
|
+
const res = await cnb.repo.getRepoList({ flags: 'KnowledgeBase' });
|
|
38
|
+
if (res.code === 200 && res.data.length > 0) {
|
|
39
|
+
repo = res.data[0].path;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
console.log("Using knowledge base repo:", repo);
|
|
43
|
+
const ragRes = await cnb.knowledgeBase.queryKnowledgeBase(repo || '', {
|
|
44
|
+
query: question,
|
|
45
|
+
score_threshold: 0.62,
|
|
46
|
+
top_k: 10,
|
|
47
|
+
});
|
|
48
|
+
if (ragRes.code !== 200) {
|
|
49
|
+
ctx.body = { content: `查询知识库失败,错误信息:${ragRes.message}` };
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const list = ragRes.data || [];
|
|
53
|
+
// 构建RAG上下文,包含文件来源和相似度信息
|
|
54
|
+
const ragContext = list.map((item, index) => {
|
|
55
|
+
const source = item.metadata?.path || item.metadata?.name || '未知来源';
|
|
56
|
+
const type = item.metadata?.type === 'code' ? '〔代码〕' : '〔文档〕';
|
|
57
|
+
const scorePercent = Math.round((item.score || 0) * 100);
|
|
58
|
+
const url = item.metadata?.url || '无';
|
|
59
|
+
return `〔来源${index + 1}〕${type} ${source} (相似度: ${scorePercent}%)\n${item.chunk}\n访问地址: ${url}`;
|
|
60
|
+
}).join('\n\n---\n\n');
|
|
61
|
+
// hunyuan-a13b
|
|
62
|
+
// enable_thinking
|
|
63
|
+
const chat = new CNBChat({
|
|
64
|
+
repo,
|
|
65
|
+
token: cnb.token,
|
|
66
|
+
model: 'hunyuan-a13b'
|
|
67
|
+
});
|
|
68
|
+
const messages = [
|
|
69
|
+
{
|
|
70
|
+
role: 'system',
|
|
71
|
+
content: `[角色定义]='''\n你是一个专业的技术助手,擅长基于提供的知识库内容进行准确、有条理的分析和回答。你的特点是:\n1. 严格基于RAG检索到的上下文内容进行回答,不添加未经验证的信息\n2. 回答时清晰标注信息来源,便于用户追溯查证\n3. 面对不确定的信息,明确标注「根据提供的内容无法确定」\n4. 代码相关问题注重可执行性和最佳实践\n'''[要求]='''\n1. 严格遵循用户的提问要求,优先解决用户的核心问题\n2. 避免侵犯版权的内容,不复制原文超过100字(技术术语和函数名除外)\n3. 使用中文进行响应,语言专业且易于理解\n4. 如果上下文存在多个来源,优先使用相似度更高的内容\n5. 对于代码片段,确保完整且可直接使用\n6. 当上下文中没有相关信息时,直接说明「知识库中未找到相关内容」\n7. 在思考过程中分析:用户的真实意图是什么?提供的上下文是否足够回答?\n'''[回答格式]='''\n- 先简要说明回答的核心结论\n- 如有必要,分点阐述详细分析过程\n- 标注关键信息来源(标注【来源X】即可)\n- 提供可操作的建议或代码示例\n'''`
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
role: 'user',
|
|
75
|
+
content: `[上下文]='''\n${ragContext}\n'''\n\n[用户问题]='''\n${question}\n'''\n\n请基于以上上下文知识库内容回答用户问题。`
|
|
76
|
+
}
|
|
77
|
+
] as Array<{ role: 'system' | 'user' | 'assistant', content: string }>;
|
|
78
|
+
const response = await chat.chat({
|
|
79
|
+
messages
|
|
80
|
+
});
|
|
81
|
+
const txt = chat.responseText;
|
|
82
|
+
ctx.body = { content: txt, response };
|
|
83
|
+
}).addTo(app);
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
// RAG知识库查询技能: 查询openlist有多少star
|
|
87
|
+
app.route({
|
|
88
|
+
path: 'cnb',
|
|
89
|
+
key: 'cnb-rag-query',
|
|
90
|
+
description: '调用cnb的知识库RAG查询功能进行问答',
|
|
91
|
+
middleware: ['auth'],
|
|
92
|
+
metadata: {
|
|
93
|
+
tags: ['opencode'],
|
|
94
|
+
...createSkill({
|
|
95
|
+
skill: 'cnb-rag-query',
|
|
96
|
+
title: '调用cnb的知识库RAG查询功能进行问答',
|
|
97
|
+
summary: '调用cnb的知识库RAG查询功能进行问答,基于cnb提供的知识库能力',
|
|
98
|
+
args: {
|
|
99
|
+
question: tool.schema.string().describe('用户输入的消息内容'),
|
|
100
|
+
repo: tool.schema.string().optional().describe('知识库仓库ID,默认为空表示使用默认知识库'),
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}).define(async (ctx) => {
|
|
105
|
+
const question = ctx.query?.question;
|
|
106
|
+
if (!question) {
|
|
107
|
+
ctx.body = { content: '请提供有效的消息内容' };
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
let repo = ctx.query?.repo;
|
|
111
|
+
if (!repo) {
|
|
112
|
+
// 如果未指定知识库仓库ID,则使用默认知识库
|
|
113
|
+
const res = await cnb.repo.getRepoList({ flags: 'KnowledgeBase' });
|
|
114
|
+
if (res.code === 200 && res.data.length > 0) {
|
|
115
|
+
repo = res.data[0].path;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
console.log("Using knowledge base repo:", repo);
|
|
119
|
+
const ragRes = await cnb.knowledgeBase.queryKnowledgeBase(repo || '', {
|
|
120
|
+
query: question,
|
|
121
|
+
score_threshold: 0.62,
|
|
122
|
+
top_k: 10,
|
|
123
|
+
});
|
|
124
|
+
if (ragRes.code !== 200) {
|
|
125
|
+
ctx.body = { content: `查询知识库失败,错误信息:${ragRes.message}` };
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const list = ragRes.data || [];
|
|
129
|
+
let answer = `基于知识库「${repo}」的查询结果:\n\n`;
|
|
130
|
+
if (list.length === 0) {
|
|
131
|
+
answer += '知识库中未找到相关内容。';
|
|
132
|
+
} else {
|
|
133
|
+
list.forEach((item, index) => {
|
|
134
|
+
const source = item.metadata?.path || item.metadata?.name || '未知来源';
|
|
135
|
+
const type = item.metadata?.type === 'code' ? '〔代码〕' : '〔文档〕';
|
|
136
|
+
const scorePercent = Math.round((item.score || 0) * 100);
|
|
137
|
+
const url = item.metadata?.url || '无';
|
|
138
|
+
answer += `【来源${index + 1}】${type} ${source} (相似度: ${scorePercent}%)\n${item.chunk}\n访问地址: ${url}\n\n`;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
ctx.body = { content: answer };
|
|
142
|
+
}).addTo(app);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './ai.ts'
|
|
@@ -1,53 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
app.route({
|
|
4
|
-
path: 'cnb',
|
|
5
|
-
key: 'repo-create',
|
|
6
|
-
description: '创建代码仓库, 参数name, visibility, description',
|
|
7
|
-
middleware: ['auth'],
|
|
8
|
-
metadata: {
|
|
9
|
-
tags: ['opencode']
|
|
10
|
-
}
|
|
11
|
-
}).define(async (ctx) => {
|
|
12
|
-
const name = ctx.query?.name;
|
|
13
|
-
const visibility = ctx.query?.visibility ?? 'private';
|
|
14
|
-
const description = ctx.query?.description ?? '';
|
|
15
|
-
|
|
16
|
-
if (!name) {
|
|
17
|
-
ctx.throw(400, '缺少参数 name');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const res = await cnb.repo.createRepo(cnb.group, {
|
|
21
|
-
name,
|
|
22
|
-
visibility,
|
|
23
|
-
description,
|
|
24
|
-
});
|
|
25
|
-
ctx.forward(res);
|
|
26
|
-
}).addTo(app);
|
|
27
|
-
|
|
28
|
-
app.route({
|
|
29
|
-
path: 'cnb',
|
|
30
|
-
key: 'repo-create-file',
|
|
31
|
-
description: '在代码仓库中创建文件, 参数repoName, path, content, encoding',
|
|
32
|
-
middleware: ['auth'],
|
|
33
|
-
metadata: {
|
|
34
|
-
tags: ['opencode']
|
|
35
|
-
}
|
|
36
|
-
}).define(async (ctx) => {
|
|
37
|
-
const repoName = ctx.query?.repoName;
|
|
38
|
-
const path = ctx.query?.path;
|
|
39
|
-
const content = ctx.query?.content;
|
|
40
|
-
const encoding = ctx.query?.encoding ?? 'raw';
|
|
41
|
-
|
|
42
|
-
if (!repoName || !path || !content) {
|
|
43
|
-
ctx.throw(400, '缺少参数 repoName, path 或 content');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const res = await cnb.repo.createCommit(repoName, {
|
|
47
|
-
message: `添加文件 ${path} 通过 API `,
|
|
48
|
-
files: [
|
|
49
|
-
{ path, content, encoding },
|
|
50
|
-
],
|
|
51
|
-
});
|
|
52
|
-
ctx.forward(res);
|
|
53
|
-
}).addTo(app);
|
|
1
|
+
import './list.ts'
|
|
2
|
+
import './repo.ts'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createSkill } from '@kevisual/router';
|
|
2
|
+
import { app, cnb } from '../../app.ts';
|
|
3
|
+
import { tool } from "@opencode-ai/plugin/tool"
|
|
4
|
+
|
|
5
|
+
// "列出我的代码仓库,search blog"
|
|
6
|
+
// 列出我的知识库的代码仓库
|
|
7
|
+
app.route({
|
|
8
|
+
path: 'cnb',
|
|
9
|
+
key: 'list-repos',
|
|
10
|
+
description: '列出我的代码仓库',
|
|
11
|
+
middleware: ['auth'],
|
|
12
|
+
metadata: {
|
|
13
|
+
tags: ['opencode'],
|
|
14
|
+
...createSkill({
|
|
15
|
+
skill: 'list-repos',
|
|
16
|
+
title: '列出cnb代码仓库',
|
|
17
|
+
summary: '列出cnb代码仓库, 可选flags参数,如 KnowledgeBase',
|
|
18
|
+
args: {
|
|
19
|
+
search: tool.schema.string().optional().describe('搜索关键词'),
|
|
20
|
+
pageSize: tool.schema.number().optional().describe('每页数量,默认999'),
|
|
21
|
+
flags: tool.schema.string().optional().describe('仓库标记,如果是知识库则填写 KnowledgeBase'),
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
}).define(async (ctx) => {
|
|
26
|
+
const search = ctx.query?.search;
|
|
27
|
+
const pageSize = ctx.query?.pageSize || 9999;
|
|
28
|
+
const flags = ctx.query?.flags;
|
|
29
|
+
const params: any = {};
|
|
30
|
+
if (flags) {
|
|
31
|
+
params.flags = flags;
|
|
32
|
+
}
|
|
33
|
+
const res = await cnb.repo.getRepoList({ search, page_size: pageSize, role: 'developer', ...params });
|
|
34
|
+
if (res.code === 200) {
|
|
35
|
+
const repos = res.data.map((item) => ({
|
|
36
|
+
name: item.name,
|
|
37
|
+
path: item.path,
|
|
38
|
+
description: item.description,
|
|
39
|
+
web_url: item.web_url,
|
|
40
|
+
}));
|
|
41
|
+
ctx.body = { content: JSON.stringify(repos), list: res.data };
|
|
42
|
+
} else {
|
|
43
|
+
ctx.throw(500, '获取仓库列表失败');
|
|
44
|
+
}
|
|
45
|
+
}).addTo(app);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { app, cnb } from '../../app.ts';
|
|
2
|
+
import { createSkill, Skill } from '@kevisual/router'
|
|
3
|
+
import { tool } from "@opencode-ai/plugin/tool"
|
|
4
|
+
|
|
5
|
+
// 创建一个仓库 kevisual/test-repo
|
|
6
|
+
app.route({
|
|
7
|
+
path: 'cnb',
|
|
8
|
+
key: 'create-repo',
|
|
9
|
+
description: '创建代码仓库, 参数name, visibility, description',
|
|
10
|
+
middleware: ['auth'],
|
|
11
|
+
metadata: {
|
|
12
|
+
tags: ['opencode'],
|
|
13
|
+
...createSkill({
|
|
14
|
+
skill: 'create-repo',
|
|
15
|
+
title: '创建代码仓库',
|
|
16
|
+
args: {
|
|
17
|
+
name: tool.schema.string().describe('代码仓库名称, 如 my-user/my-repo'),
|
|
18
|
+
visibility: tool.schema.string().describe('代码仓库可见性, public 或 private').default('public'),
|
|
19
|
+
description: tool.schema.string().describe('代码仓库描述'),
|
|
20
|
+
},
|
|
21
|
+
summary: '创建一个新的代码仓库',
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
}).define(async (ctx) => {
|
|
25
|
+
const name = ctx.query?.name;
|
|
26
|
+
const visibility = ctx.query?.visibility ?? 'public';
|
|
27
|
+
const description = ctx.query?.description ?? '';
|
|
28
|
+
|
|
29
|
+
if (!name) {
|
|
30
|
+
ctx.throw(400, '缺少参数 name');
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
|
|
34
|
+
const res = await cnb.repo.createRepo({
|
|
35
|
+
name,
|
|
36
|
+
visibility,
|
|
37
|
+
description,
|
|
38
|
+
});
|
|
39
|
+
ctx.forward(res);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
ctx.code = 200
|
|
42
|
+
ctx.body = { content: 'JS仓库可能已存在' }
|
|
43
|
+
}
|
|
44
|
+
}).addTo(app);
|
|
45
|
+
|
|
46
|
+
app.route({
|
|
47
|
+
path: 'cnb',
|
|
48
|
+
key: 'create-repo-file',
|
|
49
|
+
description: '在代码仓库中创建文件, repoName, filePath, content, encoding',
|
|
50
|
+
middleware: ['auth'],
|
|
51
|
+
metadata: {
|
|
52
|
+
tags: ['opencode'],
|
|
53
|
+
...createSkill({
|
|
54
|
+
skill: 'create-repo-file',
|
|
55
|
+
title: '在代码仓库中创建文件',
|
|
56
|
+
summary: `在代码仓库中创建文件, encoding 可选,默认 raw`,
|
|
57
|
+
args: {
|
|
58
|
+
repoName: tool.schema.string().describe('代码仓库名称, 如 my-user/my-repo'),
|
|
59
|
+
filePath: tool.schema.string().describe('文件路径, 如 src/index.ts'),
|
|
60
|
+
content: tool.schema.string().describe('文本的字符串的内容'),
|
|
61
|
+
encoding: tool.schema.string().describe('编码方式,如 raw').optional(),
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
}).define(async (ctx) => {
|
|
66
|
+
const repoName = ctx.query?.repoName;
|
|
67
|
+
const filePath = ctx.query?.filePath;
|
|
68
|
+
const content = ctx.query?.content;
|
|
69
|
+
const encoding = ctx.query?.encoding ?? 'raw';
|
|
70
|
+
|
|
71
|
+
if (!repoName || !filePath || !content) {
|
|
72
|
+
ctx.throw(400, '缺少参数 repoName, filePath 或 content');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const res = await cnb.repo.createCommit(repoName, {
|
|
76
|
+
message: `添加文件 ${filePath} 通过 API `,
|
|
77
|
+
files: [
|
|
78
|
+
{ path: filePath, content, encoding },
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
ctx.forward(res);
|
|
82
|
+
}).addTo(app);
|
|
83
|
+
|
|
84
|
+
// 删除一个仓库 kevisual/test-repo
|
|
85
|
+
app.route({
|
|
86
|
+
path: 'cnb',
|
|
87
|
+
key: 'delete-repo',
|
|
88
|
+
description: '删除代码仓库, 参数name',
|
|
89
|
+
middleware: ['auth'],
|
|
90
|
+
metadata: {
|
|
91
|
+
tags: ['opencode'],
|
|
92
|
+
...createSkill({
|
|
93
|
+
skill: 'delete-repo',
|
|
94
|
+
title: '删除代码仓库',
|
|
95
|
+
args: {
|
|
96
|
+
name: tool.schema.string().describe('代码仓库名称'),
|
|
97
|
+
},
|
|
98
|
+
summary: '删除一个代码仓库',
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
}).define(async (ctx) => {
|
|
102
|
+
const name = ctx.query?.name;
|
|
103
|
+
|
|
104
|
+
if (!name) {
|
|
105
|
+
ctx.throw(400, '缺少参数 name');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const res = await cnb.repo.deleteRepo(name);
|
|
109
|
+
ctx.forward(res);
|
|
110
|
+
}).addTo(app);
|
|
File without changes
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createSkill } from '@kevisual/router';
|
|
2
|
+
import { app, cnb } from '../../app.ts';
|
|
3
|
+
import { tool } from '@opencode-ai/plugin/tool';
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
app.route({
|
|
@@ -7,27 +9,36 @@ app.route({
|
|
|
7
9
|
description: '检查用户登录状态,参数checkToken,default true; checkCookie, default false',
|
|
8
10
|
middleware: ['auth'],
|
|
9
11
|
metadata: {
|
|
10
|
-
tags: ['opencode']
|
|
12
|
+
tags: ['opencode'],
|
|
13
|
+
...createSkill({
|
|
14
|
+
skill: 'cnb-login-verify',
|
|
15
|
+
title: 'CNB 登录验证信息',
|
|
16
|
+
summary: '验证 CNB 登录信息是否有效',
|
|
17
|
+
args: {
|
|
18
|
+
checkToken: tool.schema.boolean().describe('是否检查 Token 的有效性').default(true),
|
|
19
|
+
checkCookie: tool.schema.boolean().describe('是否检查 Cookie 的有效性').default(false),
|
|
20
|
+
},
|
|
21
|
+
})
|
|
11
22
|
}
|
|
12
23
|
}).define(async (ctx) => {
|
|
13
24
|
const checkToken = ctx.query?.checkToken ?? true;
|
|
14
25
|
const checkCookie = ctx.query?.checkCookie ?? false;
|
|
15
|
-
let
|
|
26
|
+
let content = '';
|
|
16
27
|
if (checkToken) {
|
|
17
28
|
const res = await cnb.user.getUser();
|
|
18
29
|
if (res?.code !== 200) {
|
|
19
|
-
|
|
30
|
+
content += `Token 无效,请检查 CNB_TOKEN 配置。\n`;
|
|
20
31
|
} else {
|
|
21
|
-
|
|
32
|
+
content += `Token 有效,Token用户昵称:${res.data?.nickname}\n`;
|
|
22
33
|
}
|
|
23
34
|
}
|
|
24
35
|
if (checkCookie) {
|
|
25
36
|
const res = await cnb.user.getCurrentUser();
|
|
26
37
|
if (res?.code !== 200) {
|
|
27
|
-
|
|
38
|
+
content += `Cookie 无效,请检查 CNB_COOKIE 配置。\n`;
|
|
28
39
|
} else {
|
|
29
|
-
|
|
40
|
+
content += `Cookie 有效,当前Cookie用户:${res.data?.nickname}\n`;
|
|
30
41
|
}
|
|
31
42
|
}
|
|
32
|
-
ctx.body = {
|
|
43
|
+
ctx.body = { content };
|
|
33
44
|
}).addTo(app);
|