chatccc 0.2.297 → 0.2.298
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 +2 -2
- package/dist/src/cards.js +2 -2
- package/dist/src/session.js +125 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -229,7 +229,7 @@ Claude Code、Cursor 和 Codex 需要对应的本地工具;CCC Agent 内置于
|
|
|
229
229
|
|
|
230
230
|
CCC Agent 是 ChatCCC 内置的编程 Agent,不需要额外安装 CLI,开箱即用。在首次配置向导或 Web 管理页中启用后,填写 API Key、Base URL 和模型即可使用;它可以设为 `/new` 的默认 Agent,也可以通过 `/new ccc` 显式创建会话。
|
|
231
231
|
|
|
232
|
-
CCC Agent 支持**协作式让位**:任务运行期间你可以继续在群里发消息,ChatCCC 会在 Agent 的每个模型步骤边界把新消息注入当前任务,不打断正在执行的工具步骤、不新开一轮对话。已完成的中间工作会保留,Agent 收到插话后会调整后续方向。运行中的消息进入独立的注入队列(深度 50),与整轮排队(深度 1
|
|
232
|
+
CCC Agent 支持**协作式让位**:任务运行期间你可以继续在群里发消息,ChatCCC 会在 Agent 的每个模型步骤边界把新消息注入当前任务,不打断正在执行的工具步骤、不新开一轮对话。已完成的中间工作会保留,Agent 收到插话后会调整后续方向。运行中的消息进入独立的注入队列(深度 50),与整轮排队(深度 1)分离。飞书中每条消息实际注入后,机器人会发送一条最多 200 字的回执,冻结注入前的过程卡片,并用新的“生成中”卡片展示后续过程。
|
|
233
233
|
|
|
234
234
|
DeepCCC 同时提供独立端口的本地 Web UI。ChatCCC 管理页顶部点击 **DeepCCC Web** 会复用或按需启动该服务;只全局安装 `deepccc` 的用户直接运行 `deepccc` 即可启动并打开网页版,终端模式改用 `deepccc-cli`。默认地址为 `http://127.0.0.1:28080/`,端口可通过 `~/.deepccc/config.json` 的 `web.port` 修改。网页版支持多会话并发、持久化历史、会话级 model/effort、API 设置和会话内高风险操作审批。
|
|
235
235
|
|
|
@@ -290,7 +290,7 @@ codex --version
|
|
|
290
290
|
|
|
291
291
|
Codex 的默认模型和推理强度可继续由 `~/.codex/config.toml` 管理,也可以在 `config.json` 中覆盖。
|
|
292
292
|
|
|
293
|
-
ChatCCC 通过 Codex 的 **app-server 模式**(一个常驻进程 + WebSocket JSON-RPC)驱动 Codex,而不是一次性 `codex exec` 子进程。因此 Codex 与 CCC Agent 一样支持**协作式让位**:任务运行期间继续发消息,ChatCCC 会在 Codex 的工具步骤边界把消息注入当前 turn(`turn/steer
|
|
293
|
+
ChatCCC 通过 Codex 的 **app-server 模式**(一个常驻进程 + WebSocket JSON-RPC)驱动 Codex,而不是一次性 `codex exec` 子进程。因此 Codex 与 CCC Agent 一样支持**协作式让位**:任务运行期间继续发消息,ChatCCC 会在 Codex 的工具步骤边界把消息注入当前 turn(`turn/steer`),不打断正在执行的命令、不丢已完成的工作;飞书会在 `turn/steer` 成功后发送注入回执,并从新的过程卡片继续展示。`/stop` 通过 `turn/interrupt` 只打断当前 turn,不影响同进程内的其他会话。权限策略与旧 exec 的 `--dangerously-bypass-approvals-and-sandbox` 等价(零审批、完整沙箱权限),`/plan`、`/ask` 退化为只读沙箱。要求 Codex CLI >= 0.60.0(建议保持最新版)。
|
|
294
294
|
|
|
295
295
|
#### 可选:Chrome CDP
|
|
296
296
|
|
package/dist/src/cards.js
CHANGED
|
@@ -399,7 +399,7 @@ export function buildQueueFullCard() {
|
|
|
399
399
|
],
|
|
400
400
|
});
|
|
401
401
|
}
|
|
402
|
-
//
|
|
402
|
+
// 协作式让位卡片(ccc/codex):消息进入注入队列,将在 step/item 边界吸收进本轮。
|
|
403
403
|
// 与上面的整轮队列卡片区分,强调“无需等待整轮结束”。
|
|
404
404
|
export function buildInjectionQueuedCard(text) {
|
|
405
405
|
const preview = text.length > 100 ? text.slice(0, 100) + "…" : text;
|
|
@@ -419,7 +419,7 @@ export function buildInjectionQueuedCard(text) {
|
|
|
419
419
|
],
|
|
420
420
|
});
|
|
421
421
|
}
|
|
422
|
-
//
|
|
422
|
+
// 注入队列满卡片(ccc/codex)
|
|
423
423
|
export function buildInjectionQueueFullCard() {
|
|
424
424
|
return JSON.stringify({
|
|
425
425
|
config: { wide_screen_mode: true },
|
package/dist/src/session.js
CHANGED
|
@@ -267,6 +267,28 @@ function formatTerminalCardContent(state) {
|
|
|
267
267
|
? `${content}\n\n${reason}\n以上内容可能不完整。`
|
|
268
268
|
: reason;
|
|
269
269
|
}
|
|
270
|
+
function formatTerminalCardContentForDisplay(state, display) {
|
|
271
|
+
if (display.rotationAccLen === undefined)
|
|
272
|
+
return formatTerminalCardContent(state);
|
|
273
|
+
const accumulatedDelta = state.accumulatedContent.slice(display.rotationAccLen);
|
|
274
|
+
const previousFinalReply = display.rotationFinalReply ?? "";
|
|
275
|
+
const finalReplyDelta = previousFinalReply && state.finalReply.startsWith(previousFinalReply)
|
|
276
|
+
? state.finalReply.slice(previousFinalReply.length)
|
|
277
|
+
: state.finalReply;
|
|
278
|
+
const content = accumulatedDelta + finalReplyDelta;
|
|
279
|
+
if (state.status !== "error")
|
|
280
|
+
return content;
|
|
281
|
+
const error = state.terminalError ?? {
|
|
282
|
+
kind: "unknown",
|
|
283
|
+
title: "原因未记录",
|
|
284
|
+
message: "当前状态中没有可用的错误详情,请查看运行日志。",
|
|
285
|
+
occurredAt: Date.now(),
|
|
286
|
+
};
|
|
287
|
+
const reason = formatTerminalErrorReason(error);
|
|
288
|
+
return content.trim()
|
|
289
|
+
? `${content}\n\n${reason}\n以上内容可能不完整。`
|
|
290
|
+
: reason;
|
|
291
|
+
}
|
|
270
292
|
function isCardKitSequenceConflict(err) {
|
|
271
293
|
return err instanceof Error && err.message.includes("300317");
|
|
272
294
|
}
|
|
@@ -729,6 +751,85 @@ export function _resetSessionRegistryFileForTest() {
|
|
|
729
751
|
export function pickFinalReply(state) {
|
|
730
752
|
return state.finalCompleteText || state.finalText;
|
|
731
753
|
}
|
|
754
|
+
const APPLIED_INJECTION_PREVIEW_CHARS = 200;
|
|
755
|
+
export function unwrapInjectedUserMessage(text) {
|
|
756
|
+
const match = text.match(/^\[User message\]\r?\n([\s\S]*)\r?\n\[\/User message\]$/);
|
|
757
|
+
return match?.[1] ?? text;
|
|
758
|
+
}
|
|
759
|
+
export function formatAppliedInjectionReceipt(text) {
|
|
760
|
+
const normalized = text.trim() || "(空消息)";
|
|
761
|
+
const chars = Array.from(normalized);
|
|
762
|
+
const preview = chars.length > APPLIED_INJECTION_PREVIEW_CHARS
|
|
763
|
+
? `${chars.slice(0, APPLIED_INJECTION_PREVIEW_CHARS).join("")}…`
|
|
764
|
+
: normalized;
|
|
765
|
+
return `已收到注入信息:${preview}`;
|
|
766
|
+
}
|
|
767
|
+
async function rotateFeishuCardForAppliedInjection(sessionId, turnCount, tool, injectedText, state, nextHeaderTitle) {
|
|
768
|
+
if (tool !== "codex" && tool !== "ccc")
|
|
769
|
+
return;
|
|
770
|
+
const chatId = pickDisplayChat(sessionId);
|
|
771
|
+
if (!chatId)
|
|
772
|
+
return;
|
|
773
|
+
const platform = platformForChat(chatId);
|
|
774
|
+
if (platform?.kind !== "feishu")
|
|
775
|
+
return;
|
|
776
|
+
let previousDisplay = displayCards.get(chatId);
|
|
777
|
+
if (previousDisplay
|
|
778
|
+
&& (previousDisplay.sessionId !== sessionId || previousDisplay.turnCount !== turnCount)) {
|
|
779
|
+
previousDisplay = undefined;
|
|
780
|
+
}
|
|
781
|
+
if (previousDisplay) {
|
|
782
|
+
while (previousDisplay.cardBusy)
|
|
783
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
784
|
+
if (displayCards.get(chatId) !== previousDisplay)
|
|
785
|
+
previousDisplay = undefined;
|
|
786
|
+
}
|
|
787
|
+
if (previousDisplay) {
|
|
788
|
+
previousDisplay.cardBusy = true;
|
|
789
|
+
const nextSequence = previousDisplay.sequence + 1;
|
|
790
|
+
const oldContent = state.accumulatedContent + pickFinalReply(state);
|
|
791
|
+
const oldCard = buildProgressCard(progressView({
|
|
792
|
+
text: truncateContent(oldContent) || "等待 Agent 输出...",
|
|
793
|
+
status: "done",
|
|
794
|
+
showStop: false,
|
|
795
|
+
headerTitle: "已收到注入,后续转入新卡片",
|
|
796
|
+
headerTemplate: "blue",
|
|
797
|
+
}));
|
|
798
|
+
await platform.cardUpdate(previousDisplay.cardId, oldCard, nextSequence).then(() => {
|
|
799
|
+
previousDisplay.sequence = nextSequence;
|
|
800
|
+
}).catch((err) => {
|
|
801
|
+
console.error(`[${ts()}] [INJECT] freeze old card failed: ${err.message}`);
|
|
802
|
+
if (isCardKitSequenceConflict(err))
|
|
803
|
+
previousDisplay.sequence = nextSequence;
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
await platform.sendText(chatId, formatAppliedInjectionReceipt(injectedText)).catch((err) => {
|
|
807
|
+
console.error(`[${ts()}] [INJECT] receipt send failed: ${err.message}`);
|
|
808
|
+
});
|
|
809
|
+
const newCardId = await createVisibleProgressCard(platform, chatId, sessionId, turnCount, "新生成卡片发送失败,结果将继续更新在上一张卡片中。", nextHeaderTitle);
|
|
810
|
+
if (!newCardId) {
|
|
811
|
+
if (previousDisplay)
|
|
812
|
+
previousDisplay.cardBusy = false;
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (previousDisplay) {
|
|
816
|
+
await markCardDone(sessionId, turnCount, previousDisplay.cardId);
|
|
817
|
+
}
|
|
818
|
+
displayCards.set(chatId, {
|
|
819
|
+
cardId: newCardId,
|
|
820
|
+
sequence: 1,
|
|
821
|
+
cardBusy: false,
|
|
822
|
+
cardCreatedAt: Date.now(),
|
|
823
|
+
lastSentContent: "",
|
|
824
|
+
lastSentHeaderTitle: nextHeaderTitle,
|
|
825
|
+
streamErrorNotified: false,
|
|
826
|
+
sessionId,
|
|
827
|
+
turnCount,
|
|
828
|
+
rotationAccLen: state.accumulatedContent.length,
|
|
829
|
+
rotationFinalReply: pickFinalReply(state),
|
|
830
|
+
dotCount: 0,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
732
833
|
export function accumulateBlockContent(block, state, toolCallMap) {
|
|
733
834
|
if (state.transcript !== undefined || (block.type !== "agent_progress" && block.type !== "text_reset")) {
|
|
734
835
|
const transcriptState = { transcript: state.transcript ?? [] };
|
|
@@ -1123,7 +1224,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1123
1224
|
else {
|
|
1124
1225
|
const nextSeq = display.sequence + 1;
|
|
1125
1226
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(prevState.status, prevState.terminalError);
|
|
1126
|
-
const cardContent = truncateContent(
|
|
1227
|
+
const cardContent = truncateContent(formatTerminalCardContentForDisplay(prevState, display)) || " ";
|
|
1127
1228
|
const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
|
|
1128
1229
|
await pp.cardUpdate(display.cardId, doneCard, nextSeq).catch(err => {
|
|
1129
1230
|
console.error(`[${ts()}] [DISPLAY] prevState final cardUpdate failed: ${err.message}`);
|
|
@@ -1289,7 +1390,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1289
1390
|
}
|
|
1290
1391
|
try {
|
|
1291
1392
|
// 供 onInjectionRejected 退回的“最后一条已取出但未成功注入”的消息
|
|
1292
|
-
|
|
1393
|
+
const injectionState = { lastShifted: null };
|
|
1293
1394
|
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1294
1395
|
onProcessStart: (processInfo) => {
|
|
1295
1396
|
startPromptProcessMonitor(sessionId, processInfo);
|
|
@@ -1312,16 +1413,16 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1312
1413
|
const injected = shiftInjection(sessionId);
|
|
1313
1414
|
if (!injected)
|
|
1314
1415
|
return undefined;
|
|
1315
|
-
|
|
1416
|
+
injectionState.lastShifted = injected;
|
|
1316
1417
|
// 与首次消息保持一致的结构化包装;imSkillsPrompt 已在首次消息中,
|
|
1317
1418
|
// 此处不重复注入,避免多轮注入导致上下文膨胀。
|
|
1318
1419
|
return `[User message]\n${injected.text}\n[/User message]`;
|
|
1319
1420
|
} : undefined,
|
|
1320
1421
|
// 注入未被当前 turn 吸收(如 turn 恰好已结束)时,退回队列头部,避免丢消息。
|
|
1321
1422
|
onInjectionRejected: tool === "ccc" || tool === "codex" ? () => {
|
|
1322
|
-
if (
|
|
1323
|
-
unshiftInjection(sessionId,
|
|
1324
|
-
|
|
1423
|
+
if (injectionState.lastShifted) {
|
|
1424
|
+
unshiftInjection(sessionId, injectionState.lastShifted);
|
|
1425
|
+
injectionState.lastShifted = null;
|
|
1325
1426
|
}
|
|
1326
1427
|
} : undefined,
|
|
1327
1428
|
})) {
|
|
@@ -1339,7 +1440,15 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1339
1440
|
let activityChanged = false;
|
|
1340
1441
|
let progressHeartbeat = false;
|
|
1341
1442
|
let outputReset = false;
|
|
1342
|
-
|
|
1443
|
+
let injectionBoundaryObserved = false;
|
|
1444
|
+
for (const originalBlock of unifiedMsg.blocks) {
|
|
1445
|
+
let block = originalBlock;
|
|
1446
|
+
if (originalBlock.type === "input_injected") {
|
|
1447
|
+
const injectedText = injectionState.lastShifted?.text
|
|
1448
|
+
?? unwrapInjectedUserMessage(originalBlock.text);
|
|
1449
|
+
injectionState.lastShifted = null;
|
|
1450
|
+
block = { ...originalBlock, text: injectedText };
|
|
1451
|
+
}
|
|
1343
1452
|
if (block.type === "agent_progress")
|
|
1344
1453
|
progressHeartbeat = true;
|
|
1345
1454
|
if (block.type === "text_reset") {
|
|
@@ -1349,6 +1458,10 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1349
1458
|
if (updateAgentActivity(activityTracker, block))
|
|
1350
1459
|
activityChanged = true;
|
|
1351
1460
|
accumulateBlockContent(block, state, toolCallMap);
|
|
1461
|
+
if (block.type === "input_injected") {
|
|
1462
|
+
injectionBoundaryObserved = true;
|
|
1463
|
+
await rotateFeishuCardForAppliedInjection(sessionId, nextTurnCount, tool, block.text, state, formatAgentActivityTitle(activityTracker.activity));
|
|
1464
|
+
}
|
|
1352
1465
|
if (block.type === "compact_boundary" && block.post_tokens) {
|
|
1353
1466
|
for (const cid of getChatsForSession(sessionId)) {
|
|
1354
1467
|
const sinfo = sessionInfoMap.get(cid);
|
|
@@ -1374,7 +1487,10 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1374
1487
|
}
|
|
1375
1488
|
// 定时写入文件
|
|
1376
1489
|
const now2 = Date.now();
|
|
1377
|
-
if (activityChanged
|
|
1490
|
+
if (activityChanged
|
|
1491
|
+
|| outputReset
|
|
1492
|
+
|| injectionBoundaryObserved
|
|
1493
|
+
|| now2 - lastFileWrite >= FILE_WRITE_INTERVAL_MS) {
|
|
1378
1494
|
lastFileWrite = now2;
|
|
1379
1495
|
await writeStreamState({
|
|
1380
1496
|
sessionId,
|
|
@@ -1814,7 +1930,7 @@ export function startUnifiedDisplayLoop() {
|
|
|
1814
1930
|
if (!terminalCardAlreadyUpdated) {
|
|
1815
1931
|
const nextSeq = display.sequence + 1;
|
|
1816
1932
|
const { title: headerTitle, template: headerTemplate } = formatTerminalHeader(state.status, state.terminalError);
|
|
1817
|
-
const cardContent = truncateContent(
|
|
1933
|
+
const cardContent = truncateContent(formatTerminalCardContentForDisplay(state, display)) || " ";
|
|
1818
1934
|
const doneCard = buildProgressCard(progressView({ text: cardContent, status: "done", showStop: false, headerTitle, headerTemplate }));
|
|
1819
1935
|
await p.cardUpdate(display.cardId, doneCard, nextSeq).then(() => {
|
|
1820
1936
|
display.sequence = nextSeq;
|