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.
Files changed (120) hide show
  1. package/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  2. package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  3. package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  4. package/README.md +90 -90
  5. package/agent-prompts/claude_specific.md +45 -45
  6. package/agent-prompts/codex_specific.md +2 -2
  7. package/agent-prompts/cursor_specific.md +13 -13
  8. package/bin/cccagent.mjs +17 -17
  9. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  10. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  11. package/im-skills/feishu-skill/skill.md +3 -3
  12. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  13. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  14. package/im-skills/wechat-file-skill/skill.md +10 -10
  15. package/im-skills/wechat-image-skill/skill.md +10 -10
  16. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  17. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  18. package/im-skills/wechat-video-skill/skill.md +10 -10
  19. package/package.json +1 -1
  20. package/scripts/postinstall-sharp-check.mjs +58 -58
  21. package/src/__tests__/agent-activity.test.ts +86 -76
  22. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  23. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  24. package/src/__tests__/builtin-chat-session.test.ts +65 -1
  25. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  26. package/src/__tests__/builtin-config.test.ts +26 -26
  27. package/src/__tests__/builtin-context.test.ts +202 -163
  28. package/src/__tests__/builtin-permissions.test.ts +211 -211
  29. package/src/__tests__/builtin-session-select.test.ts +116 -116
  30. package/src/__tests__/builtin-sigint.test.ts +56 -56
  31. package/src/__tests__/builtin-web-tools.test.ts +220 -220
  32. package/src/__tests__/card-action-routing.test.ts +18 -18
  33. package/src/__tests__/cardkit.test.ts +60 -60
  34. package/src/__tests__/ccc-adapter.test.ts +170 -136
  35. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  36. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  37. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  38. package/src/__tests__/claude-adapter.test.ts +614 -614
  39. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  40. package/src/__tests__/codex-adapter.test.ts +58 -58
  41. package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
  42. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  43. package/src/__tests__/cursor-adapter.test.ts +268 -268
  44. package/src/__tests__/feishu-api.test.ts +60 -60
  45. package/src/__tests__/feishu-avatar.test.ts +164 -164
  46. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  47. package/src/__tests__/feishu-platform.test.ts +22 -22
  48. package/src/__tests__/format-message.test.ts +47 -47
  49. package/src/__tests__/jsonl-stream.test.ts +79 -79
  50. package/src/__tests__/package-files.test.ts +24 -24
  51. package/src/__tests__/privacy.test.ts +198 -198
  52. package/src/__tests__/progress-reducer.test.ts +121 -110
  53. package/src/__tests__/raw-stream-log.test.ts +106 -106
  54. package/src/__tests__/response-stall.test.ts +49 -49
  55. package/src/__tests__/session.test.ts +174 -16
  56. package/src/__tests__/shared-prefix.test.ts +36 -36
  57. package/src/__tests__/sim-platform.test.ts +16 -16
  58. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  59. package/src/__tests__/stop-session.test.ts +34 -34
  60. package/src/__tests__/stream-state.test.ts +42 -42
  61. package/src/__tests__/terminal-error.test.ts +54 -0
  62. package/src/__tests__/terminal-renderer.test.ts +247 -247
  63. package/src/__tests__/update-command-guard.test.ts +144 -144
  64. package/src/__tests__/web-ui.test.ts +326 -326
  65. package/src/adapters/adapter-interface.ts +24 -18
  66. package/src/adapters/ccc-adapter.ts +141 -131
  67. package/src/adapters/claude-adapter.ts +620 -620
  68. package/src/adapters/claude-session-meta-store.ts +120 -120
  69. package/src/adapters/codex-adapter.ts +426 -426
  70. package/src/adapters/cursor-adapter.ts +681 -681
  71. package/src/adapters/jsonl-stream.ts +157 -157
  72. package/src/adapters/raw-stream-log.ts +124 -124
  73. package/src/adapters/resource-monitor.ts +140 -140
  74. package/src/agent-activity.ts +175 -170
  75. package/src/agent-delegate-task-rpc.ts +153 -153
  76. package/src/agent-delegate-task.ts +91 -91
  77. package/src/agent-reload-config-rpc.ts +34 -34
  78. package/src/agent-stop-stuck.ts +129 -129
  79. package/src/builtin/cli.ts +9 -2
  80. package/src/builtin/config.ts +84 -84
  81. package/src/builtin/context.ts +62 -4
  82. package/src/builtin/file-log.ts +38 -38
  83. package/src/builtin/index.ts +83 -19
  84. package/src/builtin/proc-tree-kill.ts +61 -61
  85. package/src/builtin/progress/cards-helpers.ts +76 -76
  86. package/src/builtin/progress/reducer.ts +113 -108
  87. package/src/builtin/progress/terminal-renderer.ts +294 -294
  88. package/src/builtin/progress/view.ts +77 -77
  89. package/src/builtin/raw-stream-log.ts +124 -124
  90. package/src/builtin/session-select.ts +48 -48
  91. package/src/builtin/sigint.ts +50 -50
  92. package/src/builtin/web-tools.ts +313 -313
  93. package/src/card-action-routing.ts +14 -14
  94. package/src/chatgpt-subscription-rpc.ts +27 -27
  95. package/src/chatgpt-subscription.ts +299 -299
  96. package/src/chrome-devtools-guard.ts +318 -318
  97. package/src/codex-reset-actions.ts +184 -184
  98. package/src/feishu-api.ts +193 -193
  99. package/src/feishu-message-ingress.ts +195 -195
  100. package/src/feishu-platform.ts +20 -20
  101. package/src/format-message.ts +293 -293
  102. package/src/litellm-proxy.ts +374 -374
  103. package/src/orchestrator.ts +8 -3
  104. package/src/platform-adapter.ts +6 -6
  105. package/src/privacy.ts +118 -118
  106. package/src/progress/reducer.ts +113 -108
  107. package/src/progress/terminal-renderer.ts +294 -294
  108. package/src/progress/view.ts +77 -77
  109. package/src/response-stall.ts +28 -28
  110. package/src/runtime-reload.ts +34 -34
  111. package/src/session-chat-binding.ts +82 -82
  112. package/src/session-name.ts +8 -8
  113. package/src/session.ts +149 -26
  114. package/src/shared-prefix.ts +29 -29
  115. package/src/sim-platform.ts +20 -20
  116. package/src/startup-lifecycle.ts +250 -250
  117. package/src/stream-state.ts +21 -18
  118. package/src/terminal-error.ts +129 -0
  119. package/src/turn-cards.ts +117 -117
  120. package/src/update-command-guard.ts +165 -165
@@ -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
+ }
@@ -216,7 +216,12 @@ function writeJsonLine(event: JsonLine): void {
216
216
  }
217
217
 
218
218
  function streamJsonEvent(event: ChatEvent): void {
219
- if (event.type === "text") {
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);
@@ -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();
@@ -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("## Existing Summary", plan.previousSummary.trim(), "");
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", serializeMessagesForSummary(plan.oldMessages));
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 splitAt = this.state.messages.length - this.keepRecentMessages;
293
- if (splitAt <= 0) return null;
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,
@@ -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
+ }