chatccc 0.2.251 → 0.2.253

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 (277) hide show
  1. package/README.md +1 -1
  2. package/bin/cccagent.mjs +12 -3
  3. package/bin/chatccc.mjs +15 -6
  4. package/deepccc-agent/package.json +1 -1
  5. package/dist/deepccc-agent/src/cli.js +634 -0
  6. package/dist/deepccc-agent/src/config.js +76 -0
  7. package/dist/deepccc-agent/src/context.js +348 -0
  8. package/dist/deepccc-agent/src/file-log.js +34 -0
  9. package/dist/deepccc-agent/src/file-tools.js +1208 -0
  10. package/dist/deepccc-agent/src/index.js +571 -0
  11. package/dist/deepccc-agent/src/permissions.js +170 -0
  12. package/dist/deepccc-agent/src/privacy.js +124 -0
  13. package/dist/deepccc-agent/src/proc-tree-kill.js +60 -0
  14. package/dist/deepccc-agent/src/progress/cards-helpers.js +70 -0
  15. package/dist/deepccc-agent/src/progress/reducer.js +102 -0
  16. package/dist/deepccc-agent/src/progress/terminal-renderer.js +264 -0
  17. package/dist/deepccc-agent/src/progress/view.js +30 -0
  18. package/dist/deepccc-agent/src/raw-stream-log.js +106 -0
  19. package/dist/deepccc-agent/src/session-search.js +276 -0
  20. package/dist/deepccc-agent/src/session-select.js +23 -0
  21. package/dist/deepccc-agent/src/sigint.js +26 -0
  22. package/dist/deepccc-agent/src/skills.js +178 -0
  23. package/dist/deepccc-agent/src/web-tools.js +246 -0
  24. package/dist/src/adapters/adapter-interface.js +19 -0
  25. package/dist/src/adapters/ccc-adapter.js +112 -0
  26. package/dist/src/adapters/claude-adapter.js +497 -0
  27. package/dist/src/adapters/claude-session-meta-store.js +92 -0
  28. package/dist/src/adapters/codex-adapter.js +279 -0
  29. package/dist/src/adapters/codex-session-meta-store.js +94 -0
  30. package/dist/src/adapters/cursor-adapter.js +491 -0
  31. package/dist/src/adapters/cursor-session-meta-store.js +116 -0
  32. package/dist/src/adapters/jsonl-stream.js +104 -0
  33. package/{src/adapters/proc-tree-kill.ts → dist/src/adapters/proc-tree-kill.js} +94 -97
  34. package/dist/src/adapters/raw-stream-log.js +106 -0
  35. package/dist/src/adapters/resource-monitor.js +113 -0
  36. package/dist/src/agent-activity.js +133 -0
  37. package/dist/src/agent-delegate-task-rpc.js +129 -0
  38. package/dist/src/agent-delegate-task.js +48 -0
  39. package/dist/src/agent-file-rpc.js +152 -0
  40. package/dist/src/agent-image-rpc.js +148 -0
  41. package/dist/src/agent-platform-routing.js +13 -0
  42. package/dist/src/agent-reload-config-rpc.js +23 -0
  43. package/dist/src/agent-rpc-body.js +87 -0
  44. package/dist/src/agent-stop-stuck.js +110 -0
  45. package/dist/src/card-action-routing.js +7 -0
  46. package/dist/src/card-plain-text.js +101 -0
  47. package/dist/src/cardkit.js +158 -0
  48. package/dist/src/cards.js +573 -0
  49. package/dist/src/chatgpt-subscription-rpc.js +18 -0
  50. package/dist/src/chatgpt-subscription.js +199 -0
  51. package/dist/src/chrome-devtools-guard.js +238 -0
  52. package/dist/src/claude-sdk-installer.js +249 -0
  53. package/dist/src/codex-reset-actions.js +143 -0
  54. package/dist/src/config-utils.js +149 -0
  55. package/dist/src/config.js +804 -0
  56. package/dist/src/cursor-usage.js +77 -0
  57. package/dist/src/exit-banner.js +28 -0
  58. package/dist/src/feishu-api.js +1404 -0
  59. package/dist/src/feishu-message-ingress.js +137 -0
  60. package/dist/src/feishu-platform.js +97 -0
  61. package/dist/src/format-message.js +252 -0
  62. package/dist/src/git-command.js +155 -0
  63. package/dist/src/im-skills.js +121 -0
  64. package/dist/src/index.js +833 -0
  65. package/dist/src/litellm-proxy.js +300 -0
  66. package/dist/src/orchestrator.js +2078 -0
  67. package/dist/src/package-root.js +26 -0
  68. package/dist/src/platform-adapter.js +7 -0
  69. package/dist/src/platform-startup.js +6 -0
  70. package/dist/src/privacy.js +100 -0
  71. package/dist/src/progress/reducer.js +102 -0
  72. package/dist/src/progress/terminal-renderer.js +264 -0
  73. package/dist/src/progress/view.js +30 -0
  74. package/dist/src/response-stall.js +14 -0
  75. package/dist/src/runtime-entry.js +13 -0
  76. package/dist/src/runtime-reload.js +19 -0
  77. package/dist/src/session-chat-binding.js +183 -0
  78. package/dist/src/session-name.js +7 -0
  79. package/dist/src/session.js +2144 -0
  80. package/dist/src/shared-prefix.js +16 -0
  81. package/dist/src/shared.js +493 -0
  82. package/dist/src/sim-agent.js +105 -0
  83. package/dist/src/sim-platform.js +142 -0
  84. package/dist/src/sim-store.js +231 -0
  85. package/dist/src/simplify.js +99 -0
  86. package/dist/src/startup-lifecycle.js +209 -0
  87. package/dist/src/stream-state.js +141 -0
  88. package/dist/src/terminal-error.js +100 -0
  89. package/dist/src/trace.js +50 -0
  90. package/dist/src/turn-cards.js +92 -0
  91. package/dist/src/update-command-guard.js +114 -0
  92. package/{src/web-ui.ts → dist/src/web-ui.js} +749 -823
  93. package/dist/src/wechat-platform.js +545 -0
  94. package/package.json +7 -5
  95. package/deepccc-agent/LICENSE +0 -201
  96. package/deepccc-agent/bin/deepccc.mjs +0 -26
  97. package/deepccc-agent/docs/cache-hit-rate-1.jpg +0 -0
  98. package/deepccc-agent/docs/cache-hit-rate-2.jpg +0 -0
  99. package/deepccc-agent/package-lock.json +0 -2027
  100. package/deepccc-agent/src/__tests__/chat-session.test.ts +0 -852
  101. package/deepccc-agent/src/__tests__/cli-json.test.ts +0 -49
  102. package/deepccc-agent/src/__tests__/config.test.ts +0 -34
  103. package/deepccc-agent/src/__tests__/context.test.ts +0 -341
  104. package/deepccc-agent/src/__tests__/file-tools.test.ts +0 -240
  105. package/deepccc-agent/src/__tests__/permissions.test.ts +0 -199
  106. package/deepccc-agent/src/__tests__/privacy.test.ts +0 -318
  107. package/deepccc-agent/src/__tests__/progress-reducer.test.ts +0 -121
  108. package/deepccc-agent/src/__tests__/session-search.test.ts +0 -262
  109. package/deepccc-agent/src/__tests__/session-select.test.ts +0 -116
  110. package/deepccc-agent/src/__tests__/sigint.test.ts +0 -56
  111. package/deepccc-agent/src/__tests__/skills.test.ts +0 -284
  112. package/deepccc-agent/src/__tests__/terminal-renderer.test.ts +0 -247
  113. package/deepccc-agent/src/__tests__/web-tools.test.ts +0 -220
  114. package/deepccc-agent/src/cli.ts +0 -682
  115. package/deepccc-agent/src/config.ts +0 -101
  116. package/deepccc-agent/src/context.ts +0 -465
  117. package/deepccc-agent/src/file-log.ts +0 -38
  118. package/deepccc-agent/src/file-tools.ts +0 -1493
  119. package/deepccc-agent/src/index.ts +0 -676
  120. package/deepccc-agent/src/permissions.ts +0 -226
  121. package/deepccc-agent/src/privacy.ts +0 -141
  122. package/deepccc-agent/src/proc-tree-kill.ts +0 -61
  123. package/deepccc-agent/src/progress/cards-helpers.ts +0 -76
  124. package/deepccc-agent/src/progress/reducer.ts +0 -113
  125. package/deepccc-agent/src/progress/terminal-renderer.ts +0 -294
  126. package/deepccc-agent/src/progress/view.ts +0 -77
  127. package/deepccc-agent/src/raw-stream-log.ts +0 -124
  128. package/deepccc-agent/src/session-search.ts +0 -370
  129. package/deepccc-agent/src/session-select.ts +0 -48
  130. package/deepccc-agent/src/sigint.ts +0 -50
  131. package/deepccc-agent/src/skills.ts +0 -205
  132. package/deepccc-agent/src/web-tools.ts +0 -313
  133. package/deepccc-agent/tsconfig.build.json +0 -13
  134. package/deepccc-agent/tsconfig.json +0 -13
  135. package/deepccc-agent/vitest.config.ts +0 -7
  136. package/src/__tests__/adapter-interface.test.ts +0 -152
  137. package/src/__tests__/agent-activity.test.ts +0 -86
  138. package/src/__tests__/agent-delegate-task-rpc.test.ts +0 -165
  139. package/src/__tests__/agent-image-rpc.test.ts +0 -34
  140. package/src/__tests__/agent-platform-routing.test.ts +0 -26
  141. package/src/__tests__/agent-reload-config-rpc.test.ts +0 -99
  142. package/src/__tests__/agent-rpc-body.test.ts +0 -42
  143. package/src/__tests__/builtin-chat-session.test.ts +0 -532
  144. package/src/__tests__/builtin-cli-json.test.ts +0 -39
  145. package/src/__tests__/builtin-config.test.ts +0 -26
  146. package/src/__tests__/builtin-context.test.ts +0 -319
  147. package/src/__tests__/builtin-file-tools.test.ts +0 -240
  148. package/src/__tests__/builtin-permissions.test.ts +0 -219
  149. package/src/__tests__/builtin-session-search.test.ts +0 -262
  150. package/src/__tests__/builtin-session-select.test.ts +0 -116
  151. package/src/__tests__/builtin-sigint.test.ts +0 -56
  152. package/src/__tests__/builtin-skills.test.ts +0 -284
  153. package/src/__tests__/builtin-web-tools.test.ts +0 -220
  154. package/src/__tests__/card-action-routing.test.ts +0 -18
  155. package/src/__tests__/card-plain-text.test.ts +0 -45
  156. package/src/__tests__/cardkit.test.ts +0 -60
  157. package/src/__tests__/cards.test.ts +0 -607
  158. package/src/__tests__/ccc-adapter.test.ts +0 -194
  159. package/src/__tests__/chatgpt-subscription-rpc.test.ts +0 -89
  160. package/src/__tests__/chatgpt-subscription.test.ts +0 -135
  161. package/src/__tests__/chrome-devtools-guard.test.ts +0 -165
  162. package/src/__tests__/claude-adapter.test.ts +0 -614
  163. package/src/__tests__/claude-raw-stream-log.test.ts +0 -96
  164. package/src/__tests__/claude-sdk-installer.test.ts +0 -285
  165. package/src/__tests__/codex-adapter.test.ts +0 -331
  166. package/src/__tests__/codex-raw-stream-log.test.ts +0 -170
  167. package/src/__tests__/codex-reset-actions.test.ts +0 -146
  168. package/src/__tests__/config-reload.test.ts +0 -284
  169. package/src/__tests__/config-sample.test.ts +0 -97
  170. package/src/__tests__/config-utils.test.ts +0 -40
  171. package/src/__tests__/config.test.ts +0 -395
  172. package/src/__tests__/crash-logging.test.ts +0 -360
  173. package/src/__tests__/cursor-adapter.test.ts +0 -890
  174. package/src/__tests__/cursor-session-meta-store.test.ts +0 -212
  175. package/src/__tests__/feishu-api.test.ts +0 -60
  176. package/src/__tests__/feishu-avatar.test.ts +0 -504
  177. package/src/__tests__/feishu-message-ingress.test.ts +0 -138
  178. package/src/__tests__/feishu-platform.test.ts +0 -75
  179. package/src/__tests__/fixtures/codex_simple_text.jsonl +0 -4
  180. package/src/__tests__/fixtures/codex_with_tool.jsonl +0 -6
  181. package/src/__tests__/fixtures/cursor_partial_only.jsonl +0 -5
  182. package/src/__tests__/fixtures/cursor_partial_with_final.jsonl +0 -13
  183. package/src/__tests__/fixtures/cursor_with_tool_call.jsonl +0 -12
  184. package/src/__tests__/format-message.test.ts +0 -316
  185. package/src/__tests__/git-command.test.ts +0 -288
  186. package/src/__tests__/im-skills.test.ts +0 -125
  187. package/src/__tests__/jsonl-stream.test.ts +0 -79
  188. package/src/__tests__/orchestrator.test.ts +0 -1268
  189. package/src/__tests__/package-files.test.ts +0 -24
  190. package/src/__tests__/platform-startup.test.ts +0 -19
  191. package/src/__tests__/privacy.test.ts +0 -198
  192. package/src/__tests__/proc-tree-kill.test.ts +0 -108
  193. package/src/__tests__/progress-reducer.test.ts +0 -121
  194. package/src/__tests__/raw-stream-log.test.ts +0 -106
  195. package/src/__tests__/response-stall.test.ts +0 -49
  196. package/src/__tests__/restart.test.ts +0 -232
  197. package/src/__tests__/session-ccc-config.test.ts +0 -66
  198. package/src/__tests__/session.test.ts +0 -3004
  199. package/src/__tests__/shared-prefix.test.ts +0 -36
  200. package/src/__tests__/sim-agent.test.ts +0 -174
  201. package/src/__tests__/sim-platform.test.ts +0 -93
  202. package/src/__tests__/sim-store.test.ts +0 -214
  203. package/src/__tests__/simplify.test.ts +0 -283
  204. package/src/__tests__/startup-lifecycle.test.ts +0 -231
  205. package/src/__tests__/stop-session.test.ts +0 -162
  206. package/src/__tests__/stream-state.test.ts +0 -164
  207. package/src/__tests__/terminal-error.test.ts +0 -54
  208. package/src/__tests__/terminal-renderer.test.ts +0 -247
  209. package/src/__tests__/update-command-guard.test.ts +0 -144
  210. package/src/__tests__/web-ui.test.ts +0 -438
  211. package/src/__tests__/wechat-platform.test.ts +0 -111
  212. package/src/adapters/adapter-interface.ts +0 -217
  213. package/src/adapters/ccc-adapter.ts +0 -150
  214. package/src/adapters/claude-adapter.ts +0 -673
  215. package/src/adapters/claude-session-meta-store.ts +0 -120
  216. package/src/adapters/codex-adapter.ts +0 -426
  217. package/src/adapters/codex-session-meta-store.ts +0 -131
  218. package/src/adapters/cursor-adapter.ts +0 -681
  219. package/src/adapters/cursor-session-meta-store.ts +0 -154
  220. package/src/adapters/jsonl-stream.ts +0 -157
  221. package/src/adapters/raw-stream-log.ts +0 -124
  222. package/src/adapters/resource-monitor.ts +0 -141
  223. package/src/agent-activity.ts +0 -175
  224. package/src/agent-delegate-task-rpc.ts +0 -153
  225. package/src/agent-delegate-task.ts +0 -91
  226. package/src/agent-file-rpc.ts +0 -172
  227. package/src/agent-image-rpc.ts +0 -168
  228. package/src/agent-platform-routing.ts +0 -28
  229. package/src/agent-reload-config-rpc.ts +0 -34
  230. package/src/agent-rpc-body.ts +0 -92
  231. package/src/agent-stop-stuck.ts +0 -129
  232. package/src/card-action-routing.ts +0 -14
  233. package/src/card-plain-text.ts +0 -108
  234. package/src/cardkit.ts +0 -179
  235. package/src/cards.ts +0 -684
  236. package/src/chatgpt-subscription-rpc.ts +0 -27
  237. package/src/chatgpt-subscription.ts +0 -299
  238. package/src/chrome-devtools-guard.ts +0 -318
  239. package/src/claude-sdk-installer.ts +0 -324
  240. package/src/codex-reset-actions.ts +0 -184
  241. package/src/config-utils.ts +0 -211
  242. package/src/config.ts +0 -1063
  243. package/src/cursor-usage.ts +0 -128
  244. package/src/exit-banner.ts +0 -33
  245. package/src/feishu-api.ts +0 -1616
  246. package/src/feishu-message-ingress.ts +0 -195
  247. package/src/feishu-platform.ts +0 -159
  248. package/src/format-message.ts +0 -293
  249. package/src/git-command.ts +0 -202
  250. package/src/im-skills.ts +0 -149
  251. package/src/index.ts +0 -1089
  252. package/src/litellm-proxy.ts +0 -374
  253. package/src/orchestrator.ts +0 -2543
  254. package/src/platform-adapter.ts +0 -70
  255. package/src/platform-startup.ts +0 -16
  256. package/src/privacy.ts +0 -118
  257. package/src/progress/reducer.ts +0 -113
  258. package/src/progress/terminal-renderer.ts +0 -294
  259. package/src/progress/view.ts +0 -77
  260. package/src/response-stall.ts +0 -28
  261. package/src/runtime-reload.ts +0 -34
  262. package/src/session-chat-binding.ts +0 -292
  263. package/src/session-name.ts +0 -8
  264. package/src/session.ts +0 -2659
  265. package/src/shared-prefix.ts +0 -29
  266. package/src/shared.ts +0 -552
  267. package/src/sim-agent.ts +0 -167
  268. package/src/sim-platform.ts +0 -177
  269. package/src/sim-store.ts +0 -317
  270. package/src/simplify.ts +0 -120
  271. package/src/startup-lifecycle.ts +0 -250
  272. package/src/stream-state.ts +0 -177
  273. package/src/terminal-error.ts +0 -129
  274. package/src/trace.ts +0 -51
  275. package/src/turn-cards.ts +0 -118
  276. package/src/update-command-guard.ts +0 -165
  277. package/src/wechat-platform.ts +0 -680
