chatccc 0.2.229 → 0.2.231
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/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
- package/README.md +90 -90
- package/agent-prompts/claude_specific.md +45 -45
- package/agent-prompts/codex_specific.md +2 -2
- package/agent-prompts/cursor_specific.md +13 -13
- package/bin/cccagent.mjs +17 -17
- package/im-skills/feishu-skill/receive-send-file.md +63 -63
- package/im-skills/feishu-skill/receive-send-image.md +24 -24
- package/im-skills/feishu-skill/skill.md +3 -3
- package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
- package/im-skills/wechat-file-skill/send-file.mjs +83 -83
- package/im-skills/wechat-file-skill/skill.md +10 -10
- package/im-skills/wechat-image-skill/skill.md +10 -10
- package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
- package/im-skills/wechat-video-skill/send-video.mjs +79 -79
- package/im-skills/wechat-video-skill/skill.md +10 -10
- package/package.json +1 -1
- package/scripts/postinstall-sharp-check.mjs +58 -58
- package/src/__tests__/agent-activity.test.ts +86 -76
- package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
- package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
- package/src/__tests__/builtin-chat-session.test.ts +65 -1
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +26 -26
- package/src/__tests__/builtin-context.test.ts +202 -163
- package/src/__tests__/builtin-permissions.test.ts +211 -211
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/builtin-web-tools.test.ts +220 -220
- package/src/__tests__/card-action-routing.test.ts +18 -18
- package/src/__tests__/cardkit.test.ts +60 -60
- package/src/__tests__/ccc-adapter.test.ts +170 -136
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
- package/src/__tests__/claude-adapter.test.ts +614 -614
- package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-adapter.test.ts +58 -58
- package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
- package/src/__tests__/codex-reset-actions.test.ts +146 -146
- package/src/__tests__/cursor-adapter.test.ts +268 -268
- package/src/__tests__/feishu-api.test.ts +60 -60
- package/src/__tests__/feishu-avatar.test.ts +164 -164
- package/src/__tests__/feishu-message-ingress.test.ts +138 -138
- package/src/__tests__/feishu-platform.test.ts +22 -22
- package/src/__tests__/format-message.test.ts +47 -47
- package/src/__tests__/jsonl-stream.test.ts +79 -79
- package/src/__tests__/package-files.test.ts +24 -24
- package/src/__tests__/privacy.test.ts +198 -198
- package/src/__tests__/progress-reducer.test.ts +121 -110
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/response-stall.test.ts +49 -49
- package/src/__tests__/session.test.ts +174 -16
- package/src/__tests__/shared-prefix.test.ts +36 -36
- package/src/__tests__/sim-platform.test.ts +16 -16
- package/src/__tests__/startup-lifecycle.test.ts +231 -231
- package/src/__tests__/stop-session.test.ts +34 -34
- package/src/__tests__/stream-state.test.ts +42 -42
- package/src/__tests__/terminal-error.test.ts +54 -0
- package/src/__tests__/terminal-renderer.test.ts +247 -247
- package/src/__tests__/update-command-guard.test.ts +144 -144
- package/src/__tests__/web-ui.test.ts +326 -326
- package/src/adapters/adapter-interface.ts +24 -18
- package/src/adapters/ccc-adapter.ts +141 -131
- package/src/adapters/claude-adapter.ts +620 -620
- package/src/adapters/claude-session-meta-store.ts +120 -120
- package/src/adapters/codex-adapter.ts +426 -426
- package/src/adapters/cursor-adapter.ts +681 -681
- package/src/adapters/jsonl-stream.ts +157 -157
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/adapters/resource-monitor.ts +140 -140
- package/src/agent-activity.ts +175 -170
- package/src/agent-delegate-task-rpc.ts +153 -153
- package/src/agent-delegate-task.ts +91 -91
- package/src/agent-reload-config-rpc.ts +34 -34
- package/src/agent-stop-stuck.ts +129 -129
- package/src/builtin/cli.ts +9 -2
- package/src/builtin/config.ts +84 -84
- package/src/builtin/context.ts +62 -4
- package/src/builtin/file-log.ts +38 -38
- package/src/builtin/index.ts +83 -19
- package/src/builtin/proc-tree-kill.ts +61 -61
- package/src/builtin/progress/cards-helpers.ts +76 -76
- package/src/builtin/progress/reducer.ts +113 -108
- package/src/builtin/progress/terminal-renderer.ts +294 -294
- package/src/builtin/progress/view.ts +77 -77
- package/src/builtin/raw-stream-log.ts +124 -124
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/builtin/web-tools.ts +313 -313
- package/src/card-action-routing.ts +14 -14
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -318
- package/src/codex-reset-actions.ts +184 -184
- package/src/feishu-api.ts +193 -193
- package/src/feishu-message-ingress.ts +195 -195
- package/src/feishu-platform.ts +20 -20
- package/src/format-message.ts +293 -293
- package/src/litellm-proxy.ts +374 -374
- package/src/orchestrator.ts +8 -3
- package/src/platform-adapter.ts +6 -6
- package/src/privacy.ts +118 -118
- package/src/progress/reducer.ts +113 -108
- package/src/progress/terminal-renderer.ts +294 -294
- package/src/progress/view.ts +77 -77
- package/src/response-stall.ts +28 -28
- package/src/runtime-reload.ts +34 -34
- package/src/session-chat-binding.ts +82 -82
- package/src/session-name.ts +8 -8
- package/src/session.ts +149 -26
- package/src/shared-prefix.ts +29 -29
- package/src/sim-platform.ts +20 -20
- package/src/startup-lifecycle.ts +250 -250
- package/src/stream-state.ts +21 -18
- package/src/terminal-error.ts +129 -0
- package/src/turn-cards.ts +117 -117
- package/src/update-command-guard.ts +165 -165
package/src/agent-stop-stuck.ts
CHANGED
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
|
|
3
|
-
import { readUtf8JsonBody } from "./agent-rpc-body.ts";
|
|
4
|
-
import { activePrompts, getChatsForSession } from "./session-chat-binding.ts";
|
|
5
|
-
import { getPlatformForChat } from "./session.ts";
|
|
6
|
-
import { readStreamState, writeStreamState } from "./stream-state.ts";
|
|
7
|
-
import { ts } from "./config.ts";
|
|
8
|
-
|
|
9
|
-
export const AGENT_STOP_STUCK_PATH = "/api/agent/stop-stuck-loop";
|
|
10
|
-
|
|
11
|
-
const MAX_REQUEST_BYTES = 8 * 1024;
|
|
12
|
-
|
|
13
|
-
function jsonReply(res: ServerResponse, status: number, data: unknown): void {
|
|
14
|
-
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
15
|
-
res.end(JSON.stringify(data));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* 从原始 body 字节中用正则尽力提取 session_id。
|
|
20
|
-
* 兼容 JSON 编码异常(如 GBK 中文导致 JSON.parse 失败)的场景。
|
|
21
|
-
*/
|
|
22
|
-
function extractSessionIdFromRaw(buf: Buffer): string | null {
|
|
23
|
-
// 匹配 "session_id":"<uuid>" 或 "session_id": "<uuid>"
|
|
24
|
-
const match = buf.toString("latin1").match(/"session_id"\s*:\s*"([^"]+)"/);
|
|
25
|
-
return match?.[1] ?? null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function handleAgentStopStuckRequest(
|
|
29
|
-
req: IncomingMessage,
|
|
30
|
-
res: ServerResponse,
|
|
31
|
-
): Promise<boolean> {
|
|
32
|
-
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
33
|
-
if (url.pathname !== AGENT_STOP_STUCK_PATH) return false;
|
|
34
|
-
|
|
35
|
-
if (req.method !== "POST") {
|
|
36
|
-
jsonReply(res, 405, { error: "Method not allowed, use POST" });
|
|
37
|
-
return true;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
let body: { session_id?: string; final_reply?: string };
|
|
41
|
-
let rawBody: Buffer | null = null;
|
|
42
|
-
try {
|
|
43
|
-
body = await readUtf8JsonBody<{ session_id?: string; final_reply?: string }>(req, MAX_REQUEST_BYTES);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
// JSON 解析失败时仍尝试从原始字节中提取 session_id 并强行停止
|
|
46
|
-
// 宁可输出乱码,也不能让 agent 持续循环
|
|
47
|
-
rawBody = (err as { rawBody?: Buffer }).rawBody as Buffer | undefined ?? null;
|
|
48
|
-
if (!rawBody) {
|
|
49
|
-
jsonReply(res, 400, { error: `Invalid request body: ${(err as Error).message}` });
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
const fallbackId = extractSessionIdFromRaw(rawBody);
|
|
53
|
-
if (!fallbackId) {
|
|
54
|
-
jsonReply(res, 400, { error: `Invalid request body: ${(err as Error).message}` });
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
body = { session_id: fallbackId };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const sessionId = typeof body?.session_id === "string" ? body.session_id.trim() : "";
|
|
61
|
-
if (!sessionId) {
|
|
62
|
-
jsonReply(res, 400, { error: "session_id is required" });
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const prompt = activePrompts.get(sessionId);
|
|
67
|
-
if (!prompt) {
|
|
68
|
-
jsonReply(res, 404, { error: "Session not found or not running" });
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// 先发"卡住"提示消息给所有绑定的群聊
|
|
73
|
-
const chats = getChatsForSession(sessionId);
|
|
74
|
-
for (const chatId of chats) {
|
|
75
|
-
const platform = getPlatformForChat(chatId);
|
|
76
|
-
if (platform) {
|
|
77
|
-
await platform.sendText(chatId, "⚠️ Agent 检测到自己陷入了循环,正在结束生成…").catch(() => {});
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// 不丢弃缓存队列中的消息,让 runAgentSession 的 finally 块正常消费,
|
|
82
|
-
// 确保 stop-stuck-loop 结束后排队的消息仍能被正常处理。
|
|
83
|
-
|
|
84
|
-
const finalReply = typeof body?.final_reply === "string" ? body.final_reply.trim() : "";
|
|
85
|
-
|
|
86
|
-
// fire-and-forget:立即把 stream-state 标为 done(而非 stopped),
|
|
87
|
-
// 让 display loop 以"正常完成"而非"已停止"来渲染卡片和最终回复。
|
|
88
|
-
// 不设 prompt.stopped = true,这样 runAgentSession 的 finally 也会写 "done"。
|
|
89
|
-
void (async () => {
|
|
90
|
-
try {
|
|
91
|
-
const current = await readStreamState(sessionId);
|
|
92
|
-
if (!current) return;
|
|
93
|
-
if (current.status !== "running") return;
|
|
94
|
-
await writeStreamState({
|
|
95
|
-
...current,
|
|
96
|
-
status: "done",
|
|
97
|
-
stuckAt: Date.now(),
|
|
98
|
-
finalReply: finalReply ? current.finalReply + "\n\n" + finalReply : current.finalReply,
|
|
99
|
-
updatedAt: Date.now(),
|
|
100
|
-
});
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.warn(
|
|
103
|
-
`[${ts()}] [STUCK-LOOP] writeStreamState(done) failed for ${sessionId}: ${(err as Error).message}`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
})();
|
|
107
|
-
|
|
108
|
-
// 先关闭底层 SDK session,终止 CLI 子进程,然后再 abort 清理 adapter 层
|
|
109
|
-
prompt.closeSession?.();
|
|
110
|
-
|
|
111
|
-
// 强制杀死 CLI 子进程。controller.abort() 只在 for-await 收到下一条
|
|
112
|
-
// stream 消息时才被检测到——如果 agent 陷入无输出的计算循环,abort 信号
|
|
113
|
-
// 永远不会生效;SDK 的 session.close() 也不保证立即终止子进程。
|
|
114
|
-
if (prompt.processPid !== undefined) {
|
|
115
|
-
try {
|
|
116
|
-
process.kill(prompt.processPid);
|
|
117
|
-
} catch {
|
|
118
|
-
// 进程可能已退出,忽略错误
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// 不设 stopped 标记 → finally block 写 "done" → 卡片正常结束
|
|
123
|
-
prompt.controller.abort();
|
|
124
|
-
|
|
125
|
-
console.log(`[${ts()}] [STUCK-LOOP] Session ${sessionId} aborted as done (agent detected stuck loop, final_reply=${finalReply ? "yes" : "no"})`);
|
|
126
|
-
|
|
127
|
-
jsonReply(res, 200, { ok: true });
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
|
|
3
|
+
import { readUtf8JsonBody } from "./agent-rpc-body.ts";
|
|
4
|
+
import { activePrompts, getChatsForSession } from "./session-chat-binding.ts";
|
|
5
|
+
import { getPlatformForChat } from "./session.ts";
|
|
6
|
+
import { readStreamState, writeStreamState } from "./stream-state.ts";
|
|
7
|
+
import { ts } from "./config.ts";
|
|
8
|
+
|
|
9
|
+
export const AGENT_STOP_STUCK_PATH = "/api/agent/stop-stuck-loop";
|
|
10
|
+
|
|
11
|
+
const MAX_REQUEST_BYTES = 8 * 1024;
|
|
12
|
+
|
|
13
|
+
function jsonReply(res: ServerResponse, status: number, data: unknown): void {
|
|
14
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
15
|
+
res.end(JSON.stringify(data));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 从原始 body 字节中用正则尽力提取 session_id。
|
|
20
|
+
* 兼容 JSON 编码异常(如 GBK 中文导致 JSON.parse 失败)的场景。
|
|
21
|
+
*/
|
|
22
|
+
function extractSessionIdFromRaw(buf: Buffer): string | null {
|
|
23
|
+
// 匹配 "session_id":"<uuid>" 或 "session_id": "<uuid>"
|
|
24
|
+
const match = buf.toString("latin1").match(/"session_id"\s*:\s*"([^"]+)"/);
|
|
25
|
+
return match?.[1] ?? null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function handleAgentStopStuckRequest(
|
|
29
|
+
req: IncomingMessage,
|
|
30
|
+
res: ServerResponse,
|
|
31
|
+
): Promise<boolean> {
|
|
32
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
33
|
+
if (url.pathname !== AGENT_STOP_STUCK_PATH) return false;
|
|
34
|
+
|
|
35
|
+
if (req.method !== "POST") {
|
|
36
|
+
jsonReply(res, 405, { error: "Method not allowed, use POST" });
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let body: { session_id?: string; final_reply?: string };
|
|
41
|
+
let rawBody: Buffer | null = null;
|
|
42
|
+
try {
|
|
43
|
+
body = await readUtf8JsonBody<{ session_id?: string; final_reply?: string }>(req, MAX_REQUEST_BYTES);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// JSON 解析失败时仍尝试从原始字节中提取 session_id 并强行停止
|
|
46
|
+
// 宁可输出乱码,也不能让 agent 持续循环
|
|
47
|
+
rawBody = (err as { rawBody?: Buffer }).rawBody as Buffer | undefined ?? null;
|
|
48
|
+
if (!rawBody) {
|
|
49
|
+
jsonReply(res, 400, { error: `Invalid request body: ${(err as Error).message}` });
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const fallbackId = extractSessionIdFromRaw(rawBody);
|
|
53
|
+
if (!fallbackId) {
|
|
54
|
+
jsonReply(res, 400, { error: `Invalid request body: ${(err as Error).message}` });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
body = { session_id: fallbackId };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const sessionId = typeof body?.session_id === "string" ? body.session_id.trim() : "";
|
|
61
|
+
if (!sessionId) {
|
|
62
|
+
jsonReply(res, 400, { error: "session_id is required" });
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const prompt = activePrompts.get(sessionId);
|
|
67
|
+
if (!prompt) {
|
|
68
|
+
jsonReply(res, 404, { error: "Session not found or not running" });
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 先发"卡住"提示消息给所有绑定的群聊
|
|
73
|
+
const chats = getChatsForSession(sessionId);
|
|
74
|
+
for (const chatId of chats) {
|
|
75
|
+
const platform = getPlatformForChat(chatId);
|
|
76
|
+
if (platform) {
|
|
77
|
+
await platform.sendText(chatId, "⚠️ Agent 检测到自己陷入了循环,正在结束生成…").catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 不丢弃缓存队列中的消息,让 runAgentSession 的 finally 块正常消费,
|
|
82
|
+
// 确保 stop-stuck-loop 结束后排队的消息仍能被正常处理。
|
|
83
|
+
|
|
84
|
+
const finalReply = typeof body?.final_reply === "string" ? body.final_reply.trim() : "";
|
|
85
|
+
|
|
86
|
+
// fire-and-forget:立即把 stream-state 标为 done(而非 stopped),
|
|
87
|
+
// 让 display loop 以"正常完成"而非"已停止"来渲染卡片和最终回复。
|
|
88
|
+
// 不设 prompt.stopped = true,这样 runAgentSession 的 finally 也会写 "done"。
|
|
89
|
+
void (async () => {
|
|
90
|
+
try {
|
|
91
|
+
const current = await readStreamState(sessionId);
|
|
92
|
+
if (!current) return;
|
|
93
|
+
if (current.status !== "running") return;
|
|
94
|
+
await writeStreamState({
|
|
95
|
+
...current,
|
|
96
|
+
status: "done",
|
|
97
|
+
stuckAt: Date.now(),
|
|
98
|
+
finalReply: finalReply ? current.finalReply + "\n\n" + finalReply : current.finalReply,
|
|
99
|
+
updatedAt: Date.now(),
|
|
100
|
+
});
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.warn(
|
|
103
|
+
`[${ts()}] [STUCK-LOOP] writeStreamState(done) failed for ${sessionId}: ${(err as Error).message}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
|
|
108
|
+
// 先关闭底层 SDK session,终止 CLI 子进程,然后再 abort 清理 adapter 层
|
|
109
|
+
prompt.closeSession?.();
|
|
110
|
+
|
|
111
|
+
// 强制杀死 CLI 子进程。controller.abort() 只在 for-await 收到下一条
|
|
112
|
+
// stream 消息时才被检测到——如果 agent 陷入无输出的计算循环,abort 信号
|
|
113
|
+
// 永远不会生效;SDK 的 session.close() 也不保证立即终止子进程。
|
|
114
|
+
if (prompt.processPid !== undefined) {
|
|
115
|
+
try {
|
|
116
|
+
process.kill(prompt.processPid);
|
|
117
|
+
} catch {
|
|
118
|
+
// 进程可能已退出,忽略错误
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 不设 stopped 标记 → finally block 写 "done" → 卡片正常结束
|
|
123
|
+
prompt.controller.abort();
|
|
124
|
+
|
|
125
|
+
console.log(`[${ts()}] [STUCK-LOOP] Session ${sessionId} aborted as done (agent detected stuck loop, final_reply=${finalReply ? "yes" : "no"})`);
|
|
126
|
+
|
|
127
|
+
jsonReply(res, 200, { ok: true });
|
|
128
|
+
return true;
|
|
129
|
+
}
|
package/src/builtin/cli.ts
CHANGED
|
@@ -216,7 +216,12 @@ function writeJsonLine(event: JsonLine): void {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
function streamJsonEvent(event: ChatEvent): void {
|
|
219
|
-
if (event.type === "
|
|
219
|
+
if (event.type === "status") {
|
|
220
|
+
writeJsonLine({
|
|
221
|
+
type: "status",
|
|
222
|
+
phase: event.phase,
|
|
223
|
+
});
|
|
224
|
+
} else if (event.type === "text") {
|
|
220
225
|
writeJsonLine({
|
|
221
226
|
type: "text_delta",
|
|
222
227
|
text: event.text,
|
|
@@ -484,11 +489,13 @@ async function runRepl(args: ParsedArgs): Promise<void> {
|
|
|
484
489
|
for await (const event of session.chat(input, signal)) {
|
|
485
490
|
if (renderer && view) {
|
|
486
491
|
view = reduceProgress(view, event);
|
|
487
|
-
if (event.type === "text" || event.type === "compact") {
|
|
492
|
+
if (event.type === "text" || event.type === "compact" || event.type === "status") {
|
|
488
493
|
renderer.render(view);
|
|
489
494
|
} else {
|
|
490
495
|
renderer.flush();
|
|
491
496
|
}
|
|
497
|
+
} else if (event.type === "status") {
|
|
498
|
+
console.log(`${C.dim}[${event.phase === "compacting" ? "compacting context" : "generating reply"}]${C.reset}`);
|
|
492
499
|
} else if (event.type === "text") {
|
|
493
500
|
const newText = event.accumulated.slice(lastAccumulated.length);
|
|
494
501
|
process.stdout.write(newText);
|
package/src/builtin/config.ts
CHANGED
|
@@ -1,84 +1,84 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
|
|
5
|
-
export interface DeepCccConfig {
|
|
6
|
-
apiKey: string;
|
|
7
|
-
baseURL: string;
|
|
8
|
-
model: string;
|
|
9
|
-
/** Reasoning effort(none/minimal/low/medium/high/xhigh/max),留空不传 reasoning_effort */
|
|
10
|
-
effort: string;
|
|
11
|
-
rawStreamLogs: {
|
|
12
|
-
enabled: boolean;
|
|
13
|
-
maxBytesPerTurn: number;
|
|
14
|
-
retentionDays: number;
|
|
15
|
-
keepCompleted: boolean;
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const DEEPCCC_HOME = join(homedir(), ".deepccc");
|
|
20
|
-
export const RAW_STREAM_LOGS_DIR = join(DEEPCCC_HOME, "raw-stream-logs");
|
|
21
|
-
const CONFIG_PATH = join(DEEPCCC_HOME, "config.json");
|
|
22
|
-
|
|
23
|
-
const DEFAULT_CONFIG: DeepCccConfig = {
|
|
24
|
-
apiKey: "",
|
|
25
|
-
baseURL: "https://api.deepseek.com/v1",
|
|
26
|
-
model: "deepseek-v4-pro",
|
|
27
|
-
effort: "",
|
|
28
|
-
rawStreamLogs: {
|
|
29
|
-
enabled: false,
|
|
30
|
-
maxBytesPerTurn: 1024 * 1024,
|
|
31
|
-
retentionDays: 7,
|
|
32
|
-
keepCompleted: false,
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
function readConfigFile(): Partial<DeepCccConfig> {
|
|
37
|
-
if (!existsSync(CONFIG_PATH)) return {};
|
|
38
|
-
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<DeepCccConfig>;
|
|
39
|
-
return raw && typeof raw === "object" ? raw : {};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function env(name: string): string | undefined {
|
|
43
|
-
const value = process.env[name]?.trim();
|
|
44
|
-
return value ? value : undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function boolEnv(name: string): boolean | undefined {
|
|
48
|
-
const value = env(name)?.toLowerCase();
|
|
49
|
-
if (value === undefined) return undefined;
|
|
50
|
-
if (["1", "true", "yes", "on"].includes(value)) return true;
|
|
51
|
-
if (["0", "false", "no", "off"].includes(value)) return false;
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function numberEnv(name: string): number | undefined {
|
|
56
|
-
const value = Number(env(name));
|
|
57
|
-
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function loadConfig(): DeepCccConfig {
|
|
61
|
-
const file = readConfigFile();
|
|
62
|
-
const rawLogs: Partial<DeepCccConfig["rawStreamLogs"]> = file.rawStreamLogs && typeof file.rawStreamLogs === "object"
|
|
63
|
-
? file.rawStreamLogs
|
|
64
|
-
: {};
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
|
|
68
|
-
baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
|
|
69
|
-
model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
|
|
70
|
-
effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
|
|
71
|
-
rawStreamLogs: {
|
|
72
|
-
enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
|
|
73
|
-
maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
|
|
74
|
-
retentionDays: numberEnv("DEEPCCC_RAW_STREAM_RETENTION_DAYS") ?? rawLogs.retentionDays ?? DEFAULT_CONFIG.rawStreamLogs.retentionDays,
|
|
75
|
-
keepCompleted: boolEnv("DEEPCCC_RAW_STREAM_KEEP_COMPLETED") ?? rawLogs.keepCompleted ?? DEFAULT_CONFIG.rawStreamLogs.keepCompleted,
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function ensureConfigDir(): void {
|
|
81
|
-
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export const config = loadConfig();
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface DeepCccConfig {
|
|
6
|
+
apiKey: string;
|
|
7
|
+
baseURL: string;
|
|
8
|
+
model: string;
|
|
9
|
+
/** Reasoning effort(none/minimal/low/medium/high/xhigh/max),留空不传 reasoning_effort */
|
|
10
|
+
effort: string;
|
|
11
|
+
rawStreamLogs: {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
maxBytesPerTurn: number;
|
|
14
|
+
retentionDays: number;
|
|
15
|
+
keepCompleted: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DEEPCCC_HOME = join(homedir(), ".deepccc");
|
|
20
|
+
export const RAW_STREAM_LOGS_DIR = join(DEEPCCC_HOME, "raw-stream-logs");
|
|
21
|
+
const CONFIG_PATH = join(DEEPCCC_HOME, "config.json");
|
|
22
|
+
|
|
23
|
+
const DEFAULT_CONFIG: DeepCccConfig = {
|
|
24
|
+
apiKey: "",
|
|
25
|
+
baseURL: "https://api.deepseek.com/v1",
|
|
26
|
+
model: "deepseek-v4-pro",
|
|
27
|
+
effort: "",
|
|
28
|
+
rawStreamLogs: {
|
|
29
|
+
enabled: false,
|
|
30
|
+
maxBytesPerTurn: 1024 * 1024,
|
|
31
|
+
retentionDays: 7,
|
|
32
|
+
keepCompleted: false,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function readConfigFile(): Partial<DeepCccConfig> {
|
|
37
|
+
if (!existsSync(CONFIG_PATH)) return {};
|
|
38
|
+
const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf8")) as Partial<DeepCccConfig>;
|
|
39
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function env(name: string): string | undefined {
|
|
43
|
+
const value = process.env[name]?.trim();
|
|
44
|
+
return value ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function boolEnv(name: string): boolean | undefined {
|
|
48
|
+
const value = env(name)?.toLowerCase();
|
|
49
|
+
if (value === undefined) return undefined;
|
|
50
|
+
if (["1", "true", "yes", "on"].includes(value)) return true;
|
|
51
|
+
if (["0", "false", "no", "off"].includes(value)) return false;
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function numberEnv(name: string): number | undefined {
|
|
56
|
+
const value = Number(env(name));
|
|
57
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadConfig(): DeepCccConfig {
|
|
61
|
+
const file = readConfigFile();
|
|
62
|
+
const rawLogs: Partial<DeepCccConfig["rawStreamLogs"]> = file.rawStreamLogs && typeof file.rawStreamLogs === "object"
|
|
63
|
+
? file.rawStreamLogs
|
|
64
|
+
: {};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
apiKey: env("DEEPCCC_API_KEY") ?? env("DEEPSEEK_API_KEY") ?? file.apiKey ?? DEFAULT_CONFIG.apiKey,
|
|
68
|
+
baseURL: env("DEEPCCC_BASE_URL") ?? env("DEEPSEEK_BASE_URL") ?? file.baseURL ?? DEFAULT_CONFIG.baseURL,
|
|
69
|
+
model: env("DEEPCCC_MODEL") ?? env("DEEPSEEK_MODEL") ?? file.model ?? DEFAULT_CONFIG.model,
|
|
70
|
+
effort: env("DEEPCCC_EFFORT") ?? env("DEEPSEEK_EFFORT") ?? file.effort ?? DEFAULT_CONFIG.effort,
|
|
71
|
+
rawStreamLogs: {
|
|
72
|
+
enabled: boolEnv("DEEPCCC_RAW_STREAM_LOGS") ?? rawLogs.enabled ?? DEFAULT_CONFIG.rawStreamLogs.enabled,
|
|
73
|
+
maxBytesPerTurn: numberEnv("DEEPCCC_RAW_STREAM_MAX_BYTES") ?? rawLogs.maxBytesPerTurn ?? DEFAULT_CONFIG.rawStreamLogs.maxBytesPerTurn,
|
|
74
|
+
retentionDays: numberEnv("DEEPCCC_RAW_STREAM_RETENTION_DAYS") ?? rawLogs.retentionDays ?? DEFAULT_CONFIG.rawStreamLogs.retentionDays,
|
|
75
|
+
keepCompleted: boolEnv("DEEPCCC_RAW_STREAM_KEEP_COMPLETED") ?? rawLogs.keepCompleted ?? DEFAULT_CONFIG.rawStreamLogs.keepCompleted,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function ensureConfigDir(): void {
|
|
81
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const config = loadConfig();
|
package/src/builtin/context.ts
CHANGED
|
@@ -51,6 +51,10 @@ export interface BuiltinContextOptions {
|
|
|
51
51
|
export const DEFAULT_BUILTIN_CONTEXT_DIR = join(homedir(), ".deepccc", "sessions");
|
|
52
52
|
export const DEFAULT_COMPACT_AT_TOKENS = 48_000;
|
|
53
53
|
export const DEFAULT_KEEP_RECENT_MESSAGES = 16;
|
|
54
|
+
const RECENT_CONTEXT_BUDGET_RATIO = 0.6;
|
|
55
|
+
const MAX_COMPACTION_SUMMARY_CHARS = 16_000;
|
|
56
|
+
const MAX_COMPACTION_MESSAGE_CHARS = 24_000;
|
|
57
|
+
const MAX_COMPACTION_SOURCE_CHARS = 64_000;
|
|
54
58
|
|
|
55
59
|
export function normalizeBuiltinSessionId(value: string): string {
|
|
56
60
|
return value.replace(/[^a-zA-Z0-9_.-]+/g, "_").replace(/^_+|_+$/g, "") || "default";
|
|
@@ -202,6 +206,38 @@ export function serializeMessagesForSummary(messages: readonly BuiltinContextMes
|
|
|
202
206
|
.join("\n\n");
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
function truncateMiddle(value: string, maxChars: number, marker: string): string {
|
|
210
|
+
if (value.length <= maxChars) return value;
|
|
211
|
+
const available = Math.max(0, maxChars - marker.length - 2);
|
|
212
|
+
const headChars = Math.ceil(available / 2);
|
|
213
|
+
const tailChars = Math.floor(available / 2);
|
|
214
|
+
return `${value.slice(0, headChars)}\n${marker}\n${value.slice(-tailChars)}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function serializeMessagesForCompaction(messages: readonly BuiltinContextMessage[]): string {
|
|
218
|
+
const sections: string[] = [];
|
|
219
|
+
let remaining = MAX_COMPACTION_SOURCE_CHARS;
|
|
220
|
+
|
|
221
|
+
for (let index = 0; index < messages.length && remaining > 0; index += 1) {
|
|
222
|
+
const message = messages[index];
|
|
223
|
+
const header = `### ${index + 1}. ${message.role}\n`;
|
|
224
|
+
if (header.length >= remaining) break;
|
|
225
|
+
const maxContentChars = Math.min(MAX_COMPACTION_MESSAGE_CHARS, remaining - header.length);
|
|
226
|
+
const content = truncateMiddle(
|
|
227
|
+
message.content,
|
|
228
|
+
maxContentChars,
|
|
229
|
+
"...[truncated for compaction]...",
|
|
230
|
+
);
|
|
231
|
+
sections.push(`${header}${content}`);
|
|
232
|
+
remaining -= header.length + content.length + 2;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (sections.length < messages.length) {
|
|
236
|
+
sections.push(`...[${messages.length - sections.length} additional messages omitted for compaction]...`);
|
|
237
|
+
}
|
|
238
|
+
return sections.join("\n\n");
|
|
239
|
+
}
|
|
240
|
+
|
|
205
241
|
export function buildSummaryPrompt(plan: BuiltinCompactionPlan): string {
|
|
206
242
|
const sections = [
|
|
207
243
|
"Compress the older DeepCCC conversation context.",
|
|
@@ -215,10 +251,18 @@ export function buildSummaryPrompt(plan: BuiltinCompactionPlan): string {
|
|
|
215
251
|
];
|
|
216
252
|
|
|
217
253
|
if (plan.previousSummary.trim()) {
|
|
218
|
-
sections.push(
|
|
254
|
+
sections.push(
|
|
255
|
+
"## Existing Summary",
|
|
256
|
+
truncateMiddle(
|
|
257
|
+
plan.previousSummary.trim(),
|
|
258
|
+
MAX_COMPACTION_SUMMARY_CHARS,
|
|
259
|
+
"...[existing summary truncated for compaction]...",
|
|
260
|
+
),
|
|
261
|
+
"",
|
|
262
|
+
);
|
|
219
263
|
}
|
|
220
264
|
|
|
221
|
-
sections.push("## Messages To Compress",
|
|
265
|
+
sections.push("## Messages To Compress", serializeMessagesForCompaction(plan.oldMessages));
|
|
222
266
|
return sections.join("\n");
|
|
223
267
|
}
|
|
224
268
|
|
|
@@ -289,8 +333,22 @@ export class BuiltinContextManager {
|
|
|
289
333
|
const estimated = estimateBuiltinContextTokens(this.state.summary, this.state.messages);
|
|
290
334
|
if (estimated <= this.compactAtTokens) return null;
|
|
291
335
|
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
336
|
+
const messages = this.state.messages;
|
|
337
|
+
if (messages.length <= 1) return null;
|
|
338
|
+
|
|
339
|
+
const earliestAllowed = Math.max(0, messages.length - this.keepRecentMessages);
|
|
340
|
+
const recentTokenBudget = Math.max(1, Math.floor(this.compactAtTokens * RECENT_CONTEXT_BUDGET_RATIO));
|
|
341
|
+
let splitAt = messages.length - 1;
|
|
342
|
+
for (let index = messages.length - 2; index >= earliestAllowed; index -= 1) {
|
|
343
|
+
const candidate = messages.slice(index);
|
|
344
|
+
if (estimateBuiltinContextTokens("", candidate) > recentTokenBudget) break;
|
|
345
|
+
splitAt = index;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// A large existing summary can put the total over budget even when every raw
|
|
349
|
+
// message fits. Always leave at least one old message to re-summarize so the
|
|
350
|
+
// compaction pass can still make progress.
|
|
351
|
+
if (splitAt <= 0) splitAt = 1;
|
|
294
352
|
|
|
295
353
|
return {
|
|
296
354
|
previousSummary: this.state.summary,
|
package/src/builtin/file-log.ts
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* file-log.ts — DeepCCC 文件日志(写入 ~/.deepccc/logs/)
|
|
3
|
-
*
|
|
4
|
-
* 交互渲染模式下渲染器独占终端 stdout:普通 console 日志只写文件、不回显
|
|
5
|
-
* 到终端,避免生成过程中任何 console 输出混入过程区块、破坏行数计数
|
|
6
|
-
* (导致重绘上移不足、把上方历史内容"吃掉")。
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
10
|
-
import { homedir } from "node:os";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
|
|
13
|
-
export function setupFileLogging(logDir: string, prefix: string): { logPath: string } {
|
|
14
|
-
mkdirSync(logDir, { recursive: true });
|
|
15
|
-
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
16
|
-
const logPath = join(logDir, `${prefix}-${ts}.log`);
|
|
17
|
-
appendFileSync(logPath, "", { flag: "a", encoding: "utf8" });
|
|
18
|
-
return { logPath };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** 默认日志目录:~/.deepccc/logs */
|
|
22
|
-
export function defaultLogDir(): string {
|
|
23
|
-
return join(homedir(), ".deepccc", "logs");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function writeLogLine(logPath: string, level: string, args: unknown[]): void {
|
|
27
|
-
try {
|
|
28
|
-
const text = args
|
|
29
|
-
.map((a) =>
|
|
30
|
-
typeof a === "string" ? a
|
|
31
|
-
: a instanceof Error ? (a.stack ?? a.message)
|
|
32
|
-
: JSON.stringify(a))
|
|
33
|
-
.join(" ");
|
|
34
|
-
appendFileSync(logPath, `[${new Date().toISOString()}] [${level}] ${text}\n`, "utf8");
|
|
35
|
-
} catch {
|
|
36
|
-
// 日志系统自身失败不影响主流程
|
|
37
|
-
}
|
|
38
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* file-log.ts — DeepCCC 文件日志(写入 ~/.deepccc/logs/)
|
|
3
|
+
*
|
|
4
|
+
* 交互渲染模式下渲染器独占终端 stdout:普通 console 日志只写文件、不回显
|
|
5
|
+
* 到终端,避免生成过程中任何 console 输出混入过程区块、破坏行数计数
|
|
6
|
+
* (导致重绘上移不足、把上方历史内容"吃掉")。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
export function setupFileLogging(logDir: string, prefix: string): { logPath: string } {
|
|
14
|
+
mkdirSync(logDir, { recursive: true });
|
|
15
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
16
|
+
const logPath = join(logDir, `${prefix}-${ts}.log`);
|
|
17
|
+
appendFileSync(logPath, "", { flag: "a", encoding: "utf8" });
|
|
18
|
+
return { logPath };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 默认日志目录:~/.deepccc/logs */
|
|
22
|
+
export function defaultLogDir(): string {
|
|
23
|
+
return join(homedir(), ".deepccc", "logs");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function writeLogLine(logPath: string, level: string, args: unknown[]): void {
|
|
27
|
+
try {
|
|
28
|
+
const text = args
|
|
29
|
+
.map((a) =>
|
|
30
|
+
typeof a === "string" ? a
|
|
31
|
+
: a instanceof Error ? (a.stack ?? a.message)
|
|
32
|
+
: JSON.stringify(a))
|
|
33
|
+
.join(" ");
|
|
34
|
+
appendFileSync(logPath, `[${new Date().toISOString()}] [${level}] ${text}\n`, "utf8");
|
|
35
|
+
} catch {
|
|
36
|
+
// 日志系统自身失败不影响主流程
|
|
37
|
+
}
|
|
38
|
+
}
|