@mayijuntuan/cnb-chat 0.0.0-stage → 1.1.1

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/README.md CHANGED
@@ -1,3 +1,62 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # cnb-chat
2
+
3
+ CNB 流水线工具集(npx 包)。
4
+
5
+ ## 命令
6
+
7
+ ### `chat` — AI 对话流式代理(纯对话)
8
+
9
+ 在 CNB 流水线容器内运行(AI 接口仅允许流水线 `CNB_TOKEN` 调用,**本地不可用**)。
10
+ 读取环境变量(由平台 `build/start` 的 `env` 注入),
11
+ 调用 CNB AI 对话接口(`/{repo}/-/ai/chat/completions`,stream),
12
+ 将模型流式输出逐 delta POST 回平台 relay 接口,结束时发送 `done`。
13
+
14
+ | 环境变量 | 说明 |
15
+ |---|---|
16
+ | `CHAT_CALLBACK` | 平台 relay 回调地址 |
17
+ | `CHAT_SECRET` | 回调鉴权密钥(`X-Chat-Secret` 头) |
18
+ | `CHAT_ID` | 会话 ID(随每个回调携带) |
19
+ | `CHAT_MODEL` | 模型名,可空 |
20
+ | `CHAT_MESSAGES` | JSON 数组 `[{role, content}, ...]` |
21
+ | `CNB_REPO_SLUG` / `CNB_TOKEN` | CNB 流水线自动注入 |
22
+
23
+ ### `theme` — CodeBuddy 代理开发(可编辑代码)
24
+
25
+ 调用 `@tencent-ai/agent-sdk` 驱动 CodeBuddy 在仓库工作目录中编辑代码,
26
+ 文本块逐段回调 `delta`、工具调用回调 `progress`,完成后自动 `git commit + push`。
27
+
28
+ | 环境变量 | 说明 |
29
+ |---|---|
30
+ | `CHAT_MESSAGES` | JSON 数组(取用户/助手轮次组装上下文,以最后一条用户消息为准) |
31
+ | `CHAT_CB_API_KEY` | CodeBuddy API Key(copilot.tencent.com 创建) |
32
+ | `CHAT_MODEL` | CodeBuddy 模型名,可空 |
33
+ | `CHAT_CALLBACK` / `CHAT_SECRET` / `CHAT_ID` | 平台 relay 回调(缺失时进入本地模式) |
34
+ | `CHAT_BRANCH` | push 目标分支,默认 main |
35
+ | `CHAT_NOTIFY_URL` | push 后的主题更新通知地址,可空 |
36
+ | `CNB_REPO_SLUG` / `CNB_TOKEN` | CNB 流水线自动注入(git push 凭据) |
37
+
38
+ ## 流水线接入
39
+
40
+ 平台触发构建时通过 `build/start` 的 `config` 参数内联传入流水线配置
41
+ (`CnbPipeline::chatConfig($mode)` 生成,仓库无需 `.cnb.yml`):
42
+
43
+ ```yaml
44
+ $:
45
+ api_trigger_chat:
46
+ - name: AI 对话
47
+ docker:
48
+ image: node:20
49
+ stages:
50
+ - name: chat-relay
51
+ script: |
52
+ npm i -g @tencent-ai/codebuddy-code --no-audit --no-fund
53
+ npx -y @mayijuntuan/cnb-chat@latest theme
54
+ ```
55
+
56
+ ## 发布
57
+
58
+ ```bash
59
+ npm publish
60
+ ```
61
+
62
+ 发布后流水线中的 `npx -y @mayijuntuan/cnb-chat@latest` 即可拉取最新版本。
package/bin/cli.mjs ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ // cnb-chat CLI:CNB 流水线工具集
3
+ // 用法: npx @mayijuntuan/cnb-chat <command>
4
+ // chat AI 对话流式代理(纯对话,不读写文件,调用 CNB AI 接口并逐 delta 回调)
5
+ // theme CodeBuddy 代理开发(调用 @tencent-ai/agent-sdk 编辑主题代码并 push)
6
+
7
+ const [cmd] = process.argv.slice(2);
8
+
9
+ switch (cmd) {
10
+ case 'chat':
11
+ await import('../lib/chat-relay.mjs');
12
+ break;
13
+ case 'theme':
14
+ await import('../lib/theme-agent.mjs');
15
+ break;
16
+ default:
17
+ console.error('Usage: cnb-chat <chat|theme>');
18
+ process.exit(1);
19
+ }
@@ -0,0 +1,83 @@
1
+ // AI 对话流式代理
2
+ // 在 CNB 流水线容器内运行(依赖容器注入的 CNB_TOKEN)
3
+ // 环境变量:
4
+ // CHAT_CALLBACK 回调地址(平台 relay 接口)
5
+ // CHAT_SECRET 回调鉴权密钥(放入 X-Chat-Secret 头)
6
+ // CHAT_ID 会话 ID(随每个回调携带)
7
+ // CHAT_MODEL 模型名(可空,使用默认模型)
8
+ // CHAT_MESSAGES JSON 数组 [{role, content}, ...]
9
+ // CNB_REPO_SLUG CNB 自动注入:仓库路径
10
+ // CNB_TOKEN CNB 自动注入:流水线临时令牌
11
+
12
+ const CALLBACK = process.env.CHAT_CALLBACK || '';
13
+ const CHAT_ID = process.env.CHAT_ID || '';
14
+ const SECRET = process.env.CHAT_SECRET || '';
15
+ const MODEL = process.env.CHAT_MODEL || '';
16
+ const MESSAGES = process.env.CHAT_MESSAGES || '[]';
17
+ const API = 'https://api.cnb.cool/' + process.env.CNB_REPO_SLUG + '/-/ai/chat/completions';
18
+
19
+ const post = (payload) => fetch(CALLBACK, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/json', 'Chat-Id': CHAT_ID,'X-Chat-Secret': SECRET },
22
+ body: JSON.stringify(payload)
23
+ }).catch(() => {});
24
+
25
+ (async () => {
26
+ let messages = [];
27
+ try { messages = JSON.parse(MESSAGES); } catch (e) { messages = []; }
28
+ if (!CALLBACK || !messages.length) {
29
+ await post({ type: 'error', content: '流水线参数缺失' });
30
+ process.exit(1);
31
+ }
32
+ const body = { stream: true, messages };
33
+ if (MODEL) body.model = MODEL;
34
+
35
+ try {
36
+ const res = await fetch(API, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Authorization': 'Bearer ' + process.env.CNB_TOKEN,
40
+ 'Content-Type': 'application/json',
41
+ 'Accept': 'text/event-stream'
42
+ },
43
+ body: JSON.stringify(body)
44
+ });
45
+ if (!res.ok) {
46
+ const t = await res.text();
47
+ await post({ type: 'error', content: 'AI 接口错误 ' + res.status + ': ' + t.slice(0, 500) });
48
+ process.exit(1);
49
+ }
50
+ const reader = res.body.getReader();
51
+ const decoder = new TextDecoder();
52
+ let buf = '';
53
+ let full = '';
54
+ while (true) {
55
+ const { done, value } = await reader.read();
56
+ if (done) break;
57
+ buf += decoder.decode(value, { stream: true });
58
+ let idx;
59
+ while ((idx = buf.indexOf('\n')) >= 0) {
60
+ const line = buf.slice(0, idx).trim();
61
+ buf = buf.slice(idx + 1);
62
+ if (!line.startsWith('data:')) continue;
63
+ const data = line.slice(5).trim();
64
+ if (data === '[DONE]') {
65
+ await post({ type: 'done', content: full });
66
+ process.exit(0);
67
+ }
68
+ try {
69
+ const j = JSON.parse(data);
70
+ const delta = (j.choices && j.choices[0] && j.choices[0].delta && j.choices[0].delta.content) || '';
71
+ if (delta) {
72
+ full += delta;
73
+ await post({ type: 'delta', content: delta });
74
+ }
75
+ } catch (e) {}
76
+ }
77
+ }
78
+ await post({ type: 'done', content: full });
79
+ } catch (e) {
80
+ await post({ type: 'error', content: String((e && e.message) || e) });
81
+ process.exit(1);
82
+ }
83
+ })();
@@ -0,0 +1,124 @@
1
+ // CodeBuddy 代理开发:编辑主题代码 + 流式回传 + git push
2
+ // 在 CNB 流水线容器内运行。工作目录为 CNB 检出的仓库代码。
3
+ // 详见 README.md 的环境变量表
4
+
5
+ import { execFileSync } from 'node:child_process';
6
+
7
+ const CALLBACK = process.env.CHAT_CALLBACK || '';
8
+ const CHAT_ID = process.env.CHAT_ID || '';
9
+ const SECRET = process.env.CHAT_SECRET || '';
10
+ const MODEL = process.env.CHAT_MODEL || '';
11
+ const MESSAGES = process.env.CHAT_MESSAGES || '[]';
12
+ const CB_API_KEY = process.env.CHAT_CB_API_KEY || '';
13
+ const BRANCH = process.env.CHAT_BRANCH || 'main';
14
+
15
+ const post = (payload) => fetch(CALLBACK, {
16
+ method: 'POST',
17
+ headers: { 'Content-Type': 'application/json', 'X-Chat-Secret': SECRET },
18
+ body: JSON.stringify({ chat_id: CHAT_ID, ...payload })
19
+ }).catch(() => {});
20
+
21
+ const sh = (cmd, args) => execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
22
+ const text = (e) => String((e && e.message) || e).slice(0, 300);
23
+
24
+ (async () => {
25
+ let messages = [];
26
+ try { messages = JSON.parse(MESSAGES); } catch (e) { messages = []; }
27
+ const turns = messages.filter((m) => m.role === 'user' || m.role === 'assistant');
28
+ const lastUser = [...turns].reverse().find((m) => m.role === 'user');
29
+ if (!CALLBACK || !lastUser) {
30
+ await post({ type: 'error', content: '流水线参数缺失' });
31
+ process.exit(1);
32
+ }
33
+
34
+ // 多轮上下文 + 最新需求
35
+ const prompt = turns.map((m) => (m.role === 'user' ? '【用户】' : '【AI】') + m.content).join('\n\n')
36
+ + '\n\n请以上面的对话为上下文(以最后的用户消息为准),直接在当前仓库中修改代码并保存。';
37
+
38
+ // CodeBuddy 认证(SDK 要求;中国版需 internal 环境标识)
39
+ if (CB_API_KEY) {
40
+ process.env.CODEBUDDY_API_KEY = CB_API_KEY;
41
+ process.env.CODEBUDDY_INTERNET_ENVIRONMENT = process.env.CODEBUDDY_INTERNET_ENVIRONMENT || 'internal';
42
+ }
43
+
44
+ let query;
45
+ try {
46
+ ({ query } = await import('@tencent-ai/agent-sdk'));
47
+ } catch (e) {
48
+ await post({ type: 'error', content: 'agent-sdk 加载失败: ' + text(e) });
49
+ process.exit(1);
50
+ }
51
+
52
+ const full = [];
53
+
54
+ // 优先使用环境预装的 codebuddy CLI(如 cnbcool/default-dev-env,已由 CNB 注入认证),
55
+ // 未预装时 SDK 自动回退到包内捆绑的 CLI
56
+ let cliPath = '';
57
+ try { cliPath = sh('which', ['codebuddy']).trim(); } catch (e) { /* 未预装 */ }
58
+
59
+ try {
60
+ const q = query({
61
+ prompt,
62
+ options: {
63
+ cwd: process.cwd(),
64
+ permissionMode: 'acceptEdits',
65
+ allowedTools: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash'],
66
+ ...(MODEL ? { model: MODEL } : {}),
67
+ ...(cliPath ? { pathToCodebuddyCode: cliPath } : {}),
68
+ maxTurns: 30,
69
+ },
70
+ });
71
+
72
+ for await (const message of q) {
73
+ if (message.type === 'assistant' && Array.isArray(message.message?.content)) {
74
+ for (const block of message.message.content) {
75
+ if (block.type === 'text' && block.text) {
76
+ full.push(block.text);
77
+ await post({ type: 'delta', content: block.text });
78
+ } else if (block.type === 'tool_use') {
79
+ const name = block.name || 'tool';
80
+ const file = block.input?.file_path || block.input?.path || '';
81
+ await post({ type: 'progress', content: file ? `🔧 ${name} ${file}` : `🔧 ${name}` });
82
+ }
83
+ }
84
+ } else if (message.type === 'result' && message.subtype !== 'success') {
85
+ throw new Error('CodeBuddy 执行失败: ' + (message.subtype || 'unknown'));
86
+ }
87
+ }
88
+
89
+ // 提交并推送到仓库
90
+ try {
91
+ sh('git', ['config', 'user.email', 'codebuddy@cnb.build']);
92
+ sh('git', ['config', 'user.name', 'CodeBuddy']);
93
+ sh('git', ['add', '-A']);
94
+ try { sh('git', ['commit', '-m', 'AI: ' + String(lastUser.content).slice(0, 80)]); } catch (e) { /* 无变更 */ }
95
+ try {
96
+ // 优先用容器内已配置凭据的 origin(Prepare 阶段克隆时注入)
97
+ sh('git', ['push', 'origin', 'HEAD:' + BRANCH]);
98
+ } catch (e) {
99
+ // 兜底:显式 token URL
100
+ sh('git', ['push', 'https://' + process.env.CNB_TOKEN + '@cnb.cool/' + process.env.CNB_REPO_SLUG + '.git', 'HEAD:' + BRANCH]);
101
+ }
102
+ await post({ type: 'progress', content: '✅ 代码已推送到 ' + BRANCH + ' 分支' });
103
+ } catch (e) {
104
+ await post({ type: 'progress', content: '⚠️ git push 失败: ' + text(e) });
105
+ }
106
+
107
+ // 通知平台刷新主题(沿用现有回调)
108
+ const notify = process.env.CHAT_NOTIFY_URL || '';
109
+ if (notify) {
110
+ await fetch(notify, { method: 'POST' }).catch(() => {});
111
+ }
112
+
113
+ await post({ type: 'done', content: full.join('\n') });
114
+ process.exit(0);
115
+ } catch (e) {
116
+ const msg = text(e);
117
+ // SDK preview 版在 CLI 立即退出(认证失败/CLI 缺失)时仅抛 Transport closed
118
+ const hint = /Transport closed/i.test(msg)
119
+ ? 'CodeBuddy CLI 启动失败,请检查 CODEBUDDY_API_KEY 是否有效、CLI 是否已安装'
120
+ : msg;
121
+ await post({ type: 'error', content: hint });
122
+ process.exit(1);
123
+ }
124
+ })();
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
- {
2
- "name": "@mayijuntuan/cnb-chat",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@mayijuntuan/cnb-chat",
3
+ "version": "1.1.1",
4
+ "description": "CNB pipeline for AI chat streaming relay and CodeBuddy code editing",
5
+ "type": "module",
6
+ "bin": {
7
+ "cnb-chat": "bin/cli.mjs"
8
+ },
9
+ "dependencies": {
10
+ "@tencent-ai/agent-sdk": "^0.1.0"
11
+ },
12
+ "engines": {
13
+ "node": ">=18.20"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "lib"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "registry": "https://registry.npmjs.org/"
22
+ },
23
+ "keywords": [
24
+ "cnb",
25
+ "chat",
26
+ "streaming",
27
+ "codebuddy"
28
+ ]
29
+ }