@@ -0,0 +1,804 @@
1
+ import { existsSync, readFileSync, copyFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
7
+ import { CHATCCC_PACKAGE_ROOT } from "./package-root.js";
8
+ import { printServiceDidNotStart } from "./exit-banner.js";
9
+ import { appendStartupTrace, setupFileLogging } from "./shared.js";
10
+ import { anthropicConfigDisplay, autoDetectCodexPath, autoDetectCursorPath, normalizeCccProviderOverride, normalizeOptionalConfigField, readToolCliPath, resolveCccEnabled, } from "./config-utils.js";
11
+ // 重新导出 config-utils 中的纯函数/常量,保持对外 API 不变
12
+ // (历史上这些符号都从 ./config.ts 导入;新代码可直接从 ./config-utils.ts 导入以避免触发本文件的副作用)
13
+ export { DEFAULT_GIT_TIMEOUT_SECONDS, MIN_GIT_TIMEOUT_SECONDS, MAX_GIT_TIMEOUT_SECONDS, parseGitTimeoutSeconds, normalizeOptionalConfigField, isAnthropicConfigEmpty, anthropicConfigDisplay, resolveCccEnabled, } from "./config-utils.js";
14
+ // ---------------------------------------------------------------------------
15
+ // Paths & logging
16
+ // ---------------------------------------------------------------------------
17
+ export const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ export const PROJECT_ROOT = CHATCCC_PACKAGE_ROOT;
19
+ /** 用户持久化数据根目录(不随 npm 升级清空) */
20
+ export const USER_DATA_DIR = join(homedir(), ".chatccc");
21
+ export const PID_FILE = join(USER_DATA_DIR, "state", "runtime.pid");
22
+ export const LOG_DIR = join(USER_DATA_DIR, "logs");
23
+ export const fileLog = setupFileLogging(LOG_DIR, "index");
24
+ export const CHAT_LOGS_DIR = join(USER_DATA_DIR, "state", "chat_logs");
25
+ export const RAW_STREAM_LOGS_DIR = join(LOG_DIR, "raw-streams");
26
+ export async function appendChatLog(chatId, sender, text) {
27
+ try {
28
+ await mkdir(CHAT_LOGS_DIR, { recursive: true });
29
+ const line = JSON.stringify({ ts: Date.now(), sender, text: text.slice(0, 200) }) + "\n";
30
+ await appendFile(join(CHAT_LOGS_DIR, `${chatId}.jsonl`), line);
31
+ }
32
+ catch {
33
+ // 静默失败,不影响主流程
34
+ }
35
+ }
36
+ export const AGENT_TOOLS = ["claude", "cursor", "codex", "ccc"];
37
+ /** 获取指定 agent 配置中所有模型相关的值(最多 100 个,去重) */
38
+ export function getAllModelsForTool(tool, cfg = config) {
39
+ const seen = new Set();
40
+ const collect = (v) => {
41
+ if (typeof v === "string" && v.trim())
42
+ seen.add(v.trim());
43
+ };
44
+ if (tool === "claude") {
45
+ collect(cfg.claude.model);
46
+ collect(cfg.claude.subagentModel);
47
+ }
48
+ else if (tool === "cursor") {
49
+ collect(cfg.cursor.model);
50
+ collect(cfg.cursor.alternativeModel);
51
+ }
52
+ else if (tool === "codex") {
53
+ collect(cfg.codex.model);
54
+ collect(cfg.codex.alternativeModel);
55
+ }
56
+ else if (tool === "ccc") {
57
+ collect(cfg.ccc.model);
58
+ collect(cfg.ccc.alternativeModel);
59
+ }
60
+ return Array.from(seen).slice(0, 100);
61
+ }
62
+ export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
63
+ export const CODEX_EFFORT_LEVELS = ["minimal", "low", "medium", "high", "xhigh"];
64
+ export const CCC_EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh", "max"];
65
+ export function getAllEffortsForTool(tool) {
66
+ if (tool === "claude")
67
+ return [...CLAUDE_EFFORT_LEVELS];
68
+ if (tool === "codex")
69
+ return [...CODEX_EFFORT_LEVELS];
70
+ if (tool === "ccc")
71
+ return [...CCC_EFFORT_LEVELS];
72
+ return [];
73
+ }
74
+ export function getDefaultEffortForTool(tool, cfg = config) {
75
+ if (tool === "claude")
76
+ return cfg.claude.effort;
77
+ if (tool === "codex")
78
+ return cfg.codex.effort;
79
+ if (tool === "ccc")
80
+ return cfg.ccc.effort;
81
+ return "";
82
+ }
83
+ const CONFIG_FILE = join(USER_DATA_DIR, "config.json");
84
+ const CONFIG_SAMPLE_FILE = join(PROJECT_ROOT, "config.sample.json");
85
+ export const DEFAULT_CCC_DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1";
86
+ export const DEFAULT_CCC_MODEL = "deepseek-v4-pro";
87
+ /**
88
+ * 将旧位置(PROJECT_ROOT)的持久化数据一次性迁移到 USER_DATA_DIR。
89
+ * 仅当 USER_DATA_DIR 下没有 config.json 且 PROJECT_ROOT 下有旧数据时才执行。
90
+ */
91
+ function migrateLegacyData() {
92
+ const oldConfig = join(PROJECT_ROOT, "config.json");
93
+ const oldState = join(PROJECT_ROOT, "state");
94
+ const oldLogs = join(PROJECT_ROOT, "logs");
95
+ const oldImagesDownloads = join(PROJECT_ROOT, "images", "downloads");
96
+ if (existsSync(CONFIG_FILE))
97
+ return; // 已迁移过或全新安装
98
+ let migrated = false;
99
+ try {
100
+ mkdirSync(USER_DATA_DIR, { recursive: true });
101
+ if (existsSync(oldConfig)) {
102
+ copyFileSync(oldConfig, CONFIG_FILE);
103
+ console.log(`[MIGRATE] config.json → ${CONFIG_FILE}`);
104
+ migrated = true;
105
+ }
106
+ if (existsSync(oldState)) {
107
+ const destState = join(USER_DATA_DIR, "state");
108
+ if (!existsSync(destState)) {
109
+ // 同步递归复制(cpSync 不可用,改用 copyFileSync 遍历)
110
+ copyDirSync(oldState, destState);
111
+ console.log(`[MIGRATE] state/ → ${destState}`);
112
+ migrated = true;
113
+ }
114
+ }
115
+ if (existsSync(oldLogs)) {
116
+ const destLogs = join(USER_DATA_DIR, "logs");
117
+ if (!existsSync(destLogs)) {
118
+ copyDirSync(oldLogs, destLogs);
119
+ console.log(`[MIGRATE] logs/ → ${destLogs}`);
120
+ migrated = true;
121
+ }
122
+ }
123
+ if (existsSync(oldImagesDownloads)) {
124
+ const destDownloads = join(USER_DATA_DIR, "images", "downloads");
125
+ if (!existsSync(destDownloads)) {
126
+ copyDirSync(oldImagesDownloads, destDownloads);
127
+ console.log(`[MIGRATE] images/downloads/ → ${destDownloads}`);
128
+ migrated = true;
129
+ }
130
+ }
131
+ }
132
+ catch (err) {
133
+ console.error(`[MIGRATE] 迁移失败: ${err.message}`);
134
+ }
135
+ if (migrated) {
136
+ console.log("[MIGRATE] 旧数据已迁移到 ~/.chatccc/,原位置文件保留未删除。");
137
+ }
138
+ }
139
+ /** 递归同步复制目录(仅文件和子目录,不处理符号链接) */
140
+ function copyDirSync(src, dest) {
141
+ mkdirSync(dest, { recursive: true });
142
+ for (const entry of readdirSync(src)) {
143
+ const srcPath = join(src, entry);
144
+ const destPath = join(dest, entry);
145
+ const s = statSync(srcPath);
146
+ if (s.isDirectory()) {
147
+ copyDirSync(srcPath, destPath);
148
+ }
149
+ else if (s.isFile()) {
150
+ copyFileSync(srcPath, destPath);
151
+ }
152
+ }
153
+ }
154
+ /**
155
+ * 是否处于 vitest 测试环境。
156
+ *
157
+ * 由于 `src/config.ts` 在模块顶层立即执行 `loadConfig()`,而很多生产模块
158
+ * (session.ts、adapters/* 等)又会 import config.ts,因此**任何**一个
159
+ * import 了这些生产模块的单测都会间接触发 config.ts 顶层副作用——这是不
160
+ * 应该的:单测不应该改动工作区的文件系统。
161
+ *
162
+ * vitest 在运行测试时会自动设置 `VITEST=true`,据此跳过自动复制 sample 的
163
+ * 写文件副作用即可(顶层依然会正常加载默认值与可能已存在的 config.json)。
164
+ */
165
+ const IS_TEST_ENV = process.env.VITEST === "true" || process.env.NODE_ENV === "test";
166
+ /**
167
+ * Windows 上用 `where`、其它平台用 `which` 查找命令的绝对路径。
168
+ * 命令未安装、命令查找进程出错都视为"找不到",返回 null。
169
+ */
170
+ function whichSync(cmd) {
171
+ try {
172
+ const tool = process.platform === "win32" ? "where" : "which";
173
+ const out = execFileSync(tool, [cmd], {
174
+ stdio: ["ignore", "pipe", "ignore"],
175
+ windowsHide: true,
176
+ timeout: 5000,
177
+ });
178
+ const first = out.toString().split(/\r?\n/)[0]?.trim();
179
+ return first || null;
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ }
185
+ /**
186
+ * 在刚从 config.sample.json 复制出来的 config.json 上立即探测一次 cursor/codex
187
+ * 路径,命中就回写,便于用户无需手动编辑就拿到可运行的默认配置。
188
+ *
189
+ * 仅在"复制 sample 这一刻"调用,已存在的 config.json 不会触发——避免悄悄改写
190
+ * 用户主动留空的字段。
191
+ */
192
+ function autofillToolPathsAfterSampleCopy(configFile) {
193
+ let raw;
194
+ try {
195
+ raw = readFileSync(configFile, "utf-8");
196
+ }
197
+ catch {
198
+ return;
199
+ }
200
+ let parsed;
201
+ try {
202
+ parsed = JSON.parse(raw);
203
+ }
204
+ catch {
205
+ return;
206
+ }
207
+ const cursor = parsed.cursor ?? {};
208
+ const codex = parsed.codex ?? {};
209
+ const cursorEmpty = (typeof cursor.path !== "string" || cursor.path.trim() === "") &&
210
+ (typeof cursor.command !== "string" || cursor.command.trim() === "");
211
+ const codexEmpty = (typeof codex.path !== "string" || codex.path.trim() === "") &&
212
+ (typeof codex.command !== "string" || codex.command.trim() === "");
213
+ let mutated = false;
214
+ if (cursorEmpty) {
215
+ const detected = autoDetectCursorPath({
216
+ platform: process.platform,
217
+ localAppData: process.env.LOCALAPPDATA,
218
+ existsSync,
219
+ whichSync,
220
+ });
221
+ if (detected) {
222
+ parsed.cursor = { ...cursor, path: detected };
223
+ mutated = true;
224
+ console.log(`[CONFIG] 已自动探测 Cursor CLI 路径: ${detected}`);
225
+ }
226
+ else {
227
+ console.log("[CONFIG] 未探测到 Cursor CLI,cursor.path 留空(运行时按 PATH 兜底)。");
228
+ }
229
+ }
230
+ if (codexEmpty) {
231
+ const detected = autoDetectCodexPath({ whichSync });
232
+ if (detected) {
233
+ parsed.codex = { ...codex, path: detected };
234
+ mutated = true;
235
+ console.log(`[CONFIG] 已自动探测 Codex CLI 路径: ${detected}`);
236
+ }
237
+ else {
238
+ console.log("[CONFIG] 未探测到 Codex CLI,codex.path 留空(运行时退回 PATH 中的 codex)。");
239
+ }
240
+ }
241
+ if (mutated) {
242
+ try {
243
+ writeFileSync(configFile, JSON.stringify(parsed, null, 2) + "\n", "utf-8");
244
+ }
245
+ catch (err) {
246
+ console.error(`[CONFIG] 自动探测路径回写 config.json 失败: ${err.message}`);
247
+ }
248
+ }
249
+ }
250
+ function normalizePlatformType(raw) {
251
+ if (raw === "lark")
252
+ return "lark";
253
+ return "feishu";
254
+ }
255
+ function normalizeCursorAvatarBatteryMode(raw) {
256
+ return raw === "onDemandUse" ? "onDemandUse" : "apiPercent";
257
+ }
258
+ function normalizeCursorOnDemandMonthlyBudget(raw) {
259
+ const value = typeof raw === "string" ? Number(raw.trim()) : Number(raw);
260
+ return Number.isFinite(value) && value > 0 ? value : 1000;
261
+ }
262
+ function normalizePositiveInteger(raw, fallback) {
263
+ const value = typeof raw === "string" ? Number(raw.trim()) : Number(raw);
264
+ return Number.isInteger(value) && value > 0 ? value : fallback;
265
+ }
266
+ function normalizeRawStreamAgentLogConfig(raw) {
267
+ const obj = typeof raw === "object" && raw !== null
268
+ ? raw
269
+ : {};
270
+ return {
271
+ enabled: typeof obj.enabled === "boolean" ? obj.enabled : false,
272
+ maxBytesPerTurn: normalizePositiveInteger(obj.maxBytesPerTurn, 50 * 1024 * 1024),
273
+ retentionDays: normalizePositiveInteger(obj.retentionDays, 7),
274
+ keepCompleted: typeof obj.keepCompleted === "boolean" ? obj.keepCompleted : false,
275
+ };
276
+ }
277
+ function loadConfig() {
278
+ const defaults = {
279
+ feishu: { appId: "", appSecret: "" },
280
+ platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
281
+ webUi: { openOnStart: true },
282
+ chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
283
+ port: 18080,
284
+ gitTimeoutSeconds: 180,
285
+ allowInterrupt: false,
286
+ rawStreamLogs: {
287
+ claude: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
288
+ cursor: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
289
+ codex: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
290
+ ccc: { enabled: false, maxBytesPerTurn: 50 * 1024 * 1024, retentionDays: 7, keepCompleted: false },
291
+ },
292
+ claude: { enabled: false, defaultAgent: true, model: "", subagentModel: "", effort: "", apiKey: "", baseUrl: "", maxTurn: 0 },
293
+ cursor: {
294
+ enabled: false,
295
+ defaultAgent: false,
296
+ path: "",
297
+ model: "claude-opus-4-7-max",
298
+ alternativeModel: "",
299
+ avatarBatteryMode: "apiPercent",
300
+ onDemandMonthlyBudget: 1000,
301
+ },
302
+ codex: { enabled: false, defaultAgent: false, path: "", model: "", alternativeModel: "", effort: "", fastMode: false },
303
+ ccc: {
304
+ enabled: false,
305
+ defaultAgent: false,
306
+ DEEPSEEK_API_KEY: "",
307
+ DEEPSEEK_BASE_URL: DEFAULT_CCC_DEEPSEEK_BASE_URL,
308
+ model: DEFAULT_CCC_MODEL,
309
+ alternativeModel: "",
310
+ effort: "",
311
+ provider: "",
312
+ },
313
+ };
314
+ if (!IS_TEST_ENV) {
315
+ migrateLegacyData();
316
+ }
317
+ if (!existsSync(CONFIG_FILE)) {
318
+ if (IS_TEST_ENV) {
319
+ // 测试环境下绝不写文件,直接走默认值
320
+ return defaults;
321
+ }
322
+ if (existsSync(CONFIG_SAMPLE_FILE)) {
323
+ console.log(`[CONFIG] config.json 不存在,基于 config.sample.json 创建...`);
324
+ try {
325
+ mkdirSync(dirname(CONFIG_FILE), { recursive: true });
326
+ copyFileSync(CONFIG_SAMPLE_FILE, CONFIG_FILE);
327
+ }
328
+ catch (err) {
329
+ console.error(`[CONFIG] 无法从 config.sample.json 创建 config.json: ${err.message}`);
330
+ return defaults;
331
+ }
332
+ // 复制完成立即探测 CLI 路径并回写,让用户开箱即用而无须手编 config.json。
333
+ autofillToolPathsAfterSampleCopy(CONFIG_FILE);
334
+ }
335
+ else {
336
+ console.error(`[CONFIG] config.json 和 config.sample.json 都不存在,使用默认配置。`);
337
+ return defaults;
338
+ }
339
+ }
340
+ let raw;
341
+ try {
342
+ raw = readFileSync(CONFIG_FILE, "utf-8");
343
+ // 移除可能意外写入的 UTF-8 BOM(如通过记事本编辑等场景),
344
+ // 避免 JSON.parse 因 BOM 失败导致返回空默认值、丢失所有配置。
345
+ if (raw.charCodeAt(0) === 0xfeff)
346
+ raw = raw.slice(1);
347
+ }
348
+ catch (err) {
349
+ console.error(`[CONFIG] 无法读取 config.json: ${err.message}`);
350
+ return defaults;
351
+ }
352
+ let parsed;
353
+ try {
354
+ parsed = JSON.parse(raw);
355
+ }
356
+ catch (err) {
357
+ console.error(`[CONFIG] config.json 不是合法 JSON: ${err.message}`);
358
+ return defaults;
359
+ }
360
+ const feishu = parsed.feishu ?? { appId: "", appSecret: "" };
361
+ const claude = parsed.claude ?? {};
362
+ const cursorRaw = (parsed.cursor ?? {});
363
+ const codexRaw = (parsed.codex ?? {});
364
+ const cccRaw = (parsed.ccc ?? {});
365
+ const webUiRaw = (parsed.webUi ?? {});
366
+ const chromeDevtoolsRaw = (parsed.chromeDevtools ?? {});
367
+ const rawStreamLogsRaw = typeof parsed.rawStreamLogs === "object" && parsed.rawStreamLogs !== null
368
+ ? parsed.rawStreamLogs
369
+ : {};
370
+ // 兼容旧字段 `command`:命中时打印一次性 warning 提示用户改名
371
+ const onLegacyField = (label, value) => {
372
+ console.warn(`[CONFIG] ${label}.command 字段已废弃,请改为 ${label}.path(当前仍按旧字段读到 "${value}")。`);
373
+ };
374
+ /**
375
+ * 解析 `<agent>.enabled` 字段:
376
+ * - 显式 boolean → 用原值
377
+ * - 缺省 / 其它类型 → 按 `nonEmptyFn()` 推断("有任意字段非空" 即视为启用),向后兼容旧 config.json
378
+ */
379
+ const resolveEnabled = (raw, nonEmptyFn) => {
380
+ if (typeof raw === "boolean")
381
+ return raw;
382
+ return nonEmptyFn();
383
+ };
384
+ const claudeNonEmpty = () => Boolean((typeof claude.model === "string" && claude.model.trim()) ||
385
+ (typeof claude.subagentModel === "string" && claude.subagentModel.trim()) ||
386
+ (typeof claude.effort === "string" && claude.effort.trim()) ||
387
+ (typeof claude.apiKey === "string" && claude.apiKey.trim()) ||
388
+ (typeof claude.baseUrl === "string" && claude.baseUrl.trim()));
389
+ const cursorNonEmpty = () => Boolean((typeof cursorRaw.path === "string" && cursorRaw.path.trim()) ||
390
+ (typeof cursorRaw.command === "string" && cursorRaw.command.trim()) ||
391
+ (typeof cursorRaw.model === "string" && cursorRaw.model.trim()) ||
392
+ (typeof cursorRaw.alternativeModel === "string" && cursorRaw.alternativeModel.trim()));
393
+ const codexNonEmpty = () => Boolean((typeof codexRaw.path === "string" && codexRaw.path.trim()) ||
394
+ (typeof codexRaw.command === "string" && codexRaw.command.trim()) ||
395
+ (typeof codexRaw.model === "string" && codexRaw.model.trim()) ||
396
+ (typeof codexRaw.alternativeModel === "string" && codexRaw.alternativeModel.trim()) ||
397
+ (typeof codexRaw.effort === "string" && codexRaw.effort.trim()) ||
398
+ codexRaw.fastMode === true);
399
+ // 旧版 ccc 配置没有 enabled。只用 API Key 推断启用,避免 sample 中自带的
400
+ // 默认 Base URL / model 让升级用户在未配置凭证时意外启用 CCC Agent。
401
+ const claudeEnabled = resolveEnabled(claude.enabled, claudeNonEmpty);
402
+ const cursorEnabled = resolveEnabled(cursorRaw.enabled, cursorNonEmpty);
403
+ const codexEnabled = resolveEnabled(codexRaw.enabled, codexNonEmpty);
404
+ const cccEnabled = resolveCccEnabled(cccRaw.enabled, cccRaw.DEEPSEEK_API_KEY);
405
+ const chromeDevtoolsPort = Number(chromeDevtoolsRaw.port);
406
+ const explicitDefaultTool = typeof claude.defaultAgent === "boolean" && claude.defaultAgent && claudeEnabled ? "claude" :
407
+ typeof cursorRaw.defaultAgent === "boolean" && cursorRaw.defaultAgent && cursorEnabled ? "cursor" :
408
+ typeof codexRaw.defaultAgent === "boolean" && codexRaw.defaultAgent && codexEnabled ? "codex" :
409
+ typeof cccRaw.defaultAgent === "boolean" && cccRaw.defaultAgent && cccEnabled ? "ccc" :
410
+ null;
411
+ const fallbackDefaultTool = claudeEnabled ? "claude" :
412
+ cursorEnabled ? "cursor" :
413
+ codexEnabled ? "codex" :
414
+ cccEnabled ? "ccc" :
415
+ "claude";
416
+ const defaultTool = explicitDefaultTool ?? fallbackDefaultTool;
417
+ return {
418
+ feishu: {
419
+ appId: feishu.appId ?? "",
420
+ appSecret: feishu.appSecret ?? "",
421
+ },
422
+ platforms: {
423
+ feishu: {
424
+ enabled: typeof parsed.platforms?.feishu === "object"
425
+ ? Boolean(parsed.platforms.feishu.enabled ?? true)
426
+ : true,
427
+ platformType: normalizePlatformType(typeof parsed.platforms?.feishu === "object"
428
+ ? String(parsed.platforms.feishu.platformType ?? "feishu")
429
+ : "feishu"),
430
+ },
431
+ ilink: {
432
+ enabled: typeof parsed.platforms?.ilink === "object"
433
+ ? Boolean(parsed.platforms.ilink.enabled ?? true)
434
+ : true,
435
+ reuseTokenOnStart: typeof parsed.platforms?.ilink === "object"
436
+ ? Boolean(parsed.platforms.ilink.reuseTokenOnStart ?? true)
437
+ : true,
438
+ },
439
+ },
440
+ webUi: {
441
+ // 兼容升级前没有 webUi 字段的 config.json:缺省仍按“自动打开”处理。
442
+ openOnStart: typeof webUiRaw.openOnStart === "boolean" ? webUiRaw.openOnStart : true,
443
+ },
444
+ chromeDevtools: {
445
+ enabled: typeof chromeDevtoolsRaw.enabled === "boolean" ? chromeDevtoolsRaw.enabled : false,
446
+ port: Number.isInteger(chromeDevtoolsPort) && chromeDevtoolsPort >= 1 && chromeDevtoolsPort <= 65535
447
+ ? chromeDevtoolsPort
448
+ : 15166,
449
+ chromePath: normalizeOptionalConfigField(chromeDevtoolsRaw.chromePath, { label: "chromeDevtools.chromePath" }),
450
+ },
451
+ port: typeof parsed.port === "number" ? parsed.port : 18080,
452
+ gitTimeoutSeconds: typeof parsed.gitTimeoutSeconds === "number" ? parsed.gitTimeoutSeconds : 180,
453
+ allowInterrupt: typeof parsed.allowInterrupt === "boolean" ? parsed.allowInterrupt : false,
454
+ rawStreamLogs: {
455
+ claude: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.claude),
456
+ cursor: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.cursor),
457
+ codex: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.codex),
458
+ ccc: normalizeRawStreamAgentLogConfig(rawStreamLogsRaw.ccc),
459
+ },
460
+ claude: {
461
+ enabled: claudeEnabled,
462
+ defaultAgent: defaultTool === "claude",
463
+ model: normalizeOptionalConfigField(claude.model, { label: "claude.model" }),
464
+ subagentModel: normalizeOptionalConfigField(claude.subagentModel, { label: "claude.subagentModel" }),
465
+ effort: normalizeOptionalConfigField(claude.effort, { label: "claude.effort" }),
466
+ apiKey: normalizeOptionalConfigField(claude.apiKey, { label: "claude.apiKey" }),
467
+ baseUrl: normalizeOptionalConfigField(claude.baseUrl, { label: "claude.baseUrl" }),
468
+ maxTurn: typeof claude.maxTurn === "number"
469
+ ? claude.maxTurn
470
+ : 0,
471
+ },
472
+ cursor: {
473
+ enabled: cursorEnabled,
474
+ defaultAgent: defaultTool === "cursor",
475
+ path: readToolCliPath(cursorRaw, { label: "cursor", onLegacyField }),
476
+ model: normalizeOptionalConfigField(cursorRaw.model, { label: "cursor.model", fallback: "claude-opus-4-7-max" }),
477
+ alternativeModel: normalizeOptionalConfigField(cursorRaw.alternativeModel, { label: "cursor.alternativeModel" }),
478
+ avatarBatteryMode: normalizeCursorAvatarBatteryMode(cursorRaw.avatarBatteryMode),
479
+ onDemandMonthlyBudget: normalizeCursorOnDemandMonthlyBudget(cursorRaw.onDemandMonthlyBudget),
480
+ },
481
+ codex: {
482
+ enabled: codexEnabled,
483
+ defaultAgent: defaultTool === "codex",
484
+ path: readToolCliPath(codexRaw, { label: "codex", onLegacyField }),
485
+ model: normalizeOptionalConfigField(codexRaw.model, { label: "codex.model" }),
486
+ alternativeModel: normalizeOptionalConfigField(codexRaw.alternativeModel, { label: "codex.alternativeModel" }),
487
+ effort: normalizeOptionalConfigField(codexRaw.effort, { label: "codex.effort" }),
488
+ fastMode: codexRaw.fastMode === true,
489
+ },
490
+ ccc: {
491
+ enabled: cccEnabled,
492
+ defaultAgent: defaultTool === "ccc",
493
+ DEEPSEEK_API_KEY: normalizeOptionalConfigField(cccRaw.DEEPSEEK_API_KEY, { label: "ccc.DEEPSEEK_API_KEY" }),
494
+ DEEPSEEK_BASE_URL: normalizeOptionalConfigField(cccRaw.DEEPSEEK_BASE_URL, {
495
+ label: "ccc.DEEPSEEK_BASE_URL",
496
+ fallback: DEFAULT_CCC_DEEPSEEK_BASE_URL,
497
+ }),
498
+ model: normalizeOptionalConfigField(cccRaw.model, { label: "ccc.model", fallback: DEFAULT_CCC_MODEL }),
499
+ alternativeModel: normalizeOptionalConfigField(cccRaw.alternativeModel, { label: "ccc.alternativeModel" }),
500
+ effort: normalizeOptionalConfigField(cccRaw.effort, { label: "ccc.effort" }),
501
+ provider: normalizeCccProviderOverride(cccRaw.provider),
502
+ },
503
+ };
504
+ }
505
+ /**
506
+ * 全局可变 config 对象。
507
+ *
508
+ * 故意用 `const + Object.assign(config, ...)` 而非 `let config = ...`:
509
+ * 这样 `config` 这个 binding 永远指向同一个引用,下游模块只要在**函数体内**
510
+ * 读 `config.xxx` 就能自动看到最新值(如 codex-adapter.ts 的 config.codex.*)。
511
+ * 这是 setup → service「在线切换」复用 config 的核心机制。
512
+ */
513
+ export const config = loadConfig();
514
+ // ---------------------------------------------------------------------------
515
+ // Re-exported config values
516
+ // ---------------------------------------------------------------------------
517
+ //
518
+ // 这些值用 `export let` 而非 `const`,是为了支持 `reloadConfigFromDisk()`:
519
+ // setup → service「在线切换」时,向导刚把新值写入 config.json,需要让进程
520
+ // 内的这些常量也跟着更新。ES module 的 live binding 保证:导入端通过命名导入
521
+ // 拿到的是 module 内的 slot,slot 在导出方被重新赋值后,导入端**自动看到新值**
522
+ // (前提是导入端在函数体内读,不是在模块顶层读)。
523
+ export const USE_LOCAL = process.argv.includes("--local");
524
+ export const USE_SIMULATE = process.argv.includes("--simulate");
525
+ export let APP_ID = config.feishu.appId;
526
+ export let APP_SECRET = config.feishu.appSecret;
527
+ export let FEISHU_ENABLED = config.platforms.feishu.enabled;
528
+ export let ILINK_ENABLED = config.platforms.ilink.enabled;
529
+ export let ILINK_REUSE_TOKEN_ON_START = config.platforms.ilink.reuseTokenOnStart ?? true;
530
+ function computeBaseUrl(platformType) {
531
+ if (platformType === "lark")
532
+ return "https://open.larksuite.com/open-apis";
533
+ return "https://open.feishu.cn/open-apis";
534
+ }
535
+ export let BASE_URL = computeBaseUrl(config.platforms.feishu.platformType);
536
+ export let FEISHU_PLATFORM_TYPE = config.platforms.feishu.platformType === "lark" ? "lark" : "feishu";
537
+ export const CHATCCC_PORT = config.port;
538
+ /** 与 CHATCCC_PORT 一致,供 --local 连接本机中继 */
539
+ export const LOCAL_RELAY_URL = `ws://127.0.0.1:${CHATCCC_PORT}`;
540
+ export let CLAUDE_MODEL = config.claude.model;
541
+ export let CLAUDE_SUBAGENT_MODEL = config.claude.subagentModel;
542
+ export let CLAUDE_EFFORT = config.claude.effort;
543
+ export let CLAUDE_MAX_TURN = config.claude.maxTurn;
544
+ export let CLAUDE_API_KEY = config.claude.apiKey;
545
+ export let CLAUDE_BASE_URL = config.claude.baseUrl;
546
+ /** 返回当前生效的 Claude 模型(per-session 覆盖由 session.ts 管理,此处仅返回全局配置) */
547
+ export function getEffectiveClaudeModel() {
548
+ return CLAUDE_MODEL;
549
+ }
550
+ // ---------------------------------------------------------------------------
551
+ // /git 超时配置(实际值来自 config.json,纯函数与常量见 ./config-utils.ts)
552
+ // ---------------------------------------------------------------------------
553
+ export let GIT_TIMEOUT_SECONDS = config.gitTimeoutSeconds;
554
+ export let GIT_TIMEOUT_MS = GIT_TIMEOUT_SECONDS * 1000;
555
+ export let ALLOW_INTERRUPT = config.allowInterrupt;
556
+ /** 探测 cursor-agent 安装路径(优先配置,其次 LocalAppData,最后默认 agent) */
557
+ function detectCursorAgent() {
558
+ if (config.cursor.path)
559
+ return config.cursor.path;
560
+ const localAppData = process.env.LOCALAPPDATA;
561
+ if (localAppData) {
562
+ const defaultPath = join(localAppData, "cursor-agent", "agent.cmd");
563
+ if (existsSync(defaultPath))
564
+ return defaultPath;
565
+ }
566
+ return "agent";
567
+ }
568
+ /**
569
+ * spawn 第一参数:要启动的 Cursor Agent 可执行文件路径。
570
+ * 命名沿用 Node.js spawn(command, args) 的"command"语义,
571
+ * 与 config.json 的 `cursor.path` 字段不冲突。
572
+ */
573
+ export let CURSOR_AGENT_COMMAND = detectCursorAgent();
574
+ function resolveCursorAgentArgs() {
575
+ let args = "-p --force --approve-mcps --output-format stream-json --stream-partial-output";
576
+ const model = config.cursor.model;
577
+ if (model.trim() !== "") {
578
+ args += ` --model ${model}`;
579
+ }
580
+ return args.split(/\s+/).filter(Boolean);
581
+ }
582
+ /** Cursor agent 参数:-p 非交互模式,--force 强制允许命令,--approve-mcps 自动批准 MCP,stream-json 流式 JSONL 输出 */
583
+ export let CURSOR_AGENT_ARGS = resolveCursorAgentArgs();
584
+ // ---------------------------------------------------------------------------
585
+ // reloadConfigFromDisk — setup → service「在线切换」时刷新进程内 config
586
+ // ---------------------------------------------------------------------------
587
+ //
588
+ // 触发场景:setup 模式下用户在向导里填好凭证、刚把 config.json 写入磁盘,
589
+ // 紧接着要在**同一个进程**里启动飞书 service。如果不调用本函数,进程内的
590
+ // APP_ID / APP_SECRET 还是 chatccc 启动时(凭证空)的旧值,飞书 API 调用必失败。
591
+ //
592
+ // 故意不重新触发 sample 复制 / cursor 自动探测等副作用:
593
+ // reload 是「读取最新磁盘配置同步到内存」,不应该再写文件。
594
+ //
595
+ // 注意:CHATCCC_PORT / LOCAL_RELAY_URL 不重新赋值——setup HTTP server 已经
596
+ // 监听在原端口上,原地切换复用同一个 server,重新读端口只会引入混乱。
597
+ /**
598
+ * 把已加载好的 AppConfig 赋值到 module-level export let 常量里。
599
+ *
600
+ * 拆出独立函数(不直接 inline 进 reloadConfigFromDisk)的目的:
601
+ * 1. 让测试可以用任意 AppConfig 验证"赋值映射"正确性,无需碰文件系统
602
+ * 2. 把"读盘"和"赋值"两个职责分开,便于将来支持其它 config 来源
603
+ */
604
+ export function applyLoadedConfig(next) {
605
+ // 就地更新 config 对象:保留原引用,让 codex-adapter 等"直接 import config"
606
+ // 的下游模块在下次访问 config.codex.* 时就能拿到新值。
607
+ Object.assign(config, next);
608
+ APP_ID = next.feishu.appId;
609
+ APP_SECRET = next.feishu.appSecret;
610
+ FEISHU_ENABLED = next.platforms.feishu.enabled;
611
+ ILINK_ENABLED = next.platforms.ilink.enabled;
612
+ ILINK_REUSE_TOKEN_ON_START = next.platforms.ilink.reuseTokenOnStart ?? true;
613
+ FEISHU_PLATFORM_TYPE = next.platforms.feishu.platformType === "lark" ? "lark" : "feishu";
614
+ BASE_URL = computeBaseUrl(FEISHU_PLATFORM_TYPE);
615
+ CLAUDE_MODEL = next.claude.model;
616
+ CLAUDE_SUBAGENT_MODEL = next.claude.subagentModel;
617
+ CLAUDE_EFFORT = next.claude.effort;
618
+ CLAUDE_MAX_TURN = next.claude.maxTurn;
619
+ CLAUDE_API_KEY = next.claude.apiKey;
620
+ CLAUDE_BASE_URL = next.claude.baseUrl;
621
+ GIT_TIMEOUT_SECONDS = next.gitTimeoutSeconds;
622
+ GIT_TIMEOUT_MS = GIT_TIMEOUT_SECONDS * 1000;
623
+ ALLOW_INTERRUPT = next.allowInterrupt;
624
+ CURSOR_AGENT_COMMAND = detectCursorAgent();
625
+ CURSOR_AGENT_ARGS = resolveCursorAgentArgs();
626
+ }
627
+ export function reloadConfigFromDisk() {
628
+ applyLoadedConfig(loadConfig());
629
+ }
630
+ // 新建会话的默认工作路径(/cd 命令设置,按 chatId 持久化到本地文件)
631
+ // 该路径仅影响通过 /new 新建的会话,不影响已有会话的 resume。
632
+ export function getDefaultCwdFile(chatId) {
633
+ return join(USER_DATA_DIR, "state", `working_dir_${chatId}.txt`);
634
+ }
635
+ /** 会话工具类型持久化文件 */
636
+ export const SESSIONS_FILE = join(USER_DATA_DIR, "state", "sessions.json");
637
+ /** 最近成功新建会话的工作路径记录(最多 10 条) */
638
+ export const RECENT_DIRS_FILE = join(USER_DATA_DIR, "state", "recent_dirs.json");
639
+ export const MAX_RECENT_DIRS = 10;
640
+ /** 读取最近使用过的工作路径列表(最新的在前) */
641
+ export async function getRecentDirs() {
642
+ try {
643
+ const raw = await readFile(RECENT_DIRS_FILE, "utf-8");
644
+ const arr = JSON.parse(raw);
645
+ if (Array.isArray(arr)) {
646
+ const seen = new Set();
647
+ return arr.filter((d) => {
648
+ if (typeof d !== "string")
649
+ return false;
650
+ if (seen.has(d))
651
+ return false;
652
+ seen.add(d);
653
+ return true;
654
+ });
655
+ }
656
+ }
657
+ catch { /* file doesn't exist or corrupted */ }
658
+ return [];
659
+ }
660
+ /** 添加一个路径到最近使用列表(去重、限制数量、最新的在前) */
661
+ export async function addRecentDir(dir) {
662
+ const dirs = await getRecentDirs();
663
+ const filtered = dirs.filter(d => d !== dir);
664
+ filtered.unshift(dir);
665
+ const trimmed = filtered.slice(0, MAX_RECENT_DIRS);
666
+ try {
667
+ await mkdir(dirname(RECENT_DIRS_FILE), { recursive: true });
668
+ await writeFile(RECENT_DIRS_FILE, JSON.stringify(trimmed, null, 2), "utf-8");
669
+ }
670
+ catch (err) {
671
+ console.error(`[${ts()}] Failed to save recent_dirs.json: ${err.message}`);
672
+ fileLog.flush();
673
+ }
674
+ }
675
+ /** 读取 /cd 设置的默认工作路径。若 chatId 对应的文件不存在或路径已失效,回退到用户主目录。 */
676
+ export async function getDefaultCwd(chatId) {
677
+ if (chatId) {
678
+ try {
679
+ const content = await readFile(getDefaultCwdFile(chatId), "utf-8");
680
+ const dir = content.trim();
681
+ if (dir) {
682
+ try {
683
+ const s = await stat(dir);
684
+ if (s.isDirectory())
685
+ return dir;
686
+ }
687
+ catch { /* path gone, fall through */ }
688
+ }
689
+ }
690
+ catch { /* file doesn't exist yet */ }
691
+ }
692
+ return homedir();
693
+ }
694
+ /** 设置新建会话的默认工作路径(由 /cd 命令调用,按 chatId 持久化) */
695
+ export async function setDefaultCwd(dir, chatId) {
696
+ await writeFile(getDefaultCwdFile(chatId), dir, "utf-8");
697
+ }
698
+ // ---------------------------------------------------------------------------
699
+ // Tiny helpers
700
+ // ---------------------------------------------------------------------------
701
+ export function ts() {
702
+ return new Date().toLocaleTimeString("zh-CN", { hour12: false });
703
+ }
704
+ /** 仅用于日志确认「已读到某个 App」,不泄露 Secret */
705
+ export function maskAppId(id) {
706
+ if (!id)
707
+ return "(空)";
708
+ if (id.length <= 10)
709
+ return `${id.slice(0, 4)}***`;
710
+ return `${id.slice(0, 8)}…${id.slice(-4)}`;
711
+ }
712
+ /**
713
+ * 启动时逐项说明配置读取结果。
714
+ */
715
+ export function reportEnvironmentVariableReadout() {
716
+ const ok = (label, name, kind, ok, detail) => {
717
+ const state = ok ? "成功" : "失败";
718
+ console.log(` [${state}] [${kind}] ${name}`);
719
+ console.log(` ${label}: ${detail}`);
720
+ };
721
+ console.log(" --- 配置读取结果(成功=已读入;失败=必填缺失或格式错误;默认=未设置则用内置值)---");
722
+ const configExists = existsSync(CONFIG_FILE);
723
+ console.log(` [信息] config.json: ${configExists ? "存在" : "不存在"} → ${CONFIG_FILE}`);
724
+ if (!configExists) {
725
+ console.log(" config.json 不存在时已从 config.sample.json 自动创建,请编辑后重新启动。");
726
+ }
727
+ ok("飞书应用", "feishu.appId", "必填", Boolean(APP_ID.trim()), APP_ID.trim() ? `已读入,摘要 ${maskAppId(APP_ID)}` : "未读入或为空");
728
+ ok("飞书应用", "feishu.appSecret", "必填", Boolean(APP_SECRET.trim()), APP_SECRET.trim() ? "已读入(内容不在日志中显示)" : "未读入或为空");
729
+ console.log(` [默认] [可选] platforms.feishu.platformType`);
730
+ console.log(` 平台类型: ${FEISHU_PLATFORM_TYPE === "lark" ? "Lark (open.larksuite.com)" : "飞书 (open.feishu.cn)"}`);
731
+ console.log(` [默认] [可选] port`);
732
+ console.log(` 监听端口: ${CHATCCC_PORT}`);
733
+ console.log(` [默认] [可选] claude.model`);
734
+ console.log(` Claude 模型: ${anthropicConfigDisplay(CLAUDE_MODEL)}(留空时不向 SDK 传 model)`);
735
+ console.log(` [默认] [可选] claude.subagentModel`);
736
+ console.log(` Claude Subagent 模型: ${anthropicConfigDisplay(CLAUDE_SUBAGENT_MODEL)}(留空时不向 SDK 传 CLAUDE_CODE_SUBAGENT_MODEL)`);
737
+ console.log(` [默认] [可选] claude.effort`);
738
+ console.log(` 思考深度: ${anthropicConfigDisplay(CLAUDE_EFFORT)}(留空时不向 SDK 传 effort)`);
739
+ console.log(` [默认] [可选] gitTimeoutSeconds`);
740
+ console.log(` /git 命令超时: ${GIT_TIMEOUT_SECONDS}s`);
741
+ console.log(" ------------------------------------------------------------------");
742
+ }
743
+ /** 飞书凭证缺失时打印可操作的说明并退出 */
744
+ export function explainMissingFeishuCredentialsAndExit() {
745
+ appendStartupTrace("explainMissingFeishuCredentialsAndExit: exiting", {
746
+ hasAppId: Boolean(APP_ID.trim()),
747
+ hasAppSecret: Boolean(APP_SECRET.trim()),
748
+ });
749
+ const missing = [];
750
+ if (!APP_ID.trim())
751
+ missing.push("feishu.appId");
752
+ if (!APP_SECRET.trim())
753
+ missing.push("feishu.appSecret");
754
+ console.error("\n" + "=".repeat(64));
755
+ console.error(" ChatCCC 启动失败:飞书应用凭证未就绪");
756
+ console.error("=".repeat(64));
757
+ console.error("\n【失败步骤】环境与变量检查(在连接飞书之前)");
758
+ console.error(`\n【未配置的配置项】\n - ${missing.join("\n - ")}`);
759
+ console.error(`\n【配置文件路径】\n ${CONFIG_FILE}`);
760
+ console.error(" 处理: 编辑 config.json 填入飞书开放平台的 App ID / App Secret;\n" +
761
+ " 如 config.json 不存在,可复制 config.sample.json 为 config.json 后编辑。");
762
+ console.error(`\n【程序包根目录】\n ${PROJECT_ROOT}`);
763
+ console.error("\n" + "=".repeat(64) + "\n");
764
+ printServiceDidNotStart(`未配置: ${missing.join("、")}`);
765
+ process.exit(1);
766
+ }
767
+ /** 群描述中用于识别 Claude Code 会话的前缀 */
768
+ export const CLAUDE_SESSION_PREFIX = "Claude Code Session:";
769
+ /** 群描述中用于识别 Cursor 会话的前缀 */
770
+ export const CURSOR_SESSION_PREFIX = "Cursor Session:";
771
+ /** 群描述中用于识别 Codex 会话的前缀 */
772
+ export const CODEX_SESSION_PREFIX = "Codex Session:";
773
+ /** 群描述中用于识别 CCC Agent 会话的前缀 */
774
+ export const CCC_SESSION_PREFIX = "CCC Session:";
775
+ /** 根据 tool 名称返回对应的群描述前缀 */
776
+ export function sessionPrefixForTool(tool) {
777
+ if (tool === "cursor")
778
+ return CURSOR_SESSION_PREFIX;
779
+ if (tool === "codex")
780
+ return CODEX_SESSION_PREFIX;
781
+ if (tool === "ccc")
782
+ return CCC_SESSION_PREFIX;
783
+ return CLAUDE_SESSION_PREFIX;
784
+ }
785
+ /** 根据 tool 名称返回用于状态展示的标签 */
786
+ export function toolDisplayName(tool) {
787
+ if (tool === "cursor")
788
+ return "Cursor";
789
+ if (tool === "codex")
790
+ return "Codex";
791
+ if (tool === "ccc")
792
+ return "CCC Agent";
793
+ return "Claude Code";
794
+ }
795
+ /** 解析 /new 未指定工具时使用的默认 Agent。旧配置缺省 defaultAgent 时保持 Claude 优先。 */
796
+ export function resolveDefaultAgentTool(cfg = config) {
797
+ const explicit = AGENT_TOOLS.find((tool) => cfg[tool].enabled && cfg[tool].defaultAgent);
798
+ if (explicit)
799
+ return explicit;
800
+ const enabledFallback = AGENT_TOOLS.find((tool) => cfg[tool].enabled);
801
+ return enabledFallback ?? "claude";
802
+ }
803
+ // 导出 config 对象供其他模块直接访问原始配置
804
+ export { CONFIG_FILE, CONFIG_SAMPLE_FILE };