@mayijuntuan/cnb-chat 1.1.0 → 1.2.0

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.
Files changed (2) hide show
  1. package/lib/theme-agent.mjs +94 -29
  2. package/package.json +1 -1
@@ -1,10 +1,13 @@
1
1
  // CodeBuddy 代理开发:编辑主题代码 + 流式回传 + git push
2
2
  // 在 CNB 流水线容器内运行。工作目录为 CNB 检出的仓库代码。
3
+ // 保活会话:处理完当前消息后轮询 pending 接口等待后续消息,
4
+ // 空闲超过 KEEPALIVE_IDLE 秒或总存活超过 KEEPALIVE_MAX 秒自动退出。
3
5
  // 详见 README.md 的环境变量表
4
6
 
5
7
  import { execFileSync } from 'node:child_process';
6
8
 
7
9
  const CALLBACK = process.env.CHAT_CALLBACK || '';
10
+ const PENDING_URL = process.env.CHAT_PENDING_URL || '';
8
11
  const CHAT_ID = process.env.CHAT_ID || '';
9
12
  const SECRET = process.env.CHAT_SECRET || '';
10
13
  const MODEL = process.env.CHAT_MODEL || '';
@@ -12,50 +15,51 @@ const MESSAGES = process.env.CHAT_MESSAGES || '[]';
12
15
  const CB_API_KEY = process.env.CHAT_CB_API_KEY || '';
13
16
  const BRANCH = process.env.CHAT_BRANCH || 'main';
14
17
 
18
+ const POLL_INTERVAL = 3 * 1000; // 轮询间隔
19
+ const KEEPALIVE_IDLE = 180 * 1000; // 空闲超时后退出
20
+ const KEEPALIVE_MAX = 1800 * 1000; // 总存活上限(防失控计费)
21
+
22
+ const sh = (cmd, args) => execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
23
+ const text = (e) => String((e && e.message) || e).slice(0, 300);
24
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
25
+
15
26
  const post = (payload) => fetch(CALLBACK, {
16
27
  method: 'POST',
17
28
  headers: { 'Content-Type': 'application/json', 'X-Chat-Secret': SECRET },
18
29
  body: JSON.stringify({ chat_id: CHAT_ID, ...payload })
19
30
  }).catch(() => {});
20
31
 
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);
32
+ // 轮询服务器取待处理消息(含心跳);无待处理返回 null
33
+ const pollPending = async () => {
34
+ if (!PENDING_URL) return null;
35
+ try {
36
+ const res = await fetch(PENDING_URL + '?chat_id=' + encodeURIComponent(CHAT_ID), {
37
+ headers: { 'X-Chat-Secret': SECRET },
38
+ });
39
+ if (!res.ok) return null;
40
+ const data = await res.json();
41
+ if (data?.has_pending && Array.isArray(data.messages) && data.messages.length) {
42
+ return data.messages;
43
+ }
44
+ } catch (e) { /* 网络抖动忽略,下轮再试 */ }
45
+ return null;
46
+ };
23
47
 
