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