deeper-cli 1.0.0 → 1.0.1
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/dist/cli/index.js +2 -5
- package/dist/cli/index.js.map +1 -1
- package/package.json +60 -60
- package/src/cli/chat-repl.ts +929 -932
package/src/cli/chat-repl.ts
CHANGED
|
@@ -1,932 +1,929 @@
|
|
|
1
|
-
import readline from 'node:readline';
|
|
2
|
-
import process from 'node:process';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
import { DEEPER_HOME } from '../core/constants.js';
|
|
6
|
-
import { TOOL_SAFETY_MAP } from '../tools/tool-types.js';
|
|
7
|
-
import type { Tool } from '../tools/tool-types.js';
|
|
8
|
-
import { ToolValidator } from '../tools/ToolValidator.js';
|
|
9
|
-
import { xmemory, setSessionId } from '../memory/xmemory.js';
|
|
10
|
-
import { MarkdownStreamRenderer } from '../ui/markdown.js';
|
|
11
|
-
import { getTodos, todoSummary } from '../tools/builtin/ai/todo_manager.js';
|
|
12
|
-
import { estimateTokens } from '../tools/builtin/ai/token_count.js';
|
|
13
|
-
import { SkillEngine } from '../skills/SkillEngine.js';
|
|
14
|
-
import { MCPClient } from '../mcp/MCPClient.js';
|
|
15
|
-
import { O, Oflush, A, d, b, c, g, y, r, B, G, resetTimer, thinkingAnim, thinkingAnimAt } from '../ui/ansi.js';
|
|
16
|
-
|
|
17
|
-
export interface ReplOptions {
|
|
18
|
-
apiKey: string; model: string; baseUrl: string;
|
|
19
|
-
maxTokens: number; temperature: number;
|
|
20
|
-
thinkEnabled: boolean; thinkBudget: number;
|
|
21
|
-
}
|
|
22
|
-
type Role = 'system' | 'user' | 'assistant' | 'tool';
|
|
23
|
-
interface Message {
|
|
24
|
-
role: Role; content: string | null;
|
|
25
|
-
tool_calls?: Array<{ id: string; name: string; arguments: Record<string, unknown> }>;
|
|
26
|
-
tool_call_id?: string; name?: string; reasoning_content?: string;
|
|
27
|
-
}
|
|
28
|
-
interface ToolDef {
|
|
29
|
-
type: 'function';
|
|
30
|
-
function: { name: string; description: string; parameters: Record<string, unknown> };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const MAX_HISTORY = 60;
|
|
34
|
-
const CONTEXT_LIMIT = 1_048_576;
|
|
35
|
-
const CTX_WARN = 786_432;
|
|
36
|
-
const TOOL_RESULT_MAX = 4000;
|
|
37
|
-
const SESSION_DIR = join(DEEPER_HOME, 'sessions');
|
|
38
|
-
const AUTOSAVE_FILE = join(SESSION_DIR, '_autosave.json');
|
|
39
|
-
let GS = { tc: 0, api: 0, ch: 0 };
|
|
40
|
-
let skillEngine: SkillEngine | null = null;
|
|
41
|
-
let mcpClient: MCPClient | null = null;
|
|
42
|
-
const validator = new ToolValidator();
|
|
43
|
-
|
|
44
|
-
function getSkillSystemPrompt(): string {
|
|
45
|
-
if (!skillEngine) return '';
|
|
46
|
-
return skillEngine.getSystemPrompt();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function startRepl(opts: ReplOptions): Promise<void> {
|
|
50
|
-
const tools = await loadBuiltinTools();
|
|
51
|
-
const toolDefs = toolsToDefs(tools);
|
|
52
|
-
|
|
53
|
-
// 连接 subagent 工具到真正的子代理执行引擎
|
|
54
|
-
const { setSubagentRunner } = await import('../tools/builtin/index.js');
|
|
55
|
-
setSubagentRunner(async (task: string, mode: 'foreground' | 'background') => {
|
|
56
|
-
const isBg = mode === 'background';
|
|
57
|
-
const run = async () => {
|
|
58
|
-
const lh: Message[] = [
|
|
59
|
-
{ role: 'system', content: `DeeperCode 子代理。用工具完成任务,完成输出摘要。cwd=${process.cwd()}` },
|
|
60
|
-
{ role: 'user', content: task },
|
|
61
|
-
];
|
|
62
|
-
const tds = toolsToDefs(tools);
|
|
63
|
-
let ce = 0, stag = 0;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
let
|
|
68
|
-
let
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (chunk.type === '
|
|
75
|
-
if (chunk.type === '
|
|
76
|
-
if (chunk.type === '
|
|
77
|
-
if (chunk.type === '
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (chunk.type === '
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
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
|
-
const
|
|
152
|
-
const
|
|
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
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
let
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
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
|
-
if (
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
O(
|
|
232
|
-
O(G(' /
|
|
233
|
-
O(G(' /
|
|
234
|
-
O(G(' /
|
|
235
|
-
O(G(' /
|
|
236
|
-
O(G(' /
|
|
237
|
-
O(G(' /
|
|
238
|
-
O(G(' /
|
|
239
|
-
O(G(' /
|
|
240
|
-
O(G(' /
|
|
241
|
-
O(G(' /
|
|
242
|
-
O(G(' /
|
|
243
|
-
O(G(' /
|
|
244
|
-
O(G(' /
|
|
245
|
-
O(G(' /
|
|
246
|
-
O(G(' /
|
|
247
|
-
O(G(' /
|
|
248
|
-
O(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (cmd === '/
|
|
255
|
-
if (cmd === '/
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
if (cmd === '/
|
|
263
|
-
if (cmd === '/
|
|
264
|
-
if (cmd === '/
|
|
265
|
-
if (cmd === '/
|
|
266
|
-
if (cmd === '/
|
|
267
|
-
if (cmd === '/
|
|
268
|
-
if (cmd === '/
|
|
269
|
-
if (cmd === '/
|
|
270
|
-
if (cmd === '/
|
|
271
|
-
if (cmd === '/
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
if (!
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const mcpTools
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
let
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
let
|
|
356
|
-
|
|
357
|
-
let
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
thinkAnimIv
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
if (
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
tools
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
if (!
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
O(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (tc.name === '
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
.replace(
|
|
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
|
-
const
|
|
607
|
-
if (
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const
|
|
615
|
-
if (
|
|
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
|
-
if (m.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
701
|
-
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
if (
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
} finally { try { reader.releaseLock(); } catch { /* */ } }
|
|
931
|
-
yield { type: 'done' };
|
|
932
|
-
}
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { DEEPER_HOME } from '../core/constants.js';
|
|
6
|
+
import { TOOL_SAFETY_MAP } from '../tools/tool-types.js';
|
|
7
|
+
import type { Tool } from '../tools/tool-types.js';
|
|
8
|
+
import { ToolValidator } from '../tools/ToolValidator.js';
|
|
9
|
+
import { xmemory, setSessionId } from '../memory/xmemory.js';
|
|
10
|
+
import { MarkdownStreamRenderer } from '../ui/markdown.js';
|
|
11
|
+
import { getTodos, todoSummary } from '../tools/builtin/ai/todo_manager.js';
|
|
12
|
+
import { estimateTokens } from '../tools/builtin/ai/token_count.js';
|
|
13
|
+
import { SkillEngine } from '../skills/SkillEngine.js';
|
|
14
|
+
import { MCPClient } from '../mcp/MCPClient.js';
|
|
15
|
+
import { O, Oflush, A, d, b, c, g, y, r, B, G, resetTimer, thinkingAnim, thinkingAnimAt } from '../ui/ansi.js';
|
|
16
|
+
|
|
17
|
+
export interface ReplOptions {
|
|
18
|
+
apiKey: string; model: string; baseUrl: string;
|
|
19
|
+
maxTokens: number; temperature: number;
|
|
20
|
+
thinkEnabled: boolean; thinkBudget: number;
|
|
21
|
+
}
|
|
22
|
+
type Role = 'system' | 'user' | 'assistant' | 'tool';
|
|
23
|
+
interface Message {
|
|
24
|
+
role: Role; content: string | null;
|
|
25
|
+
tool_calls?: Array<{ id: string; name: string; arguments: Record<string, unknown> }>;
|
|
26
|
+
tool_call_id?: string; name?: string; reasoning_content?: string;
|
|
27
|
+
}
|
|
28
|
+
interface ToolDef {
|
|
29
|
+
type: 'function';
|
|
30
|
+
function: { name: string; description: string; parameters: Record<string, unknown> };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const MAX_HISTORY = 60;
|
|
34
|
+
const CONTEXT_LIMIT = 1_048_576;
|
|
35
|
+
const CTX_WARN = 786_432;
|
|
36
|
+
const TOOL_RESULT_MAX = 4000;
|
|
37
|
+
const SESSION_DIR = join(DEEPER_HOME, 'sessions');
|
|
38
|
+
const AUTOSAVE_FILE = join(SESSION_DIR, '_autosave.json');
|
|
39
|
+
let GS = { tc: 0, api: 0, ch: 0 };
|
|
40
|
+
let skillEngine: SkillEngine | null = null;
|
|
41
|
+
let mcpClient: MCPClient | null = null;
|
|
42
|
+
const validator = new ToolValidator();
|
|
43
|
+
|
|
44
|
+
function getSkillSystemPrompt(): string {
|
|
45
|
+
if (!skillEngine) return '';
|
|
46
|
+
return skillEngine.getSystemPrompt();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function startRepl(opts: ReplOptions): Promise<void> {
|
|
50
|
+
const tools = await loadBuiltinTools();
|
|
51
|
+
const toolDefs = toolsToDefs(tools);
|
|
52
|
+
|
|
53
|
+
// 连接 subagent 工具到真正的子代理执行引擎
|
|
54
|
+
const { setSubagentRunner } = await import('../tools/builtin/index.js');
|
|
55
|
+
setSubagentRunner(async (task: string, mode: 'foreground' | 'background') => {
|
|
56
|
+
const isBg = mode === 'background';
|
|
57
|
+
const run = async () => {
|
|
58
|
+
const lh: Message[] = [
|
|
59
|
+
{ role: 'system', content: `DeeperCode 子代理。用工具完成任务,完成输出摘要。cwd=${process.cwd()}` },
|
|
60
|
+
{ role: 'user', content: task },
|
|
61
|
+
];
|
|
62
|
+
const tds = toolsToDefs(tools);
|
|
63
|
+
let ce = 0, stag = 0;
|
|
64
|
+
while (true) {
|
|
65
|
+
const msgs = buildMsgs(lh);
|
|
66
|
+
let fc = '', th = '';
|
|
67
|
+
let tcs: Array<{ id: string; name: string; args: Record<string, unknown> }> = [];
|
|
68
|
+
let curTc: { id: string; name: string; argsStr: string } | null = null;
|
|
69
|
+
let se: string | null = null;
|
|
70
|
+
try {
|
|
71
|
+
const stream = await callApi(opts, msgs, tds, 0, 131072); GS.api++; ce = 0;
|
|
72
|
+
for await (const chunk of stream) {
|
|
73
|
+
if (chunk.type === 'text') { fc += chunk.content || ''; }
|
|
74
|
+
if (chunk.type === 'thinking') th += chunk.content || '';
|
|
75
|
+
if (chunk.type === 'tool_call_start') { const tc = (chunk as any).tool_call; if (tc) curTc = { id: tc.id, name: tc.name, argsStr: '' }; }
|
|
76
|
+
if (chunk.type === 'tool_call_args' && curTc) curTc.argsStr += (chunk as any).content || '';
|
|
77
|
+
if (chunk.type === 'tool_call_end' && curTc) {
|
|
78
|
+
try { tcs.push({ id: curTc.id, name: curTc.name, args: JSON.parse(curTc.argsStr || '{}') }); } catch { tcs.push({ id: curTc.id, name: curTc.name, args: {} }); }
|
|
79
|
+
curTc = null;
|
|
80
|
+
}
|
|
81
|
+
if (chunk.type === 'done') break;
|
|
82
|
+
if (chunk.type === 'error') { se = chunk.error || '?'; break; }
|
|
83
|
+
}
|
|
84
|
+
} catch (e: unknown) { se = e instanceof Error ? e.message : String(e); }
|
|
85
|
+
|
|
86
|
+
if (se) { ce++; stag++; if (ce >= 2) return `子代理失败: ${se}`; await new Promise(r2 => setTimeout(r2, 1000)); continue; }
|
|
87
|
+
|
|
88
|
+
if (tcs.length > 0) {
|
|
89
|
+
stag = 0;
|
|
90
|
+
lh.push({ role: 'assistant', content: fc || null, reasoning_content: undefined as string | undefined, tool_calls: tcs.map(t => ({ id: t.id, name: t.name, arguments: { ...t.args } })) });
|
|
91
|
+
for (const tc of tcs) {
|
|
92
|
+
const tool = tools.find(t => t.name === tc.name);
|
|
93
|
+
if (!tool) { lh.push({ role: 'tool', content: `Error: unknown ${tc.name}`, tool_call_id: tc.id }); continue; }
|
|
94
|
+
const s2 = TOOL_SAFETY_MAP[tc.name] || 'safe';
|
|
95
|
+
if (s2 === 'dangerous') { lh.push({ role: 'tool', content: 'Skipped', tool_call_id: tc.id }); continue; }
|
|
96
|
+
try {
|
|
97
|
+
const r = await tool.execute(tc.args, new AbortController().signal);
|
|
98
|
+
const txt = sanitize(r.output || '').slice(0, TOOL_RESULT_MAX);
|
|
99
|
+
lh.push({ role: 'tool', content: r.success ? txt : `Error: ${(r as any).error}`, tool_call_id: tc.id });
|
|
100
|
+
GS.tc++;
|
|
101
|
+
} catch (e: unknown) { lh.push({ role: 'tool', content: `Error: ${e instanceof Error ? e.message : String(e)}`, tool_call_id: tc.id }); }
|
|
102
|
+
}
|
|
103
|
+
trimHistory(lh, 20); continue;
|
|
104
|
+
}
|
|
105
|
+
// 无工具调用但有文本 → 完成
|
|
106
|
+
if (fc) { lh.push({ role: 'assistant', content: fc }); stag = 0; }
|
|
107
|
+
else { stag++; if (stag >= 3) return `停滞: 连续${stag}轮无进展`; continue; }
|
|
108
|
+
const final = lh[lh.length-1]?.content || '完成';
|
|
109
|
+
return final.slice(0, 800);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (isBg) {
|
|
114
|
+
run().then(result => {
|
|
115
|
+
history.push({ role: 'system', content: `[子代理] ${task.slice(0,50)} → ${result.slice(0, 300)}` });
|
|
116
|
+
});
|
|
117
|
+
return `后台子代理已启动: ${task.slice(0, 80)}`;
|
|
118
|
+
} else {
|
|
119
|
+
O(G(` 🟊 subagent ${task.slice(0, 60)}...\n`));
|
|
120
|
+
const result = await run();
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const history: Message[] = [];
|
|
126
|
+
if (!existsSync(SESSION_DIR)) mkdirSync(SESSION_DIR, { recursive: true });
|
|
127
|
+
|
|
128
|
+
const sid = `sess_${Date.now()}`;
|
|
129
|
+
setSessionId(sid);
|
|
130
|
+
await xmemory.load();
|
|
131
|
+
const prevCount = xmemory.totalEntries;
|
|
132
|
+
const prevSummary = xmemory.getSessionSummary();
|
|
133
|
+
if (prevSummary) {
|
|
134
|
+
history.push({ role: 'system', content: `[跨会话记忆·已加载 ${prevCount} 条]\n${prevSummary.slice(0, 800)}` });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 加载 Skills
|
|
138
|
+
skillEngine = new SkillEngine();
|
|
139
|
+
let skillCount = 0;
|
|
140
|
+
try {
|
|
141
|
+
skillCount = await skillEngine.loadAll();
|
|
142
|
+
} catch { /* skills optional */ }
|
|
143
|
+
|
|
144
|
+
// 连接 MCP 服务器
|
|
145
|
+
let mcpCount = 0;
|
|
146
|
+
let mcpToolCount = 0;
|
|
147
|
+
try {
|
|
148
|
+
mcpClient = new MCPClient();
|
|
149
|
+
const { getConfig } = await import('../core/config.js');
|
|
150
|
+
const cfg = getConfig();
|
|
151
|
+
const servers = cfg.mcpServers || [];
|
|
152
|
+
for (const srv of servers) {
|
|
153
|
+
if (!srv.enabled || !srv.autoConnect) continue;
|
|
154
|
+
try {
|
|
155
|
+
const mcpConfig: { name: string; type: 'stdio' | 'sse'; command?: string; args?: string[]; url?: string; env?: Record<string, string> } = {
|
|
156
|
+
name: srv.name,
|
|
157
|
+
type: srv.url ? 'sse' : 'stdio',
|
|
158
|
+
};
|
|
159
|
+
if (srv.command) { mcpConfig.command = srv.command; mcpConfig.args = srv.args; }
|
|
160
|
+
if (srv.url) mcpConfig.url = srv.url;
|
|
161
|
+
await mcpClient.connect(mcpConfig);
|
|
162
|
+
mcpCount++;
|
|
163
|
+
} catch (e: unknown) {
|
|
164
|
+
O(y(` MCP ${srv.name} 连接失败: ${e instanceof Error ? e.message.slice(0, 60) : String(e).slice(0, 60)}\n`));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (mcpCount > 0) {
|
|
168
|
+
const mcpTools = mcpClient.getAdaptedTools();
|
|
169
|
+
mcpToolCount = mcpTools.length;
|
|
170
|
+
tools.push(...mcpTools);
|
|
171
|
+
}
|
|
172
|
+
} catch { /* MCP optional */ }
|
|
173
|
+
|
|
174
|
+
const drawHeader = (sCount = skillCount, mc = mcpCount, mt = mcpToolCount) => {
|
|
175
|
+
O('\x1b[2J\x1b[H');
|
|
176
|
+
O(b(c(' DeeperCode')) + G(' · 全栈 AI 编程代理') + '\n');
|
|
177
|
+
const modes: string[] = [`${tools.length}工具`];
|
|
178
|
+
if (xmemory.totalEntries > 0) modes.push(`${xmemory.totalEntries}记忆`);
|
|
179
|
+
if (sCount > 0) modes.push(`${sCount}技能`);
|
|
180
|
+
if (mc > 0) modes.push(`${mc}MCP·${mt}工具`);
|
|
181
|
+
O(G(` ▸ V4-Pro · ${modes.join(' · ')}`) + '\n\n');
|
|
182
|
+
};
|
|
183
|
+
drawHeader();
|
|
184
|
+
|
|
185
|
+
let resizeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
186
|
+
process.stdout.on('resize', () => {
|
|
187
|
+
if (resizeTimer) return;
|
|
188
|
+
resizeTimer = setTimeout(() => { resizeTimer = null; Oflush(); }, 200);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
let resolveLine: ((v: string) => void) | null = null;
|
|
192
|
+
let running = true;
|
|
193
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
194
|
+
let currentPrompt = c('❯ ');
|
|
195
|
+
const showPrompt = () => { Oflush(); rl.setPrompt(currentPrompt); rl.prompt(true); };
|
|
196
|
+
rl.on('line', (line: string) => { if (resolveLine) { const cb = resolveLine; resolveLine = null; cb(line); } });
|
|
197
|
+
const ask = (): Promise<string> => new Promise(r => { resolveLine = r; showPrompt(); });
|
|
198
|
+
const confirm = async (msg: string): Promise<boolean> => {
|
|
199
|
+
const sv = resolveLine; resolveLine = null;
|
|
200
|
+
O(y(`\n ⚠ ${msg} [y/N] `));
|
|
201
|
+
const a = await new Promise<string>(r2 => { resolveLine = r2; rl.prompt(true); });
|
|
202
|
+
const ok = a.toLowerCase().startsWith('y');
|
|
203
|
+
if (sv) resolveLine = sv; else showPrompt();
|
|
204
|
+
return ok;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
rl.on('SIGINT', () => {
|
|
208
|
+
if (resolveLine) { const cb = resolveLine; resolveLine = null; cb('/quit'); return; }
|
|
209
|
+
Oflush();
|
|
210
|
+
xmemory.save().then(() => { O('\n' + y('再见!') + '\n'); running = false; rl.close(); process.exit(0); });
|
|
211
|
+
});
|
|
212
|
+
process.on('uncaughtException', (err) => { if (!err.message?.includes('readline')) O(r(`\n ⚠ ${err.message}`) + '\n'); });
|
|
213
|
+
process.on('unhandledRejection', (reason: unknown) => { const m = reason instanceof Error ? reason.message : String(reason); if (!m.includes('readline') && !m.includes('Abort') && !m.includes('timeout')) O(r(`\n ⚠ ${m}`) + '\n'); });
|
|
214
|
+
|
|
215
|
+
while (running) {
|
|
216
|
+
currentPrompt = c('❯ ');
|
|
217
|
+
const tasks = getTodos();
|
|
218
|
+
if (tasks.length > 0) {
|
|
219
|
+
const active = tasks.filter(t => t.status === 'pending' || t.status === 'in_progress');
|
|
220
|
+
if (active.length > 0) currentPrompt = c(`❯ [${active.length}/${tasks.length}] `);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const input = await ask();
|
|
224
|
+
const trimmed = input.trim();
|
|
225
|
+
if (!trimmed) continue;
|
|
226
|
+
|
|
227
|
+
if (trimmed.startsWith('/')) {
|
|
228
|
+
if (trimmed === '/') {
|
|
229
|
+
O(c(' 可用命令:\n'));
|
|
230
|
+
O(G(' /help') + G(' 帮助信息') + '\n');
|
|
231
|
+
O(G(' /clear') + G(' 清空对话') + '\n');
|
|
232
|
+
O(G(' /quit') + G(' 退出') + '\n');
|
|
233
|
+
O(G(' /save') + G(' [n] 保存会话') + '\n');
|
|
234
|
+
O(G(' /load') + G(' [n] 加载会话') + '\n');
|
|
235
|
+
O(G(' /sessions') + G(' 会话列表') + '\n');
|
|
236
|
+
O(G(' /tools') + G(' [c] 工具列表') + '\n');
|
|
237
|
+
O(G(' /stats') + G(' 统计信息') + '\n');
|
|
238
|
+
O(G(' /memory') + G(' 记忆系统') + '\n');
|
|
239
|
+
O(G(' /tasks') + G(' 任务列表') + '\n');
|
|
240
|
+
O(G(' /rules') + G(' 规则管理') + '\n');
|
|
241
|
+
O(G(' /mcp') + G(' MCP服务器') + '\n');
|
|
242
|
+
O(G(' /status') + G(' 当前状态') + '\n');
|
|
243
|
+
O(G(' /model') + G(' 模型设置') + '\n');
|
|
244
|
+
O(G(' /config') + G(' 配置管理') + '\n');
|
|
245
|
+
O(G(' /cwd') + G(' 当前目录') + '\n');
|
|
246
|
+
O(G(' /export') + G(' 导出对话') + '\n');
|
|
247
|
+
O(G(' /init') + G(' 初始化项目') + '\n');
|
|
248
|
+
O('\n'); continue;
|
|
249
|
+
}
|
|
250
|
+
const [cmd, ...rest] = trimmed.split(/\s+/);
|
|
251
|
+
const arg = rest.join(' ');
|
|
252
|
+
if (cmd === '/quit') break;
|
|
253
|
+
if (cmd === '/clear') { history.length = 0; O('\x1b[2J\x1b[H' + g('已清空') + '\n\n'); continue; }
|
|
254
|
+
if (cmd === '/save') { await saveSession(history, arg); continue; }
|
|
255
|
+
if (cmd === '/load' || cmd === '/resume') {
|
|
256
|
+
if (arg) await loadNamedSession(history, arg);
|
|
257
|
+
else await loadLatestSession(history);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (cmd === '/sessions') { await listSessions(); continue; }
|
|
261
|
+
if (cmd === '/tools') { if (arg) await showToolsOf(arg, tools); else await showToolsBrief(tools); continue; }
|
|
262
|
+
if (cmd === '/stats') { O(B(`▸ API:${GS.api} 工具:${GS.tc} 字符:${GS.ch}`) + '\n\n'); continue; }
|
|
263
|
+
if (cmd === '/memory') { await showMemory(); continue; }
|
|
264
|
+
if (cmd === '/tasks') { await showTasks(); continue; }
|
|
265
|
+
if (cmd === '/model') { O(c('模型: deepseek-v4-pro | deepseek-v4-flash\n deeper config set model <name>\n\n')); continue; }
|
|
266
|
+
if (cmd === '/config') { O(c('配置: deeper config list | deeper config set <key> <value>\n\n')); continue; }
|
|
267
|
+
if (cmd === '/cwd') { O(G(` ${process.cwd()}\n\n`)); continue; }
|
|
268
|
+
if (cmd === '/export') { await exportHistory(history); continue; }
|
|
269
|
+
if (cmd === '/init') { await initProject(); continue; }
|
|
270
|
+
if (cmd === '/status') { O(B(`▸ API:${GS.api} 工具:${GS.tc} 字符:${GS.ch} · 上下文:${history.length}条`) + '\n\n'); continue; }
|
|
271
|
+
if (cmd === '/mcp') {
|
|
272
|
+
if (!mcpClient) { O(y(' MCP 未初始化\n\n')); continue; }
|
|
273
|
+
const servers = mcpClient.getConnectedServers();
|
|
274
|
+
if (!servers.length) { O(G(' 无已连接的 MCP 服务器\n 使用 deeper mcp add 添加\n\n')); continue; }
|
|
275
|
+
O(b(c(' MCP Servers')) + G(` · ${servers.length} 个`) + '\n');
|
|
276
|
+
for (const name of servers) {
|
|
277
|
+
const mcpTools = mcpClient.listTools(name);
|
|
278
|
+
O(G(` 📡 ${name}`) + G(` · ${mcpTools.length} 工具`) + '\n');
|
|
279
|
+
for (const t of mcpTools.slice(0, 5)) O(G(` - ${t.name}: ${t.description.slice(0, 50)}`) + '\n');
|
|
280
|
+
if (mcpTools.length > 5) O(G(` …还有 ${mcpTools.length - 5} 个`) + '\n');
|
|
281
|
+
}
|
|
282
|
+
O('\n'); continue;
|
|
283
|
+
}
|
|
284
|
+
if (cmd === '/rules') {
|
|
285
|
+
const projectRules = join(process.cwd(), '.deeper', 'rules.md');
|
|
286
|
+
const globalRules = join(DEEPER_HOME, 'rules.md');
|
|
287
|
+
O(b(c(' Rules')) + '\n');
|
|
288
|
+
const showRules = (label: string, path: string) => {
|
|
289
|
+
if (existsSync(path)) {
|
|
290
|
+
const content = readFileSync(path, 'utf-8');
|
|
291
|
+
O(G(` ${label}: `) + c(path) + G(` (${content.split('\n').length}行)`) + '\n');
|
|
292
|
+
const lines = content.split('\n').slice(0, 8);
|
|
293
|
+
for (const l of lines) O(G(` ${l.slice(0, 70)}`) + '\n');
|
|
294
|
+
if (content.split('\n').length > 8) O(G(' …') + '\n');
|
|
295
|
+
} else {
|
|
296
|
+
O(G(` ${label}: `) + y('未设置') + G(` (创建: ${path})`) + '\n');
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
showRules('项目规则', projectRules);
|
|
300
|
+
showRules('全局规则', globalRules);
|
|
301
|
+
O('\n'); continue;
|
|
302
|
+
}
|
|
303
|
+
if (cmd === '/help') {
|
|
304
|
+
O(c(' /help /clear /quit /save [name] /load|resume [name] /sessions\n'));
|
|
305
|
+
O(c(' /tools [cat] /stats /memory /tasks /model /config /cwd /export /init /mcp /rules\n\n'));
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
O(G(` 未知命令: ${cmd} (输入 /help 查看帮助)\n\n`));
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const fdefs = toolsToDefs(tools);
|
|
313
|
+
O('\n');
|
|
314
|
+
const um: Message = { role: 'user', content: trimmed };
|
|
315
|
+
history.push(um); trimHistory(history, MAX_HISTORY);
|
|
316
|
+
await runLoop(opts, history, tools, fdefs, confirm);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
rl.close();
|
|
320
|
+
Oflush();
|
|
321
|
+
await xmemory.save();
|
|
322
|
+
if (mcpClient) mcpClient.disconnectAll();
|
|
323
|
+
Oflush();
|
|
324
|
+
O(y('\n再见!\n')); process.exit(0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ========== Core loop ==========
|
|
328
|
+
|
|
329
|
+
async function runLoop(
|
|
330
|
+
opts: ReplOptions, history: Message[], tools: Tool[],
|
|
331
|
+
toolDefs: ToolDef[], confirm: (msg: string) => Promise<boolean>,
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
let ttc = 0, ce = 0, stagnation = 0;
|
|
334
|
+
|
|
335
|
+
for (let iter = 0; ; iter++) {
|
|
336
|
+
// 上下文接近上限 → 降 tokens 而不是直接退出
|
|
337
|
+
const ctxTokens = estimateTokens(history.map(m => m.content || '').join('\n'));
|
|
338
|
+
const toolsTokenOverhead = toolDefs.length * 80;
|
|
339
|
+
const totalCtx = ctxTokens + toolsTokenOverhead;
|
|
340
|
+
const nearLimit = totalCtx > CONTEXT_LIMIT * 0.95;
|
|
341
|
+
// 上下文自动压缩:超过 70% 时压缩旧消息
|
|
342
|
+
if (totalCtx > CONTEXT_LIMIT * 0.7 && history.length > 10) {
|
|
343
|
+
compressHistory(history);
|
|
344
|
+
}
|
|
345
|
+
// 连续空转检测
|
|
346
|
+
if (stagnation >= 5) {
|
|
347
|
+
O(y(`\n 连续${stagnation}轮无进展,任务交还\n\n`));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const msgs = buildMsgs(history);
|
|
351
|
+
const st = Date.now();
|
|
352
|
+
let fc = '', th = '';
|
|
353
|
+
let tcs: Array<{ id: string; name: string; args: Record<string, unknown> }> = [];
|
|
354
|
+
let curTc: { id: string; name: string; argsStr: string } | null = null;
|
|
355
|
+
let se: string | null = null;
|
|
356
|
+
const amax = iter === 0 ? 131072 : nearLimit ? 32768 : 65536;
|
|
357
|
+
let showingThink = false;
|
|
358
|
+
|
|
359
|
+
// 思考动画:API 等待和推理期间持续显示
|
|
360
|
+
resetTimer();
|
|
361
|
+
const animLabel = () => showingThink ? '思考中' : '解析中';
|
|
362
|
+
const animTick = () => { O('\r' + thinkingAnim(animLabel())); };
|
|
363
|
+
let thinkAnimIv: ReturnType<typeof setInterval> | null = null;
|
|
364
|
+
animTick(); // 首帧立即渲染,不等 100ms
|
|
365
|
+
thinkAnimIv = setInterval(animTick, 80);
|
|
366
|
+
const md = new MarkdownStreamRenderer();
|
|
367
|
+
|
|
368
|
+
const startStreamAnim = () => {
|
|
369
|
+
if (thinkAnimIv) return;
|
|
370
|
+
resetTimer();
|
|
371
|
+
animTick(); // 首帧立即渲染
|
|
372
|
+
thinkAnimIv = setInterval(animTick, 80);
|
|
373
|
+
};
|
|
374
|
+
const stopStreamAnim = () => {
|
|
375
|
+
if (thinkAnimIv) { clearInterval(thinkAnimIv); thinkAnimIv = null; }
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
const stream = await callApi(opts, msgs, toolDefs, 0, amax);
|
|
380
|
+
GS.api++; ce = 0;
|
|
381
|
+
|
|
382
|
+
const cols = process.stdout.columns || 80;
|
|
383
|
+
|
|
384
|
+
for await (const chunk of stream) {
|
|
385
|
+
if (chunk.type === 'text') {
|
|
386
|
+
const t = chunk.content || '';
|
|
387
|
+
fc += t;
|
|
388
|
+
if (fc.length > 100_000) fc = fc.slice(-80_000);
|
|
389
|
+
if (fc === t) {
|
|
390
|
+
if (showingThink) { showingThink = false; stopStreamAnim(); }
|
|
391
|
+
Oflush(); O('\r' + ' '.repeat(cols) + '\r');
|
|
392
|
+
if (!fc.slice(t.length - t.length)) O(b(c('●')) + ' ');
|
|
393
|
+
}
|
|
394
|
+
const rendered = md.feed(t);
|
|
395
|
+
if (rendered) O(rendered);
|
|
396
|
+
GS.ch += t.length;
|
|
397
|
+
}
|
|
398
|
+
if (chunk.type === 'thinking') {
|
|
399
|
+
if (!showingThink) { showingThink = true; startStreamAnim(); }
|
|
400
|
+
th += chunk.content || '';
|
|
401
|
+
if (th.length > 20_000) th = th.slice(-16_000);
|
|
402
|
+
}
|
|
403
|
+
if (chunk.type === 'tool_call_start') {
|
|
404
|
+
if (showingThink) { O('\r' + ' '.repeat(cols) + '\r'); showingThink = false; }
|
|
405
|
+
stopStreamAnim();
|
|
406
|
+
const tc = (chunk as any).tool_call;
|
|
407
|
+
if (tc) curTc = { id: tc.id, name: tc.name, argsStr: '' };
|
|
408
|
+
}
|
|
409
|
+
if (chunk.type === 'tool_call_args' && curTc) curTc.argsStr += (chunk as any).content || '';
|
|
410
|
+
if (chunk.type === 'tool_call_end' && curTc) {
|
|
411
|
+
try { tcs.push({ id: curTc.id, name: curTc.name, args: JSON.parse(curTc.argsStr || '{}') }); } catch { tcs.push({ id: curTc.id, name: curTc.name, args: {} }); }
|
|
412
|
+
curTc = null;
|
|
413
|
+
}
|
|
414
|
+
if (chunk.type === 'done') break;
|
|
415
|
+
if (chunk.type === 'error') { se = chunk.error || '?'; break; }
|
|
416
|
+
}
|
|
417
|
+
} catch (e: unknown) { stopStreamAnim(); se = e instanceof Error ? e.message : String(e); }
|
|
418
|
+
|
|
419
|
+
stopStreamAnim();
|
|
420
|
+
Oflush();
|
|
421
|
+
const remaining = md.flush();
|
|
422
|
+
if (remaining) O(remaining);
|
|
423
|
+
md.reset();
|
|
424
|
+
|
|
425
|
+
if (se) {
|
|
426
|
+
ce++; O(r(`\n ✗ ${se}`));
|
|
427
|
+
if (ce >= 3) { O(r('\n 连续失败,放弃\n\n')); return; }
|
|
428
|
+
const w = Math.min(1000 * ce, 5000); O(G(` 重试(${ce}/3)...\n`)); await new Promise(r2 => setTimeout(r2, w)); continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const el = Date.now() - st;
|
|
432
|
+
if (fc && !tcs.length) O('\n');
|
|
433
|
+
|
|
434
|
+
if (tcs.length > 0) {
|
|
435
|
+
stagnation = 0;
|
|
436
|
+
Oflush();
|
|
437
|
+
const compactFc = fc && fc.length > 500 ? fc.replace(/\n/g, ' ').slice(0, 300) + '…' : fc;
|
|
438
|
+
history.push({ role: 'assistant', content: compactFc || null, reasoning_content: undefined, tool_calls: tcs.map(t => ({ id: t.id, name: t.name, arguments: { ...t.args } })) });
|
|
439
|
+
fc = ''; th = '';
|
|
440
|
+
let doneTools = 0;
|
|
441
|
+
|
|
442
|
+
// 只在第一轮显示完整任务面板
|
|
443
|
+
if (iter === 0) {
|
|
444
|
+
const tdSummary = todoSummary();
|
|
445
|
+
if (tdSummary) { Oflush(); O('\n' + d(tdSummary.split('\n').join('\n ')) + '\n'); }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const safe = tcs.filter(t => (TOOL_SAFETY_MAP[t.name] || 'safe') === 'safe');
|
|
449
|
+
const rest = tcs.filter(t => (TOOL_SAFETY_MAP[t.name] || 'safe') !== 'safe');
|
|
450
|
+
if (safe.length > 1) {
|
|
451
|
+
const results = await Promise.allSettled(safe.map(async tc => { const r = await execTool(tc, tools, opts); doneTools++; return r; }));
|
|
452
|
+
for (const r of results) {
|
|
453
|
+
if (r.status === 'fulfilled') { history.push(r.value); ttc++; GS.tc++; }
|
|
454
|
+
else { history.push({ role: 'tool', content: `Error: ${String(r.reason)}`, tool_call_id: 'parallel' }); }
|
|
455
|
+
}
|
|
456
|
+
} else if (safe.length === 1) {
|
|
457
|
+
const r = await execTool(safe[0], tools, opts);
|
|
458
|
+
history.push(r); ttc++; GS.tc++; doneTools++;
|
|
459
|
+
}
|
|
460
|
+
for (const tc of rest) {
|
|
461
|
+
const r2 = await execTool(tc, tools, opts, confirm);
|
|
462
|
+
history.push(r2); if (!r2.content?.startsWith('Error:') && !r2.content?.includes('skipped')) { ttc++; GS.tc++; }
|
|
463
|
+
doneTools++;
|
|
464
|
+
}
|
|
465
|
+
// 释放 tcs 引用帮助 GC
|
|
466
|
+
tcs.length = 0; safe.length = 0; rest.length = 0;
|
|
467
|
+
|
|
468
|
+
trimHistory(history, MAX_HISTORY); continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (fc) { history.push({ role: 'assistant', content: fc }); stagnation = 0; }
|
|
472
|
+
else { stagnation++; }
|
|
473
|
+
|
|
474
|
+
const ctxPct = ((totalCtx / CONTEXT_LIMIT) * 100).toFixed(1);
|
|
475
|
+
const ctxWarn = totalCtx > CTX_WARN ? y(` ${ctxPct}%`) : G(` ${ctxPct}%`);
|
|
476
|
+
O(G(` ▸ ${(el/1000).toFixed(1)}s · ${ttc} 工具 · 上下文${ctxWarn}`) + '\n\n');
|
|
477
|
+
trimHistory(history, MAX_HISTORY);
|
|
478
|
+
|
|
479
|
+
if (fc) {
|
|
480
|
+
const lu = lastUser(history);
|
|
481
|
+
xmemory.storeEpisodic(`完成任务: ${lu.slice(0, 100)} → ${fc.slice(0, 200)}`, ['task', 'complete'], 5);
|
|
482
|
+
xmemory.storeSemantic(`学会了关于 ${lu.slice(0, 80)} 的处理方式`, ['learning'], 4);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function execTool(
|
|
490
|
+
tc: { id: string; name: string; args: Record<string, unknown> },
|
|
491
|
+
tools: Tool[], opts: ReplOptions,
|
|
492
|
+
confirm?: (msg: string) => Promise<boolean>,
|
|
493
|
+
): Promise<Message> {
|
|
494
|
+
const tool = tools.find(t => t.name === tc.name);
|
|
495
|
+
if (!tool) return { role: 'tool', content: `Error: unknown tool ${tc.name}`, tool_call_id: tc.id };
|
|
496
|
+
|
|
497
|
+
const vResult = validator.validate(tool, tc.args);
|
|
498
|
+
if (!vResult.success) {
|
|
499
|
+
return { role: 'tool', content: `Error: ${vResult.error}`, tool_call_id: tc.id };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const s = TOOL_SAFETY_MAP[tc.name] || 'safe';
|
|
503
|
+
const toolName = tc.name;
|
|
504
|
+
const tSyms = '⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏';
|
|
505
|
+
let ti = 0;
|
|
506
|
+
|
|
507
|
+
if ((s === 'dangerous' || s === 'confirm') && confirm) {
|
|
508
|
+
Oflush(); O('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
|
|
509
|
+
const prefix = s === 'dangerous' ? '⛔' : '⚠';
|
|
510
|
+
O(y(` ${prefix}确认? `));
|
|
511
|
+
const ok = await confirm(`执行 ${toolName}?`);
|
|
512
|
+
if (!ok) { O(G(' 跳过\n')); return { role: 'tool', content: 'User skipped', tool_call_id: tc.id }; }
|
|
513
|
+
O('\r' + A.d + ' ' + A.R + A.c + tSyms[0] + A.R + A.G + ' ' + toolName + A.R + ' ');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let execAnimIv: ReturnType<typeof setInterval> | null = null;
|
|
517
|
+
const stopToolAnim = () => { if (execAnimIv) { clearInterval(execAnimIv); execAnimIv = null; } };
|
|
518
|
+
try {
|
|
519
|
+
const toolStart = Date.now();
|
|
520
|
+
const rawWrite = (s: string) => { try { process.stdout.write(s); } catch {} };
|
|
521
|
+
rawWrite('\r' + thinkingAnimAt(toolName, toolStart));
|
|
522
|
+
execAnimIv = setInterval(() => { rawWrite('\r' + thinkingAnimAt(toolName, toolStart)); }, 80);
|
|
523
|
+
const execed = Promise.race([
|
|
524
|
+
tool.execute(tc.args, new AbortController().signal),
|
|
525
|
+
new Promise<{ success: false; error: string; output: string }>((_, rej) => setTimeout(() => rej(new Error('工具超时 (30s)')), 30_000)),
|
|
526
|
+
]);
|
|
527
|
+
let result: { success: boolean; error?: string; output?: string };
|
|
528
|
+
try { result = await execed as any; } catch (e: unknown) { result = { success: false, error: (e as Error).message, output: '' }; }
|
|
529
|
+
stopToolAnim();
|
|
530
|
+
const rawResult = result.success ? result.output || '' : `Error: ${result.error || 'failed'}`;
|
|
531
|
+
// 截断超大结果防 OOM
|
|
532
|
+
const text = result.success ? sanitize(rawResult).slice(0, TOOL_RESULT_MAX) : `Error: ${result.error || 'failed'}`;
|
|
533
|
+
const showPath = (p: string) => p.split(/[/\\]/).filter(Boolean).slice(-2).join('/').slice(-35);
|
|
534
|
+
|
|
535
|
+
let brief = '';
|
|
536
|
+
if (tc.name === 'write_file' || tc.name === 'edit_file') {
|
|
537
|
+
const fp = (tc.args.file_path || tc.args.path || tc.args.file || '') as string;
|
|
538
|
+
brief = showPath(fp);
|
|
539
|
+
} else if (tc.name === 'todo_manager') {
|
|
540
|
+
brief = '任务已更新';
|
|
541
|
+
} else {
|
|
542
|
+
brief = rawResult.replace(/\n/g, ' ').slice(0, 60);
|
|
543
|
+
}
|
|
544
|
+
Oflush(); O('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
|
|
545
|
+
if (tc.name === 'todo_manager' && result.success) {
|
|
546
|
+
O(A.y + A.b + ' ▸▸ █ 任务面板' + A.R + '\n');
|
|
547
|
+
const lines = rawResult.split('\n').filter(Boolean);
|
|
548
|
+
let shown = 0; const MAX_SHOW = 16;
|
|
549
|
+
for (const line of lines) {
|
|
550
|
+
const trimmed = line.trim();
|
|
551
|
+
if (!trimmed) continue;
|
|
552
|
+
if (/^📋/.test(trimmed)) {
|
|
553
|
+
O(A.b + ' ' + trimmed + A.R + '\n');
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
shown++;
|
|
557
|
+
if (shown > MAX_SHOW) { O(G(' …\n')); break; }
|
|
558
|
+
const depth = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
559
|
+
const colored = trimmed
|
|
560
|
+
.replace(/^✓\s*/, g('✓ '))
|
|
561
|
+
.replace(/^◉\s*/, y('◉ '))
|
|
562
|
+
.replace(/^○\s*/, G('○ '))
|
|
563
|
+
.replace(/^✗\s*/, r('✗ '));
|
|
564
|
+
if (depth >= 4) O(G(' ') + colored + '\n');
|
|
565
|
+
else if (depth >= 2) O(G(' ') + colored + '\n');
|
|
566
|
+
else O(' ' + colored + '\n');
|
|
567
|
+
}
|
|
568
|
+
O('\n');
|
|
569
|
+
} else {
|
|
570
|
+
O(g(' ✓') + G(` ${c(tc.name)} ${brief}\n`));
|
|
571
|
+
}
|
|
572
|
+
if (result.success) {
|
|
573
|
+
xmemory.storeWorking(`${tc.name}: ${brief}`, [tc.name, 'tool']);
|
|
574
|
+
if (tc.name === 'write_file' || tc.name === 'edit_file' || tc.name === 'delete_file') {
|
|
575
|
+
xmemory.storeEpisodic(`修改了文件: ${brief}`, ['file', 'modified'], 6);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return { role: 'tool', content: text, tool_call_id: tc.id };
|
|
580
|
+
} catch (e: unknown) {
|
|
581
|
+
stopToolAnim();
|
|
582
|
+
const em = e instanceof Error ? e.message : String(e);
|
|
583
|
+
Oflush(); O('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
|
|
584
|
+
O(r(' ✗') + G(` ${tc.name} ${em.slice(0, 60)}\n`));
|
|
585
|
+
return { role: 'tool', content: `Error: ${em}`, tool_call_id: tc.id };
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ========== Helpers ==========
|
|
590
|
+
|
|
591
|
+
function buildMsgs(history: Message[]): Array<Record<string, unknown>> {
|
|
592
|
+
const r: Array<Record<string, unknown>> = [];
|
|
593
|
+
let sysContent = `You are DeeperCode V4-Pro, a full-stack coding agent.
|
|
594
|
+
Rules:
|
|
595
|
+
1. Read before write. Use tools to inspect project state first.
|
|
596
|
+
2. Write COMPLETE files. No placeholders like "// ..." or "rest of code".
|
|
597
|
+
3. One write_file = one complete file. Batch independent writes.
|
|
598
|
+
4. Use todo_manager for multi-step tasks. Update status as you progress.
|
|
599
|
+
5. Keep reasoning brief. Prefer action over explanation.
|
|
600
|
+
6. cwd=${process.cwd()} plat=${process.platform}
|
|
601
|
+
7. Respond in Chinese when user uses Chinese.`;
|
|
602
|
+
|
|
603
|
+
const ts = todoSummary(4);
|
|
604
|
+
if (ts) sysContent += '\n[Remaining]\n' + ts;
|
|
605
|
+
|
|
606
|
+
const lastU = history.filter(m => m.role === 'user').pop();
|
|
607
|
+
if (lastU?.content) {
|
|
608
|
+
const hints = xmemory.getProceduralHints(lastU.content || '', 3);
|
|
609
|
+
if (hints) sysContent += '\n' + hints;
|
|
610
|
+
}
|
|
611
|
+
const workCtx = xmemory.getWorkingContext(400);
|
|
612
|
+
if (workCtx) sysContent += '\n[最近工作]\n' + workCtx;
|
|
613
|
+
|
|
614
|
+
const skillPrompt = getSkillSystemPrompt();
|
|
615
|
+
if (skillPrompt) sysContent += '\n' + skillPrompt;
|
|
616
|
+
|
|
617
|
+
r.push({ role: 'system', content: sysContent });
|
|
618
|
+
|
|
619
|
+
// 自动注入 deeper.md 项目上下文 + rules 规则
|
|
620
|
+
try {
|
|
621
|
+
const deeperMd = join(process.cwd(), 'deeper.md');
|
|
622
|
+
if (existsSync(deeperMd)) {
|
|
623
|
+
const ctx = readFileSync(deeperMd, 'utf-8').slice(0, 4000);
|
|
624
|
+
if (ctx.trim() && !ctx.includes('<!-- 填写')) {
|
|
625
|
+
r.push({ role: 'system', content: `[项目上下文 deeper.md]\n${ctx}` });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} catch { /* skip */ }
|
|
629
|
+
try {
|
|
630
|
+
const rulesFile = join(process.cwd(), '.deeper', 'rules.md');
|
|
631
|
+
if (existsSync(rulesFile)) {
|
|
632
|
+
const rules = readFileSync(rulesFile, 'utf-8').slice(0, 4000);
|
|
633
|
+
if (rules.trim()) r.push({ role: 'system', content: `[项目规则 rules.md]\n${rules}` });
|
|
634
|
+
}
|
|
635
|
+
} catch { /* skip */ }
|
|
636
|
+
try {
|
|
637
|
+
const globalRules = join(DEEPER_HOME, 'rules.md');
|
|
638
|
+
if (existsSync(globalRules)) {
|
|
639
|
+
const rules = readFileSync(globalRules, 'utf-8').slice(0, 2000);
|
|
640
|
+
if (rules.trim()) r.push({ role: 'system', content: `[全局规则]\n${rules}` });
|
|
641
|
+
}
|
|
642
|
+
} catch { /* skip */ }
|
|
643
|
+
|
|
644
|
+
for (const m of history.slice(-30)) {
|
|
645
|
+
const e: Record<string, unknown> = { role: m.role };
|
|
646
|
+
// 截断大型消息防止 OOM
|
|
647
|
+
if (m.content != null) e.content = (m.content || '').slice(0, 8000);
|
|
648
|
+
if (m.reasoning_content) e.reasoning_content = (m.reasoning_content || '').slice(0, 2000);
|
|
649
|
+
if (m.tool_calls) e.tool_calls = m.tool_calls.map(t => ({
|
|
650
|
+
id: t.id, type: 'function', function: { name: t.name, arguments: JSON.stringify(t.arguments) },
|
|
651
|
+
}));
|
|
652
|
+
if (m.tool_call_id) e.tool_call_id = m.tool_call_id;
|
|
653
|
+
r.push(e);
|
|
654
|
+
}
|
|
655
|
+
return r;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function trimHistory(h: Message[], max: number) { while (h.length > max) h.shift(); }
|
|
659
|
+
|
|
660
|
+
function compressHistory(h: Message[]): void {
|
|
661
|
+
if (h.length <= 6) return;
|
|
662
|
+
const keep = 6;
|
|
663
|
+
const old = h.slice(0, h.length - keep);
|
|
664
|
+
const recent = h.slice(h.length - keep);
|
|
665
|
+
let compressed = '';
|
|
666
|
+
for (const m of old) {
|
|
667
|
+
if (m.role === 'user' && m.content) {
|
|
668
|
+
compressed += `[用户] ${m.content.slice(0, 100)}\n`;
|
|
669
|
+
} else if (m.role === 'assistant' && m.content) {
|
|
670
|
+
compressed += `[助手] ${m.content.slice(0, 150)}\n`;
|
|
671
|
+
} else if (m.role === 'tool' && m.content) {
|
|
672
|
+
compressed += `[工具] ${m.content.slice(0, 80)}\n`;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
h.length = 0;
|
|
676
|
+
h.push({ role: 'system', content: `[上下文压缩·${old.length}条摘要]\n${compressed.slice(0, 3000)}` });
|
|
677
|
+
h.push(...recent);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function sanitize(t: string): string {
|
|
681
|
+
return t.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
682
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
683
|
+
.replace(/\n{6,}/g, '\n\n');
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function lastUser(h: Message[]): string { for (let i = h.length-1; i>=0; i--) if (h[i].role==='user' && h[i].content) return h[i].content!; return ''; }
|
|
687
|
+
|
|
688
|
+
function toolsToDefs(tools: Tool[]): ToolDef[] {
|
|
689
|
+
return tools.map(t => ({ type: 'function' as const, function: { name: t.name, description: t.description, parameters: t.parameters as unknown as Record<string, unknown> } }));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ========== Session commands ==========
|
|
693
|
+
|
|
694
|
+
async function saveSession(history: Message[], name?: string) {
|
|
695
|
+
const label = name || new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
696
|
+
const file = join(SESSION_DIR, `sess_${label}.json`);
|
|
697
|
+
writeFileSync(file, JSON.stringify({ savedAt: new Date().toISOString(), cwd: process.cwd(), messages: history }, null, 2), 'utf-8');
|
|
698
|
+
O(g(`已保存: sess_${label}`) + '\n\n');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function listSessions() {
|
|
702
|
+
if (!existsSync(SESSION_DIR)) { O(r('无保存的会话\n\n')); return; }
|
|
703
|
+
const files = readdirSync(SESSION_DIR).filter(f => f.endsWith('.json') && !f.startsWith('_')).sort().reverse();
|
|
704
|
+
if (!files.length) { O(r('无保存的会话\n\n')); return; }
|
|
705
|
+
O(b(c(' Sessions')) + G(` · ${files.length} 个`) + '\n');
|
|
706
|
+
for (let i = 0; i < Math.min(files.length, 15); i++) {
|
|
707
|
+
const f = files[i];
|
|
708
|
+
try {
|
|
709
|
+
const data = JSON.parse(readFileSync(join(SESSION_DIR, f), 'utf-8'));
|
|
710
|
+
const label = f.replace(/^sess_|\.json$/g, '');
|
|
711
|
+
const msgCount = data.messages?.length || 0;
|
|
712
|
+
const cwd = data.cwd || '?';
|
|
713
|
+
const savedAt = data.savedAt?.slice(0, 16) || '?';
|
|
714
|
+
O(G(` ${i+1}. `) + c(label) + G(` · ${msgCount}条 · ${savedAt}`) + '\n');
|
|
715
|
+
} catch { O(G(` ${i+1}. ${f} (损坏)\n`)); }
|
|
716
|
+
}
|
|
717
|
+
O('\n 加载: /load <name> | 恢复: /resume\n\n');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function loadNamedSession(history: Message[], name: string) {
|
|
721
|
+
const file = join(SESSION_DIR, `sess_${name}.json`);
|
|
722
|
+
if (!existsSync(file)) { O(r(`会话不存在: ${name}\n\n`)); return; }
|
|
723
|
+
const data = JSON.parse(readFileSync(file, 'utf-8'));
|
|
724
|
+
history.push({ role: 'system', content: `[已加载: ${name} (${data.messages?.length||0} 条)]` });
|
|
725
|
+
if (data.messages) for (const m of data.messages) history.push(m);
|
|
726
|
+
trimHistory(history, MAX_HISTORY);
|
|
727
|
+
O(g(`已加载: ${name}`) + '\n\n');
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
async function loadLatestSession(history: Message[]) {
|
|
731
|
+
if (!existsSync(SESSION_DIR)) { O(r('无保存的会话\n\n')); return; }
|
|
732
|
+
const files = readdirSync(SESSION_DIR).filter(f => f.endsWith('.json') && !f.startsWith('_')).sort().reverse();
|
|
733
|
+
if (!files.length) { O(r('无保存的会话\n\n')); return; }
|
|
734
|
+
const file = join(SESSION_DIR, files[0]);
|
|
735
|
+
const data = JSON.parse(readFileSync(file, 'utf-8'));
|
|
736
|
+
const label = files[0].replace(/^sess_|\.json$/g, '');
|
|
737
|
+
history.push({ role: 'system', content: `[已加载: ${label} (${data.messages?.length||0} 条)]` });
|
|
738
|
+
if (data.messages) for (const m of data.messages) history.push(m);
|
|
739
|
+
trimHistory(history, MAX_HISTORY);
|
|
740
|
+
O(g(`已加载: ${label}`) + '\n\n');
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function exportHistory(history: Message[]) {
|
|
744
|
+
const file = join(process.cwd(), `deeper-export-${Date.now()}.json`);
|
|
745
|
+
writeFileSync(file, JSON.stringify(history, null, 2), 'utf-8');
|
|
746
|
+
O(g(`已导出: ${file}`) + '\n\n');
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async function initProject(): Promise<void> {
|
|
750
|
+
const file = join(process.cwd(), 'deeper.md');
|
|
751
|
+
if (existsSync(file)) {
|
|
752
|
+
O(y(' deeper.md 已存在,跳过\n\n'));
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const content = `# DeeperCode 项目上下文
|
|
756
|
+
|
|
757
|
+
> 此文件由 \`/init\` 自动生成,AI 会在每次对话中自动读取。
|
|
758
|
+
> 你可以手动编辑,添加项目规则、约定和背景信息。
|
|
759
|
+
|
|
760
|
+
## 📁 项目名称
|
|
761
|
+
|
|
762
|
+
<!-- 填写项目名称 -->
|
|
763
|
+
|
|
764
|
+
## 🎯 项目目标
|
|
765
|
+
|
|
766
|
+
<!-- 简要描述项目目的 -->
|
|
767
|
+
|
|
768
|
+
## 🛠 技术栈
|
|
769
|
+
|
|
770
|
+
<!-- 例如: React + TypeScript + Vite -->
|
|
771
|
+
|
|
772
|
+
## 📐 代码规范
|
|
773
|
+
|
|
774
|
+
<!-- 命名约定、文件组织等 -->
|
|
775
|
+
|
|
776
|
+
## ⚙️ 常用命令
|
|
777
|
+
|
|
778
|
+
<!-- npm run dev, npm test 等 -->
|
|
779
|
+
|
|
780
|
+
## 📝 注意事项
|
|
781
|
+
|
|
782
|
+
<!-- AI 需要注意的任何特殊要求 -->
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
*最后更新: ${new Date().toISOString()}*
|
|
787
|
+
`;
|
|
788
|
+
writeFileSync(file, content, 'utf-8');
|
|
789
|
+
O(g('已创建: deeper.md') + G(' (可编辑后 AI 自动读取)') + '\n\n');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// ========== Display commands ==========
|
|
793
|
+
|
|
794
|
+
async function showTasks(): Promise<void> {
|
|
795
|
+
const items = getTodos();
|
|
796
|
+
if (items.length === 0) { O(G(' 任务列表为空\n\n')); return; }
|
|
797
|
+
let total = 0, done = 0;
|
|
798
|
+
for (const t of items) {
|
|
799
|
+
total++; if (t.status === 'done') done++;
|
|
800
|
+
if (t.subtasks) for (const st of t.subtasks) { total++; if (st.status === 'done') done++; }
|
|
801
|
+
}
|
|
802
|
+
O(b(c(' Tasks')) + G(` · ${done}/${total}`) + '\n');
|
|
803
|
+
let shown = 0; const MAX = 15;
|
|
804
|
+
for (const t of items) {
|
|
805
|
+
if (++shown > MAX) { O(G(' …\n')); break; }
|
|
806
|
+
const s = t.status === 'done' ? g('✓') : t.status === 'in_progress' ? y('◉') : t.status === 'cancelled' ? r('✗') : G('○');
|
|
807
|
+
const plan = t.plan ? G(` → ${t.plan.slice(0,30)}`) : '';
|
|
808
|
+
O(` ${s} ${t.title.slice(0,50)}${plan}\n`);
|
|
809
|
+
if (t.subtasks) for (const st of t.subtasks) {
|
|
810
|
+
const ss = st.status === 'done' ? g(' ✓') : G(' ○');
|
|
811
|
+
O(` ${ss} ${st.title.slice(0,40)}\n`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
O('\n');
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function showMemory(): Promise<void> {
|
|
818
|
+
const total = xmemory.totalEntries;
|
|
819
|
+
const types = {
|
|
820
|
+
semantic: xmemory.getByType('semantic', 3),
|
|
821
|
+
procedural: xmemory.getByType('procedural', 3),
|
|
822
|
+
episodic: xmemory.getByType('episodic', 3),
|
|
823
|
+
working: xmemory.getWorking().slice(-3),
|
|
824
|
+
};
|
|
825
|
+
O(b(c(' XMemory')) + G(` · ${total} 条记忆`) + '\n');
|
|
826
|
+
for (const [t, entries] of Object.entries(types)) {
|
|
827
|
+
if (entries.length === 0) continue;
|
|
828
|
+
const label = t === 'semantic' ? '知识' : t === 'procedural' ? '技能' : t === 'episodic' ? '经历' : '工作';
|
|
829
|
+
O(G(` [${label}]\n`));
|
|
830
|
+
for (const e of entries) { O(G(` ${e.content.slice(0, 120)}`) + '\n'); }
|
|
831
|
+
}
|
|
832
|
+
O('\n');
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async function showToolsBrief(tools: Tool[]): Promise<void> {
|
|
836
|
+
const cats = [...new Set(tools.map(t => t.category))];
|
|
837
|
+
for (const cat of cats.slice(0, 8)) {
|
|
838
|
+
const ns = tools.filter(t => t.category === cat).map(t => t.name);
|
|
839
|
+
O(B(`${cat}: `) + ns.join(', ') + '\n');
|
|
840
|
+
}
|
|
841
|
+
O(c(`\n /tools <category> 查看分类详情 (共 ${cats.length} 个分类)\n\n`));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function showToolsOf(cat: string, tools: Tool[]): Promise<void> {
|
|
845
|
+
const filtered = tools.filter(t => t.category === cat);
|
|
846
|
+
if (!filtered.length) { O(r(`分类不存在: ${cat}\n\n`)); return; }
|
|
847
|
+
O(b(c(` ${cat}`)) + '\n');
|
|
848
|
+
for (const t of filtered) {
|
|
849
|
+
const safe = TOOL_SAFETY_MAP[t.name] === 'dangerous' ? r('⚠') : TOOL_SAFETY_MAP[t.name] === 'confirm' ? y('?') : g('✓');
|
|
850
|
+
O(` ${safe} ${c(t.name)} ${G(t.description.slice(0, 50))}\n`);
|
|
851
|
+
}
|
|
852
|
+
O('\n');
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
async function loadBuiltinTools(): Promise<Tool[]> {
|
|
856
|
+
const { builtinTools } = await import('../tools/builtin/index.js');
|
|
857
|
+
return builtinTools as Tool[];
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// ========== API ==========
|
|
861
|
+
|
|
862
|
+
interface StreamChunk {
|
|
863
|
+
type: 'text'|'thinking'|'tool_call_start'|'tool_call_args'|'tool_call_end'|'done'|'error';
|
|
864
|
+
content?: string; tool_call?: { id: string; name: string }; error?: string;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
async function callApi(opts: ReplOptions, msgs: Array<Record<string, unknown>>, tools: ToolDef[], retry = 0, cmt = 8192): Promise<AsyncIterable<StreamChunk>> {
|
|
868
|
+
const MR = 2, TO = 90_000;
|
|
869
|
+
const ac = new AbortController(); const t = setTimeout(() => ac.abort(), TO);
|
|
870
|
+
try {
|
|
871
|
+
const resp = await fetch(`${opts.baseUrl}/v1/chat/completions`, {
|
|
872
|
+
method: 'POST', signal: ac.signal,
|
|
873
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${opts.apiKey}` },
|
|
874
|
+
body: JSON.stringify({
|
|
875
|
+
model: opts.model,
|
|
876
|
+
messages: msgs,
|
|
877
|
+
tools: tools.length > 0 ? tools : undefined,
|
|
878
|
+
tool_choice: tools.length > 0 ? 'auto' : undefined,
|
|
879
|
+
stream: true,
|
|
880
|
+
max_tokens: cmt,
|
|
881
|
+
temperature: opts.temperature,
|
|
882
|
+
}),
|
|
883
|
+
});
|
|
884
|
+
if (!resp.ok) {
|
|
885
|
+
const et = await resp.text().catch(() => '');
|
|
886
|
+
if (resp.status === 401) throw new Error('API Key 无效: deeper config set api_key "sk-xxx"');
|
|
887
|
+
if ((resp.status === 429 || resp.status >= 500) && retry < MR) { const d = Math.min(1000*(retry+1),5000); await new Promise(r2 => setTimeout(r2, d)); return callApi(opts, msgs, tools, retry+1, cmt); }
|
|
888
|
+
throw new Error(`HTTP ${resp.status}: ${et.slice(0,200)}`);
|
|
889
|
+
}
|
|
890
|
+
if (!resp.body) throw new Error('空响应');
|
|
891
|
+
return sseIter(resp.body);
|
|
892
|
+
} catch (e: unknown) {
|
|
893
|
+
const m = (e instanceof Error ? e.message : String(e)).toLowerCase();
|
|
894
|
+
if ((m.includes('timeout')||m.includes('abort')||m.includes('econn')) && retry < MR) { const d = Math.min(1000*(retry+1),5000); await new Promise(r2 => setTimeout(r2, d)); return callApi(opts, msgs, tools, retry+1, cmt); }
|
|
895
|
+
throw e;
|
|
896
|
+
} finally { clearTimeout(t); }
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
async function* sseIter(body: ReadableStream<Uint8Array>): AsyncIterable<StreamChunk> {
|
|
900
|
+
const reader = body.getReader(); const dec = new TextDecoder(); let buf = '';
|
|
901
|
+
try {
|
|
902
|
+
while (true) {
|
|
903
|
+
const { done, value } = await reader.read(); if (done) break;
|
|
904
|
+
buf += dec.decode(value, { stream: true });
|
|
905
|
+
if (buf.length > 65536) buf = buf.slice(-32768);
|
|
906
|
+
const lines = buf.split('\n'); buf = lines.pop() || '';
|
|
907
|
+
for (const line of lines) {
|
|
908
|
+
const tr = line.trim(); if (!tr || tr.startsWith(':')) continue; if (!tr.startsWith('data: ')) continue;
|
|
909
|
+
const d = tr.slice(6).trim(); if (d === '[DONE]') { yield { type: 'done' }; return; }
|
|
910
|
+
try {
|
|
911
|
+
const p = JSON.parse(d); const ch = p.choices?.[0]; if (!ch) continue;
|
|
912
|
+
const dl = ch.delta;
|
|
913
|
+
if (dl?.content) yield { type: 'text', content: dl.content };
|
|
914
|
+
if (dl?.reasoning_content) yield { type: 'thinking', content: dl.reasoning_content };
|
|
915
|
+
if (dl?.tool_calls) for (const tc of dl.tool_calls) {
|
|
916
|
+
if (tc.id) yield { type: 'tool_call_start', tool_call: { id: tc.id, name: tc.function?.name || '' } };
|
|
917
|
+
if (tc.function?.arguments) yield { type: 'tool_call_args', content: tc.function.arguments };
|
|
918
|
+
if (tc.id && tc.function?.arguments) yield { type: 'tool_call_end' };
|
|
919
|
+
}
|
|
920
|
+
if (ch.finish_reason === 'tool_calls') yield { type: 'tool_call_end' };
|
|
921
|
+
} catch { /* skip */ }
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
} catch (e: unknown) {
|
|
925
|
+
if (e instanceof Error && e.name === 'AbortError') { /* ok */ }
|
|
926
|
+
else yield { type: 'error', error: e instanceof Error ? e.message : String(e) };
|
|
927
|
+
} finally { try { reader.releaseLock(); } catch { /* */ } }
|
|
928
|
+
yield { type: 'done' };
|
|
929
|
+
}
|