chatccc 0.2.156 → 0.2.158
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/package.json +1 -1
- package/src/agent-stop-stuck.ts +3 -3
- package/src/session.ts +61 -55
- package/src/web-ui.ts +54 -41
package/package.json
CHANGED
package/src/agent-stop-stuck.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
2
|
|
|
3
3
|
import { readUtf8JsonBody } from "./agent-rpc-body.ts";
|
|
4
|
-
import { activePrompts, getChatsForSession
|
|
4
|
+
import { activePrompts, getChatsForSession } from "./session-chat-binding.ts";
|
|
5
5
|
import { getPlatformForChat } from "./session.ts";
|
|
6
6
|
import { readStreamState, writeStreamState } from "./stream-state.ts";
|
|
7
7
|
import { ts } from "./config.ts";
|
|
@@ -78,8 +78,8 @@ export async function handleAgentStopStuckRequest(
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
//
|
|
82
|
-
|
|
81
|
+
// 不丢弃缓存队列中的消息,让 runAgentSession 的 finally 块正常消费,
|
|
82
|
+
// 确保 stop-stuck-loop 结束后排队的消息仍能被正常处理。
|
|
83
83
|
|
|
84
84
|
const finalReply = typeof body?.final_reply === "string" ? body.final_reply.trim() : "";
|
|
85
85
|
|
package/src/session.ts
CHANGED
|
@@ -167,15 +167,15 @@ function formatTerminalHeader(status: "running" | "done" | "stopped" | "error"):
|
|
|
167
167
|
return { title: "完成" };
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
function turnFinalStatus(status: "running" | "done" | "stopped" | "error"): "done" | "stopped" {
|
|
171
|
-
return status === "stopped" || status === "error" ? "stopped" : "done";
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function isCardKitSequenceConflict(err: unknown): boolean {
|
|
175
|
-
return err instanceof Error && err.message.includes("300317");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function startPromptProcessMonitor(sessionId: string, info: ToolProcessInfo): void {
|
|
170
|
+
function turnFinalStatus(status: "running" | "done" | "stopped" | "error"): "done" | "stopped" {
|
|
171
|
+
return status === "stopped" || status === "error" ? "stopped" : "done";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isCardKitSequenceConflict(err: unknown): boolean {
|
|
175
|
+
return err instanceof Error && err.message.includes("300317");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function startPromptProcessMonitor(sessionId: string, info: ToolProcessInfo): void {
|
|
179
179
|
const prompt = activePrompts.get(sessionId);
|
|
180
180
|
if (!prompt) return;
|
|
181
181
|
prompt.processPid = info.pid;
|
|
@@ -1161,11 +1161,11 @@ export async function runAgentSession(
|
|
|
1161
1161
|
// 标记 prompt 结束
|
|
1162
1162
|
resourceMonitor.off("stuck", onResourceStuck);
|
|
1163
1163
|
const prompt = activePrompts.get(sessionId);
|
|
1164
|
-
const wasStopped = prompt?.stopped ?? false;
|
|
1165
|
-
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1166
|
-
const wasResourceStuck = prompt?.resourceStuck ?? false;
|
|
1167
|
-
clearPromptProcessMonitor(sessionId);
|
|
1168
|
-
activePrompts.delete(sessionId);
|
|
1164
|
+
const wasStopped = prompt?.stopped ?? false;
|
|
1165
|
+
const wasAbnormalExit = prompt?.abnormalExit ?? false;
|
|
1166
|
+
const wasResourceStuck = prompt?.resourceStuck ?? false;
|
|
1167
|
+
clearPromptProcessMonitor(sessionId);
|
|
1168
|
+
activePrompts.delete(sessionId);
|
|
1169
1169
|
|
|
1170
1170
|
// 先写最终状态(done/stopped),确保 display loop 在下一轮消费前
|
|
1171
1171
|
// 读到新状态并终结旧卡片。否则 setImmediate 在 CHECK 阶段先于
|
|
@@ -1175,14 +1175,19 @@ export async function runAgentSession(
|
|
|
1175
1175
|
const finalReply = pickFinalReply(state).trim();
|
|
1176
1176
|
|
|
1177
1177
|
// stop-stuck-loop 接口可能在 fire-and-forget 中已写入带 final_reply 的
|
|
1178
|
-
// stream state,finally
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1178
|
+
// stream state,finally 不应覆盖它。同时保留 stuckAt 标记,防止
|
|
1179
|
+
// stop-stuck-loop 结束后 session 被错误恢复。
|
|
1180
|
+
let finalReplyToWrite = finalReply;
|
|
1181
|
+
let preserveStuckAt: number | undefined;
|
|
1182
|
+
try {
|
|
1183
|
+
const existing = await readStreamState(sessionId);
|
|
1184
|
+
if (existing) {
|
|
1185
|
+
if (existing.finalReply.length > finalReply.length) {
|
|
1186
|
+
finalReplyToWrite = existing.finalReply;
|
|
1187
|
+
}
|
|
1188
|
+
preserveStuckAt = existing.stuckAt;
|
|
1189
|
+
}
|
|
1190
|
+
} catch {}
|
|
1186
1191
|
|
|
1187
1192
|
await writeStreamState({
|
|
1188
1193
|
sessionId,
|
|
@@ -1191,11 +1196,12 @@ export async function runAgentSession(
|
|
|
1191
1196
|
finalReply: finalReplyToWrite,
|
|
1192
1197
|
chunkCount: state.chunkCount,
|
|
1193
1198
|
turnCount: nextTurnCount,
|
|
1194
|
-
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
1195
|
-
updatedAt: Date.now(),
|
|
1196
|
-
cwd,
|
|
1197
|
-
tool,
|
|
1198
|
-
|
|
1199
|
+
contextTokens: existingInfo?.lastContextTokens ?? 0,
|
|
1200
|
+
updatedAt: Date.now(),
|
|
1201
|
+
cwd,
|
|
1202
|
+
tool,
|
|
1203
|
+
...(preserveStuckAt ? { stuckAt: preserveStuckAt } : {}),
|
|
1204
|
+
});
|
|
1199
1205
|
|
|
1200
1206
|
// 消费队列中的缓存消息(异步,不阻塞后续清理)
|
|
1201
1207
|
// 用户 /stop 后应丢弃队列消息,避免用户停止后又自动开始新轮
|
|
@@ -1360,42 +1366,42 @@ export function startUnifiedDisplayLoop(): void {
|
|
|
1360
1366
|
displayCards.delete(chatId);
|
|
1361
1367
|
} else {
|
|
1362
1368
|
// 发送最终结果(卡片平台)
|
|
1363
|
-
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
1364
|
-
const promptStillActive = activePrompts.has(sessionId);
|
|
1365
|
-
if (
|
|
1366
|
-
promptStillActive &&
|
|
1367
|
-
display.lastSentAccLen === state.accumulatedContent.length &&
|
|
1368
|
-
display.lastSentFinalReply === state.finalReply
|
|
1369
|
-
) {
|
|
1370
|
-
continue;
|
|
1371
|
-
}
|
|
1372
|
-
const nextSeq = display.sequence + 1;
|
|
1369
|
+
while (display.cardBusy) await new Promise(r => setTimeout(r, 20));
|
|
1370
|
+
const promptStillActive = activePrompts.has(sessionId);
|
|
1371
|
+
if (
|
|
1372
|
+
promptStillActive &&
|
|
1373
|
+
display.lastSentAccLen === state.accumulatedContent.length &&
|
|
1374
|
+
display.lastSentFinalReply === state.finalReply
|
|
1375
|
+
) {
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
const nextSeq = display.sequence + 1;
|
|
1373
1379
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status);
|
|
1374
1380
|
const cardContent = truncateContent(state.accumulatedContent + state.finalReply) || " ";
|
|
1375
1381
|
const doneCard = buildProgressCard(cardContent, { showStop: false, headerTitle, headerTemplate });
|
|
1376
|
-
let terminalCardUpdateAccepted = false;
|
|
1377
|
-
await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
|
|
1378
|
-
display.sequence = nextSeq;
|
|
1379
|
-
terminalCardUpdateAccepted = true;
|
|
1380
|
-
}).catch(err => {
|
|
1381
|
-
console.error(`[${ts()}] [DISPLAY] terminal cardUpdate failed: ${(err as Error).message}`);
|
|
1382
|
-
if (isCardKitSequenceConflict(err)) {
|
|
1383
|
-
display.sequence = nextSeq;
|
|
1384
|
-
terminalCardUpdateAccepted = true;
|
|
1385
|
-
}
|
|
1386
|
-
});
|
|
1382
|
+
let terminalCardUpdateAccepted = false;
|
|
1383
|
+
await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
|
|
1384
|
+
display.sequence = nextSeq;
|
|
1385
|
+
terminalCardUpdateAccepted = true;
|
|
1386
|
+
}).catch(err => {
|
|
1387
|
+
console.error(`[${ts()}] [DISPLAY] terminal cardUpdate failed: ${(err as Error).message}`);
|
|
1388
|
+
if (isCardKitSequenceConflict(err)) {
|
|
1389
|
+
display.sequence = nextSeq;
|
|
1390
|
+
terminalCardUpdateAccepted = true;
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1387
1393
|
|
|
1388
1394
|
// 若 session 仍在 activePrompts 中,说明 runAgentSession 的 finally
|
|
1389
1395
|
// 还没执行,当前 stream state 可能是 stopSession fire-and-forget
|
|
1390
1396
|
// 写入的,finalReply 滞后于内存态。卡片已更新为终态外观,但不发送
|
|
1391
1397
|
// 文本、不删除 display 条目,留给 finally 落盘后的下一次 tick 处理。
|
|
1392
|
-
if (promptStillActive) {
|
|
1393
|
-
if (terminalCardUpdateAccepted) {
|
|
1394
|
-
display.lastSentAccLen = state.accumulatedContent.length;
|
|
1395
|
-
display.lastSentFinalReply = state.finalReply;
|
|
1396
|
-
}
|
|
1397
|
-
continue;
|
|
1398
|
-
}
|
|
1398
|
+
if (promptStillActive) {
|
|
1399
|
+
if (terminalCardUpdateAccepted) {
|
|
1400
|
+
display.lastSentAccLen = state.accumulatedContent.length;
|
|
1401
|
+
display.lastSentFinalReply = state.finalReply;
|
|
1402
|
+
}
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1399
1405
|
|
|
1400
1406
|
const finalSt = turnFinalStatus(state.status);
|
|
1401
1407
|
finalizeTurnCards(sessionId, state.turnCount, finalSt).catch(() => {});
|
package/src/web-ui.ts
CHANGED
|
@@ -158,6 +158,30 @@ function spawnService(): { ok: boolean; pid?: number; error?: string } {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* 安排一次真正的进程重启:先回复 HTTP 请求,再 spawn 一个延迟启动的子进程,
|
|
163
|
+
* 然后退出当前进程。延迟是为了让当前进程完全退出并释放端口。
|
|
164
|
+
*/
|
|
165
|
+
function scheduleRestart(): void {
|
|
166
|
+
const indexPath = join(PROJECT_ROOT, "src", "index.ts");
|
|
167
|
+
if (!existsSync(indexPath)) return;
|
|
168
|
+
if (process.platform === "win32") {
|
|
169
|
+
// Windows: ping 作为 sleep(ping 自己 3 次 ≈ 2 秒延迟)
|
|
170
|
+
spawn("cmd.exe", ["/c", "ping -n 3 127.0.0.1 > nul && npx tsx src/index.ts"], {
|
|
171
|
+
cwd: PROJECT_ROOT,
|
|
172
|
+
detached: true,
|
|
173
|
+
stdio: "ignore",
|
|
174
|
+
windowsHide: true,
|
|
175
|
+
}).unref();
|
|
176
|
+
} else {
|
|
177
|
+
spawn("bash", ["-c", "sleep 2 && npx tsx src/index.ts"], {
|
|
178
|
+
cwd: PROJECT_ROOT,
|
|
179
|
+
detached: true,
|
|
180
|
+
stdio: "ignore",
|
|
181
|
+
}).unref();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
161
185
|
function stopService(): { ok: boolean; error?: string } {
|
|
162
186
|
const pid = getServicePid();
|
|
163
187
|
if (!pid) return { ok: false, error: "No PID file found" };
|
|
@@ -396,23 +420,10 @@ async function handleStartService(_req: IncomingMessage, res: ServerResponse): P
|
|
|
396
420
|
}
|
|
397
421
|
|
|
398
422
|
if (path === "reload") {
|
|
399
|
-
// service
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
// setReloadConfigHook();返回 500 让前端有提示,避免静默"看似生效但没生效"。
|
|
404
|
-
jsonReply(res, 500, {
|
|
405
|
-
ok: false,
|
|
406
|
-
error: "reload hook 未注册(应在 main() 调用 setReloadConfigHook)",
|
|
407
|
-
});
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
try {
|
|
411
|
-
await reloadConfigHook();
|
|
412
|
-
jsonReply(res, 200, { ok: true, pid: process.pid, mode: "reload" });
|
|
413
|
-
} catch (err) {
|
|
414
|
-
jsonReply(res, 500, { ok: false, error: (err as Error).message });
|
|
415
|
-
}
|
|
423
|
+
// service 已经在跑,执行真正的进程重启让所有配置(含 WSClient domain)生效
|
|
424
|
+
jsonReply(res, 200, { ok: true, pid: process.pid, mode: "restart" });
|
|
425
|
+
scheduleRestart();
|
|
426
|
+
process.exit(0);
|
|
416
427
|
return;
|
|
417
428
|
}
|
|
418
429
|
|
|
@@ -437,14 +448,10 @@ async function handleStopService(req: IncomingMessage, res: ServerResponse): Pro
|
|
|
437
448
|
});
|
|
438
449
|
}
|
|
439
450
|
|
|
440
|
-
async function handleRestartService(
|
|
451
|
+
async function handleRestartService(_req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
441
452
|
jsonReply(res, 200, { ok: true, message: "Restarting..." });
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
setTimeout(() => {
|
|
445
|
-
spawnService();
|
|
446
|
-
}, 1000);
|
|
447
|
-
});
|
|
453
|
+
scheduleRestart();
|
|
454
|
+
process.exit(0);
|
|
448
455
|
}
|
|
449
456
|
|
|
450
457
|
async function handleValidate(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
@@ -1309,20 +1316,20 @@ async function saveAndStart() {
|
|
|
1309
1316
|
document.getElementById('btn-save-start').innerHTML = '<span class="spinner"></span> 应用中...';
|
|
1310
1317
|
var result = await api('/api/start', 'POST');
|
|
1311
1318
|
if (result.ok) {
|
|
1312
|
-
// 后端按 mode 区分场景,前端给出更贴切的 toast:
|
|
1313
|
-
// - inplace:setup → service 首次激活,进程内启动飞书 service
|
|
1314
|
-
// - reload :service 已经在跑,仅刷新进程内 config(不真重启)
|
|
1315
|
-
// - spawn :旧 service 已退出,spawn 新子进程
|
|
1316
1319
|
var msg;
|
|
1317
|
-
if (result.mode === '
|
|
1318
|
-
msg = '
|
|
1320
|
+
if (result.mode === 'restart') {
|
|
1321
|
+
msg = '配置已保存,服务正在重启…';
|
|
1319
1322
|
} else if (result.mode === 'inplace') {
|
|
1320
1323
|
msg = '服务已启动! PID: ' + result.pid;
|
|
1321
1324
|
} else {
|
|
1322
1325
|
msg = '服务已启动! PID: ' + (result.pid || '?');
|
|
1323
1326
|
}
|
|
1324
1327
|
toast(msg);
|
|
1325
|
-
|
|
1328
|
+
if (result.mode === 'restart') {
|
|
1329
|
+
pollUntilRunning();
|
|
1330
|
+
} else {
|
|
1331
|
+
setTimeout(function(){ location.reload(); }, 1500);
|
|
1332
|
+
}
|
|
1326
1333
|
} else {
|
|
1327
1334
|
toast('保存失败: ' + (result.error || '未知错误'), 'error');
|
|
1328
1335
|
document.getElementById('btn-save-start').disabled = false;
|
|
@@ -1458,16 +1465,21 @@ async function restartService() {
|
|
|
1458
1465
|
if (!confirm('确定要重启服务吗?')) return;
|
|
1459
1466
|
document.getElementById('btn-restart').disabled = true;
|
|
1460
1467
|
document.getElementById('btn-restart').textContent = '重启中...';
|
|
1461
|
-
await api('/api/
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1468
|
+
await api('/api/restart', 'POST');
|
|
1469
|
+
pollUntilRunning();
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
async function pollUntilRunning() {
|
|
1473
|
+
for (var i = 0; i < 30; i++) {
|
|
1474
|
+
await new Promise(function(r) { setTimeout(r, 1000); });
|
|
1475
|
+
try {
|
|
1476
|
+
var s = await api('/api/status');
|
|
1477
|
+
if (s.running) { location.reload(); return; }
|
|
1478
|
+
} catch(e) {}
|
|
1479
|
+
}
|
|
1480
|
+
toast('重启超时,请在终端手动运行 chatccc', 'error');
|
|
1481
|
+
document.getElementById('btn-restart').disabled = false;
|
|
1482
|
+
document.getElementById('btn-restart').textContent = '重启';
|
|
1471
1483
|
}
|
|
1472
1484
|
|
|
1473
1485
|
// ---- Edit Modal ----
|
|
@@ -1602,6 +1614,7 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
|
|
|
1602
1614
|
if (url === "/api/status" && method === "GET") return handleGetStatus(req, res);
|
|
1603
1615
|
if (url === "/api/start" && method === "POST") return handleStartService(req, res);
|
|
1604
1616
|
if (url === "/api/stop" && method === "POST") return handleStopService(req, res);
|
|
1617
|
+
if (url === "/api/restart" && method === "POST") return handleRestartService(req, res);
|
|
1605
1618
|
if (url === "/api/validate" && method === "POST") return handleValidate(req, res);
|
|
1606
1619
|
if (url === "/api/ilink/forget" && method === "POST") return handleForgetIlink(req, res);
|
|
1607
1620
|
|