24
- (async () => {
25
- let messages = [];
26
- try { messages = JSON.parse(MESSAGES); } catch (e) { messages = []; }
48
+ // 执行一轮 CodeBuddy 任务:messages 为完整 prompt(system + 历史)
49
+ const runOnce = async (messages, query, cliPath) => {
27
50
  const turns = messages.filter((m) => m.role === 'user' || m.role === 'assistant');
28
51
  const lastUser = [...turns].reverse().find((m) => m.role === 'user');
29
- if (!CALLBACK || !lastUser) {
30
- await post({ type: 'error', content: '流水线参数缺失' });
31
- process.exit(1);
52
+ if (!lastUser) {
53
+ await post({ type: 'error', content: '消息序列无用户内容' });
54
+ return false;
32
55
  }
33
56
 
34
57
  // 多轮上下文 + 最新需求
35
58
  const prompt = turns.map((m) => (m.role === 'user' ? '【用户】' : '【AI】') + m.content).join('\n\n')
36
59
  + '\n\n请以上面的对话为上下文(以最后的用户消息为准),直接在当前仓库中修改代码并保存。';
37
60
 
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
61
  const full = [];
53
62
 
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
63
  try {
60
64
  const q = query({
61
65
  prompt,
@@ -92,7 +96,13 @@ const text = (e) => String((e && e.message) || e).slice(0, 300);
92
96
  sh('git', ['config', 'user.name', 'CodeBuddy']);
93
97
  sh('git', ['add', '-A']);
94
98
  try { sh('git', ['commit', '-m', 'AI: ' + String(lastUser.content).slice(0, 80)]); } catch (e) { /* 无变更 */ }
95
- sh('git', ['push', 'https://' + process.env.CNB_TOKEN + '@cnb.cool/' + process.env.CNB_REPO_SLUG + '.git', 'HEAD:' + BRANCH]);
99
+ try {
100
+ // 优先用容器内已配置凭据的 origin(Prepare 阶段克隆时注入)
101
+ sh('git', ['push', 'origin', 'HEAD:' + BRANCH]);
102
+ } catch (e) {
103
+ // 兜底:显式 token URL
104
+ sh('git', ['push', 'https://' + process.env.CNB_TOKEN + '@cnb.cool/' + process.env.CNB_REPO_SLUG + '.git', 'HEAD:' + BRANCH]);
105
+ }
96
106
  await post({ type: 'progress', content: '✅ 代码已推送到 ' + BRANCH + ' 分支' });
97
107
  } catch (e) {
98
108
  await post({ type: 'progress', content: '⚠️ git push 失败: ' + text(e) });
@@ -105,7 +115,7 @@ const text = (e) => String((e && e.message) || e).slice(0, 300);
105
115
  }
106
116
 
107
117
  await post({ type: 'done', content: full.join('\n') });
108
- process.exit(0);
118
+ return true;
109
119
  } catch (e) {
110
120
  const msg = text(e);
111
121
  // SDK preview 版在 CLI 立即退出(认证失败/CLI 缺失)时仅抛 Transport closed
@@ -113,6 +123,61 @@ const text = (e) => String((e && e.message) || e).slice(0, 300);
113
123
  ? 'CodeBuddy CLI 启动失败,请检查 CODEBUDDY_API_KEY 是否有效、CLI 是否已安装'
114
124
  : msg;
115
125
  await post({ type: 'error', content: hint });
126
+ return false;
127
+ }
128
+ };
129
+
130
+ (async () => {
131
+ let initial = [];
132
+ try { initial = JSON.parse(MESSAGES); } catch (e) { initial = []; }
133
+ const hasInitial = initial.some((m) => m.role === 'user');
134
+ if (!CALLBACK || (!hasInitial && !PENDING_URL)) {
135
+ await post({ type: 'error', content: '流水线参数缺失' });
116
136
  process.exit(1);
117
137
  }
138
+
139
+ // CodeBuddy 认证(SDK 要求;中国版需 internal 环境标识)
140
+ if (CB_API_KEY) {
141
+ process.env.CODEBUDDY_API_KEY = CB_API_KEY;
142
+ process.env.CODEBUDDY_INTERNET_ENVIRONMENT = process.env.CODEBUDDY_INTERNET_ENVIRONMENT || 'internal';
143
+ }
144
+
145
+ let query;
146
+ try {
147
+ ({ query } = await import('@tencent-ai/agent-sdk'));
148
+ } catch (e) {
149
+ await post({ type: 'error', content: 'agent-sdk 加载失败: ' + text(e) });
150
+ process.exit(1);
151
+ }
152
+
153
+ // 优先使用环境预装的 codebuddy CLI(如 cnbcool/default-dev-env,已由 CNB 注入认证),
154
+ // 未预装时 SDK 自动回退到包内捆绑的 CLI
155
+ let cliPath = '';
156
+ try { cliPath = sh('which', ['codebuddy']).trim(); } catch (e) { /* 未预装 */ }
157
+
158
+ // 第一轮:构建触发时随 env 传入的消息
159
+ if (hasInitial) {
160
+ const ok = await runOnce(initial, query, cliPath);
161
+ if (!ok) process.exit(1);
162
+ }
163
+
164
+ // 保活循环:等待后续消息;空闲超时 / 总时长超限则退出
165
+ if (PENDING_URL) {
166
+ const startedAt = Date.now();
167
+ let idleStart = Date.now();
168
+ while (Date.now() - startedAt < KEEPALIVE_MAX) {
169
+ if (Date.now() - idleStart > KEEPALIVE_IDLE) {
170
+ break; // 空闲超时,正常退出(下次消息会触发新构建)
171
+ }
172
+ const messages = await pollPending();
173
+ if (messages) {
174
+ idleStart = Date.now();
175
+ await runOnce(messages, query, cliPath);
176
+ continue; // 处理完立刻再看有没有新消息
177
+ }
178
+ await sleep(POLL_INTERVAL);
179
+ }
180
+ }
181
+
182
+ process.exit(0);
118
183
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mayijuntuan/cnb-chat",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "CNB pipeline for AI chat streaming relay and CodeBuddy code editing",
5
5
  "type": "module",
6
6
  "bin": {