@mayijuntuan/cnb-chat 1.1.1 → 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.
- package/lib/theme-agent.mjs +87 -28
- package/package.json +1 -1
package/lib/theme-agent.mjs
CHANGED
|
@@ -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
|
-
|
|
22
|
-
const
|
|
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
|
-
|
|
25
|
-
|
|
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 (!
|
|
30
|
-
await post({ type: 'error', content: '
|
|
31
|
-
|
|
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,
|
|
@@ -111,7 +115,7 @@ const text = (e) => String((e && e.message) || e).slice(0, 300);
|
|
|
111
115
|
}
|
|
112
116
|
|
|
113
117
|
await post({ type: 'done', content: full.join('\n') });
|
|
114
|
-
|
|
118
|
+
return true;
|
|
115
119
|
} catch (e) {
|
|
116
120
|
const msg = text(e);
|
|
117
121
|
// SDK preview 版在 CLI 立即退出(认证失败/CLI 缺失)时仅抛 Transport closed
|
|
@@ -119,6 +123,61 @@ const text = (e) => String((e && e.message) || e).slice(0, 300);
|
|
|
119
123
|
? 'CodeBuddy CLI 启动失败,请检查 CODEBUDDY_API_KEY 是否有效、CLI 是否已安装'
|
|
120
124
|
: msg;
|
|
121
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: '流水线参数缺失' });
|
|
122
136
|
process.exit(1);
|
|
123
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);
|
|
124
183
|
})();
|