dsh-thesis 1.0.0 → 2.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/LICENSE +21 -21
- package/NOTICE.md +1 -1
- package/README.md +48 -204
- package/docs/autopilot.md +73 -0
- package/docs/config.md +113 -0
- package/{DESIGN.md → docs/design.md} +94 -73
- package/docs/host-api.md +728 -640
- package/docs/release.md +126 -0
- package/docs/tools.md +76 -0
- package/docs/troubleshooting.md +88 -0
- package/docs/usage.md +87 -0
- package/lib/autopilot/index.d.ts +38 -0
- package/lib/autopilot/index.js +159 -0
- package/lib/autopilot/intent.d.ts +40 -0
- package/lib/autopilot/intent.js +82 -0
- package/lib/autopilot/messages.d.ts +22 -0
- package/lib/autopilot/messages.js +35 -0
- package/lib/autopilot/types.d.ts +60 -0
- package/lib/autopilot/types.js +10 -0
- package/lib/codewalk/engine.d.ts +1 -1
- package/lib/codewalk/engine.js +1 -1
- package/lib/codewalk/gate.d.ts +1 -1
- package/lib/codewalk/gate.js +1 -1
- package/lib/codewalk/host-types.d.ts +4 -31
- package/lib/codewalk/prestep.js +1 -1
- package/lib/codewalk/tools.js +2 -2
- package/lib/config.d.ts +17 -0
- package/lib/config.js +9 -1
- package/lib/dedup/index.js +0 -6
- package/lib/gates/messages.d.ts +7 -3
- package/lib/gates/messages.js +7 -4
- package/lib/index.d.ts +3 -1
- package/lib/index.js +10 -7
- package/lib/ingest/index.d.ts +1 -1
- package/lib/ingest/index.js +1 -3
- package/lib/intake/gate-rules.d.ts +6 -5
- package/lib/intake/gate-rules.js +6 -2
- package/lib/intake/index.d.ts +0 -17
- package/lib/intake/index.js +0 -51
- package/lib/intake/questions.d.ts +1 -1
- package/lib/intake/questions.js +1 -1
- package/lib/paper/lib/docx.js +70 -70
- package/lib/paper/lib/layout.d.ts +1 -1
- package/lib/paper/lib/layout.js +372 -372
- package/lib/paper/lib/ledger.d.ts +1 -1
- package/lib/paper/lib/ledger.js +1 -1
- package/lib/paper/lib/lit-cache.js +7 -7
- package/lib/paper/register.d.ts +2 -2
- package/lib/paper/register.js +2 -8
- package/lib/paper/tools/decision.js +5 -5
- package/lib/paper/tools/init.d.ts +1 -1
- package/lib/paper/tools/init.js +1 -1
- package/lib/paper/tools/lit.js +32 -32
- package/lib/ppt/index.d.ts +1 -2
- package/lib/ppt/index.js +1 -2
- package/lib/shared/define-tool.d.ts +1 -1
- package/lib/shared/define-tool.js +1 -1
- package/package.json +26 -21
- package/skills/code-walkthrough/SKILL.md +5 -3
- package/skills/thesis-defense/SKILL.md +1 -1
- package/skills/thesis-pipeline/SKILL.md +3 -3
- package/lib/codewalk/commands.d.ts +0 -23
- package/lib/codewalk/commands.js +0 -168
- package/lib/commands.d.ts +0 -33
- package/lib/commands.js +0 -60
- package/lib/dedup/commands.d.ts +0 -21
- package/lib/dedup/commands.js +0 -93
- package/lib/ingest/commands.d.ts +0 -23
- package/lib/ingest/commands.js +0 -74
- package/lib/paper/commands.d.ts +0 -13
- package/lib/paper/commands.js +0 -84
- package/lib/ppt/commands.d.ts +0 -28
- package/lib/ppt/commands.js +0 -71
package/docs/host-api.md
CHANGED
|
@@ -1,640 +1,728 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
z.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
readonly
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
interface
|
|
130
|
-
type: '
|
|
131
|
-
interface
|
|
132
|
-
interface
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
type
|
|
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
|
-
abstract
|
|
228
|
-
abstract
|
|
229
|
-
|
|
230
|
-
abstract
|
|
231
|
-
abstract
|
|
232
|
-
abstract
|
|
233
|
-
|
|
234
|
-
abstract
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
interface
|
|
246
|
-
interface
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
readonly
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
readonly invocation
|
|
355
|
-
readonly provider
|
|
356
|
-
}
|
|
357
|
-
interface
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
interface
|
|
362
|
-
|
|
363
|
-
readonly
|
|
364
|
-
readonly
|
|
365
|
-
readonly
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
`next()
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
'tools/
|
|
496
|
-
|
|
497
|
-
'
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
7.
|
|
1
|
+
# DSH 第三方插件宿主 API 参考
|
|
2
|
+
|
|
3
|
+
> **版本与兼容**:本文的签名摘自已安装产物,最初对着 **dsh 0.1.5-rc.1** 记录;
|
|
4
|
+
> 已用 **dsh 0.2.0-rc.2** 重新核对本插件实际用到的每一处(`defineTool` 的 DSL、
|
|
5
|
+
> `ctx.tools.register`、`agent/pre-step` 与 `tools/pre-execute` 的 waterfall、
|
|
6
|
+
> `ctx.skills.registerProvider`、`ctx.shell` / `ctx.sandboxPolicy`,以及自动驾驶用到的
|
|
7
|
+
> `ctx.userQuestions` / `ctx.goals` / 委派深度,见 §6.4),结论是契约未变。
|
|
8
|
+
> **唯一真正会挡住装载的不是 API,而是 manifest 的 peer range**——见 §7.7。
|
|
9
|
+
|
|
10
|
+
只记录契约:签名、类型、事件名、注册与加载规则。签名逐字摘自已安装产物的 `lib/types/**/*.d.ts` 与 `lib/*.js`,
|
|
11
|
+
冲突处已标注。安装根:`C:\Users\TTTT\AppData\Local\npm-cache\_npx\1e7f6d9597241db0\node_modules\@deepseek-ai\`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. 插件模块契约(`@deepseek-ai/cordis`)
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
namespace Plugin { // cordis/lib/types/registry.d.ts
|
|
19
|
+
interface Base<T = any> {
|
|
20
|
+
name?: string; Config?: StandardSchemaV1<any, T>; inject?: Inject
|
|
21
|
+
provide?: string | string[]; intercept?: Dict<boolean>
|
|
22
|
+
}
|
|
23
|
+
interface Function<T = any> extends Base<T> { (ctx: Context, config: T): any }
|
|
24
|
+
interface Constructor<T = any> extends Base<T> { new (ctx: Context, config: T): any }
|
|
25
|
+
interface Object<T = any> extends Base<T> { apply(ctx: Context, config: T): any }
|
|
26
|
+
}
|
|
27
|
+
type Inject<M = Dict> = (keyof M)[] | { [K in keyof M]?: M[K] }
|
|
28
|
+
// name=诊断名;Config=启动前校验 config 的 Standard Schema(schemastery 满足);inject=依赖服务名
|
|
29
|
+
|
|
30
|
+
export const name = 'my-plugin'
|
|
31
|
+
export const inject = ['fs'] // 服务齐备前保持 PENDING,不加载
|
|
32
|
+
export const Config = z.object({ stateDir: z.string().default('.my-plugin') })
|
|
33
|
+
export type MyConfig = z.infer<typeof Config>
|
|
34
|
+
export function apply(ctx: Context, config: MyConfig = {}): void { /* ... */ }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
函数插件用 `export const inject`,class 插件用 `static inject`,延迟到服务就绪再跑用 `ctx.inject(deps, cb)`。
|
|
38
|
+
**函数插件不要 `export default`**(只有 class service 用 default export)。
|
|
39
|
+
|
|
40
|
+
### 1.1 `ctx` 核心方法
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
interface Context { // cordis/lib/types/context.d.ts
|
|
44
|
+
root: this; baseUrl?: string
|
|
45
|
+
events: EventsService; logger: LoggerService; reflect: ReflectService
|
|
46
|
+
registry: RegistryService; fiber: Fiber
|
|
47
|
+
get<K extends string & keyof this>(name: K, strict?: boolean): undefined | this[K]
|
|
48
|
+
set<K extends string & keyof this>(name: K, value: undefined | this[K]): void
|
|
49
|
+
provide<K extends string & keyof this>(name: K, value: undefined | this[K]): () => void // 返回注销 disposer
|
|
50
|
+
// 以上三者各另有 (name: string, ...) => any 的非类型化重载
|
|
51
|
+
accessor(name: string, options: Omit<Property.Accessor, 'type'>): void
|
|
52
|
+
mixin<K extends string & keyof this>(name: K, mixins: (keyof this & keyof this[K])[] | Dict<string>): void
|
|
53
|
+
on<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
|
54
|
+
once<K extends keyof Events>(name: K, listener: Events[K], options?: boolean | EventOptions): () => boolean
|
|
55
|
+
emit<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): void
|
|
56
|
+
parallel<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promise<void>
|
|
57
|
+
serial<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): Promisify<ReturnType<Events[K]>>
|
|
58
|
+
bail<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
|
59
|
+
waterfall<K extends keyof Events>(name: K, ...args: Parameters<Events[K]>): ReturnType<Events[K]>
|
|
60
|
+
plugin<P extends Plugin>(plugin: P, ...args: Spread<GetPluginConfig<P>>): Fiber & PromiseLike<Fiber>
|
|
61
|
+
inject(deps: Inject, callback: Plugin.Function<void>): Fiber & PromiseLike<Fiber>
|
|
62
|
+
}
|
|
63
|
+
interface EventOptions { prepend?: boolean; global?: boolean }
|
|
64
|
+
interface Fiber {
|
|
65
|
+
uid: number | null; readonly ctx: Context; config: any
|
|
66
|
+
state: FiberState // PENDING=0 LOADING=1 ACTIVE=2 FAILED=3 DISPOSED=4 UNLOADING=5
|
|
67
|
+
readonly dispose: () => Promise<void>; await(): Promise<this>; restart(): Promise<void>
|
|
68
|
+
update(config: any, noSave?: boolean): void | Promise<void>
|
|
69
|
+
effect(execute: () => SyncEffect, label?: string): Disposable<Promise<void>>
|
|
70
|
+
effect(execute: () => Effect, label?: string): AsyncDisposable<Promise<void>>
|
|
71
|
+
getEffects(): EffectMeta[]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**disposal**:`provide` / `on` / `effect` 的 disposer 都归属当前 fiber,fiber 卸载时按注册**逆序**自动执行。
|
|
76
|
+
`ctx.effect(fn, label)` 的 `fn` 可返回 disposer、disposer 数组、`Promise<disposer>` 或 async generator:
|
|
77
|
+
`ctx.effect(() => { const t = setInterval(tick, 1000); return () => clearInterval(t) }, 'my-plugin.interval')`。
|
|
78
|
+
|
|
79
|
+
### 1.2 `Config`(schemastery `z`)
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
z.string(): Schema<string>; z.number(): Schema<number>; z.natural(): Schema<number>; z.percent(): Schema<number>
|
|
83
|
+
z.boolean(): Schema<boolean>; z.date(): Schema<string | Date, Date>; z.regExp(flag?): Schema<string | RegExp, RegExp>
|
|
84
|
+
z.array<X>(inner: X): Schema<TypeS<X>[], TypeT<X>[]>
|
|
85
|
+
z.dict<X, Y extends Schema<any, string> = Schema<string>>(inner: X, sKey?: Y): Schema<Dict<...>, Dict<...>>
|
|
86
|
+
z.object<X extends Dict>(dict: X): Schema<ObjectS<X>, ObjectT<X>>
|
|
87
|
+
z.union<const X>(list: readonly X[]): Schema<TypeS<X>, TypeT<X>>
|
|
88
|
+
z.intersect<const X>(list: readonly X[]): Schema<IntersectS<X>, IntersectT<X>>
|
|
89
|
+
z.transform<X, T>(inner: X, cb: (v: TypeS<X>, o: Options) => T, preserve?: boolean): Schema<TypeS<X>, T>
|
|
90
|
+
// 链式:.default(v) .description(text) .required(v?) .hidden(v?) .loose(v?) .disabled(v?)
|
|
91
|
+
// .collapse(v?) .role(text, extra?) .link(s) .comment(s) .min(n) .max(n) .step(n) .set(k, schema)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Cordis 用 `resolveConfig(runtime, config)` 把 `Config` 当 Standard Schema 校验;失败 → fiber `FAILED`,`await fiber` 抛 `ValidationError`。
|
|
95
|
+
服务声明可 `declare module '@deepseek-ai/cordis' { interface Context { myService: MyService } }`,也可不声明类型而用
|
|
96
|
+
`ctx.provide('myService', obj)` + `ctx.get('myService')`(本插件用后者规避对 `@deepseek-ai/dsh-*` 的类型依赖)。
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 2. 工具:`@deepseek-ai/dsh-tools`
|
|
101
|
+
|
|
102
|
+
### 2.1 `defineTool` 精确签名
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
export declare function defineTool<const S extends ParameterSchemaSpec, const O extends ValueSchemaSpec>(
|
|
106
|
+
options: DefineToolOptions<S, O>): ToolDefinition
|
|
107
|
+
|
|
108
|
+
export interface DefineToolOptions<S extends ParameterSchemaSpec, O extends ValueSchemaSpec> {
|
|
109
|
+
readonly name: string
|
|
110
|
+
readonly description: string
|
|
111
|
+
readonly parameters: S // 隐式开放对象根 —— 是 DSL,不是 JSON Schema
|
|
112
|
+
readonly output: {
|
|
113
|
+
readonly schema: O
|
|
114
|
+
render(args: InferArgs<S>, value: InferValue<NoInfer<O>>): ContentBlock[]
|
|
115
|
+
presentationMeta?(args: InferArgs<S>, value: InferValue<NoInfer<O>>): JsonValue
|
|
116
|
+
}
|
|
117
|
+
readonly timeoutMs?: number
|
|
118
|
+
isConcurrencySafe?(args: InferArgs<S>): boolean
|
|
119
|
+
execute(args: InferArgs<S>, exec: ToolRunContext): Promise<InferValue<NoInfer<O>>>
|
|
120
|
+
finalizeContent?(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): ContentBlock[] | undefined
|
|
121
|
+
presentCall?(args: InferArgs<S>): ToolCallView | undefined
|
|
122
|
+
presentResult?(args: InferArgs<S>, result: ToolResult): ToolResultView | undefined
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 2.2 参数/输出 schema DSL(全部字段描述符)
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
interface ValueSchemaAnnotations { description?: string; title?: string; default?: JsonValue; examples?: JsonValue }
|
|
130
|
+
interface StringValueSchemaSpec extends ValueSchemaAnnotations { type: 'string'; enum?: readonly string[]; const?: string }
|
|
131
|
+
interface NumberValueSchemaSpec extends ValueSchemaAnnotations { type: 'number'; enum?: readonly number[]; const?: number }
|
|
132
|
+
interface IntegerValueSchemaSpec extends ValueSchemaAnnotations { type: 'integer'; enum?: readonly number[]; const?: number }
|
|
133
|
+
interface BooleanValueSchemaSpec extends ValueSchemaAnnotations { type: 'boolean'; enum?: readonly boolean[]; const?: boolean }
|
|
134
|
+
interface NullValueSchemaSpec extends ValueSchemaAnnotations { type: 'null'; enum?: readonly null[]; const?: null }
|
|
135
|
+
interface ArrayValueSchemaSpec extends ValueSchemaAnnotations { type: 'array'; items?: ValueSchemaSpec }
|
|
136
|
+
interface ObjectValueSchemaSpec extends ValueSchemaAnnotations { // additionalProperties 必填:显式声明开放性
|
|
137
|
+
type: 'object'; properties?: ParameterSchemaSpec; additionalProperties: boolean }
|
|
138
|
+
interface JsonValueSchemaSpec extends ValueSchemaAnnotations { type: 'json' } // 仅供作者,编译为注解式 schema
|
|
139
|
+
interface OneOfValueSchemaSpec extends ValueSchemaAnnotations { // 至少两个分支
|
|
140
|
+
oneOf: readonly [ValueSchemaSpec, ValueSchemaSpec, ...ValueSchemaSpec[]] }
|
|
141
|
+
type ValueSchemaSpec = StringValueSchemaSpec | NumberValueSchemaSpec | IntegerValueSchemaSpec
|
|
142
|
+
| BooleanValueSchemaSpec | NullValueSchemaSpec | ArrayValueSchemaSpec
|
|
143
|
+
| ObjectValueSchemaSpec | JsonValueSchemaSpec | OneOfValueSchemaSpec
|
|
144
|
+
type ParameterPropertySpec = ValueSchemaSpec & { required?: true } // required 是每属性注解
|
|
145
|
+
type ParameterSchemaSpec = { [key: string]: ParameterPropertySpec; [key: symbol]: never }
|
|
146
|
+
type InferValue<S> = /* 精确推导,最多 16 层容器,之后退化为 JsonValue */
|
|
147
|
+
type InferArgs<S> = InferProperties<S, []>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`defineTool` 用 `parameterSchemaSpecToJsonSchema` / `valueSchemaSpecToJsonSchema` 编译成受支持的 JSON Schema 子集;
|
|
151
|
+
每次 `execute` 前用 `validateArgs(spec, args)`(= `validateJsonSchemaValue`)校验,违规抛 `ToolArgsError`(`code: 'INVALID_ARGS'`)。
|
|
152
|
+
|
|
153
|
+
### 2.3 约定与 `exec` 上下文
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
interface ToolExecutionInput {
|
|
157
|
+
readonly callId: ToolCallId; readonly rootCallId?: ToolCallId; readonly name: string
|
|
158
|
+
readonly arguments: unknown; readonly agent?: Agent // 无 agent 的组合为 undefined
|
|
159
|
+
readonly parent?: ToolExecutionToken; readonly signal: AbortSignal
|
|
160
|
+
}
|
|
161
|
+
interface ToolExecution extends ToolExecutionInput { readonly rootCallId: ToolCallId; readonly token: ToolExecutionToken }
|
|
162
|
+
interface ToolRunContext extends ToolExecution {
|
|
163
|
+
deferContext(context: UserMessage): void // 结果之后注入的上下文
|
|
164
|
+
concludeTurn(): void // 成功即结束当前轮
|
|
165
|
+
}
|
|
166
|
+
const cwd = exec.agent?.session.header.cwd // string | undefined ← 会话工作目录
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- `execute` 只返回**规范 JSON 值**;不符 `output.schema` → `ToolOutputError`(`violations: string[]`)。
|
|
170
|
+
> **不一致**:`.d.ts` 写 `constructor(toolName: string, violations: string[])`,`lib/index.js` 实为 `constructor(violations)`;以运行时为准。
|
|
171
|
+
- `render(args, value)` 的返回值就是模型看到的 `content`(`ContentBlock[]`,通常 `[{ type: 'text', text }]`)。
|
|
172
|
+
- `execute` 抛出 → `Error: <message>` 的 `isError` 结果;结构化身份走 `ToolFailure.info = { name, code }`。失败不结束轮次。
|
|
173
|
+
- 取消:调用前取消 → `ABORTED_BEFORE_DISPATCH`;调用后取消 → `ABORTED`。必须把 `exec.signal` 传给下游 I/O。
|
|
174
|
+
- `timeoutMs` 只是**声明**,注册表不强制;需要 `@deepseek-ai/dsh-tool-call-timeout-policy` 包装层。
|
|
175
|
+
- `Agent` 另有 `agent.ctx`(agent 作用域 Context)、`agent.inbox`、`agent.status`、`agent.options`、`agent.session`、
|
|
176
|
+
`cancel(cause, opts?)`、`whenIdle()`、`runMaintenance(task)`、`inject/steer/followup/send`。
|
|
177
|
+
|
|
178
|
+
### 2.4 注册
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
ctx.tools.register(definition: ToolDefinition): () => void // 返回注销 disposer
|
|
182
|
+
ctx.tools.guard(guard: (exec: Readonly<ToolExecution>) => string | undefined): () => void
|
|
183
|
+
ctx.tools.restrict(filter: { allow?: readonly string[]; deny?: readonly string[] }): () => void
|
|
184
|
+
ctx.tools.get(name: string, scope?: ScopeKey): ToolDefinition | undefined
|
|
185
|
+
ctx.tools.schemas(scope?: ScopeKey): ToolSchema[]
|
|
186
|
+
ctx.tools.execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
注册强制:同层重名抛错;`run_code` 为 PTC 保留名(注册/遮蔽均抛错);`output` 必须是带 `render` 函数的对象;
|
|
190
|
+
`output.schema` 必须落在受支持子集内;`timeoutMs` 若是则必须为正有限数。
|
|
191
|
+
流程:`tools/pre-execute` → `ctx.tools.guard()` → `tools/execute` → `tools/post-execute` → `finalizeContent` → `tools/result`。
|
|
192
|
+
`restrict()` 要求 agent 作用域 Context(`agent.ctx`);空过滤器与未知工具名(含保留名)抛错。
|
|
193
|
+
|
|
194
|
+
### 2.5 最小可运行工具
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
198
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
199
|
+
|
|
200
|
+
export function registerTools(ctx: Context, readAll: (cwd: string, rel: string, signal: AbortSignal) => Promise<string>): void {
|
|
201
|
+
ctx.tools.register(defineTool({
|
|
202
|
+
name: 'my_read',
|
|
203
|
+
description: 'Read one file inside the current session workspace.',
|
|
204
|
+
parameters: { path: { type: 'string', required: true }, maxLines: { type: 'integer' } },
|
|
205
|
+
output: {
|
|
206
|
+
schema: { type: 'object', additionalProperties: false, properties: {
|
|
207
|
+
path: { type: 'string', required: true }, text: { type: 'string', required: true } } },
|
|
208
|
+
render: (_a, v) => [{ type: 'text', text: v.text }],
|
|
209
|
+
},
|
|
210
|
+
async execute(args, exec) {
|
|
211
|
+
const cwd = exec.agent?.session.header.cwd ?? process.cwd()
|
|
212
|
+
return { path: args.path, text: await readAll(cwd, args.path, exec.signal) }
|
|
213
|
+
},
|
|
214
|
+
}))
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 3. 文件系统:`@deepseek-ai/dsh-fs`
|
|
221
|
+
|
|
222
|
+
`ctx.fs` 是抽象 `FileSystem extends Service`;**全部**成员如下(不省略,无其他方法):
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
abstract class FileSystem extends Service {
|
|
226
|
+
get sandboxMode(): SandboxMode | undefined // 基类/裸本机后端为 undefined(不限制)
|
|
227
|
+
abstract resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget>
|
|
228
|
+
abstract processPath(target: FsTarget): string
|
|
229
|
+
processPathFromHostPath(hostPath: string): string | undefined
|
|
230
|
+
abstract fileUrl(target: FsTarget): string
|
|
231
|
+
abstract contains(parent: FsTarget, child: FsTarget): boolean
|
|
232
|
+
abstract stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>
|
|
233
|
+
abstract lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise<FsPathInfo | undefined>
|
|
234
|
+
abstract readText(target: FsTarget, signal?: AbortSignal): Promise<string>
|
|
235
|
+
abstract streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>
|
|
236
|
+
abstract readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array>
|
|
237
|
+
abstract readByteRange(target: FsTarget, range: { offset: number; length: number }, signal?: AbortSignal): Promise<Uint8Array>
|
|
238
|
+
abstract listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>
|
|
239
|
+
abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent,
|
|
240
|
+
signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsWriteOutcome>
|
|
241
|
+
abstract editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion },
|
|
242
|
+
signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsEditOutcome>
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
interface FsTarget { targetKey: FsTargetKey; displayPath: string } // targetKey 不透明,禁止解析
|
|
246
|
+
interface FsInfo { version: FsVersion; type: 'file' | 'directory' | 'other'; size?: number }
|
|
247
|
+
interface FsPathInfo { version: FsVersion; type: 'file' | 'directory' | 'symlink' | 'other'; size?: number }
|
|
248
|
+
interface FsDirEntry { name: string; type: 'file' | 'directory' | 'other'; target: FsTarget; version?: FsVersion; size?: number }
|
|
249
|
+
type FsObservation = { kind: 'present'; version: FsVersion } | { kind: 'absent' }
|
|
250
|
+
type FsWriteIntent = { kind: 'createIfAbsent' } | { kind: 'replaceIfVersion'; version: FsVersion }
|
|
251
|
+
interface FsEditRequest { oldString: string; newString: string; replaceAll: boolean }
|
|
252
|
+
interface FsWriteOutcome { operation: 'create' | 'update'; version: FsVersion; before: string | null; after: string }
|
|
253
|
+
interface FsEditOutcome { version: FsVersion; before: string; after: string }
|
|
254
|
+
type FsErrorCode = 'FS_NOT_FOUND' | 'FS_NOT_DIRECTORY' | 'FS_NOT_TEXT' | 'FS_NOT_REGULAR_FILE' | 'FS_TOO_LARGE'
|
|
255
|
+
| 'FS_PERMISSION_DENIED' | 'FS_SANDBOX_DENIED' | 'FS_IO_ERROR' | 'FS_STALE_VERSION' | 'FS_NOT_OBSERVED'
|
|
256
|
+
| 'FS_AMBIGUOUS_EDIT' | 'FS_EDIT_NOT_FOUND' | 'FS_ABORTED'
|
|
257
|
+
class FsError extends HarnessError { readonly code: FsErrorCode; constructor(message: string, code: FsErrorCode, options?: ErrorOptions) }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### 3.1 关键契约(也是"缺失能力"的正式答案)
|
|
261
|
+
|
|
262
|
+
- **没有 `exists()`**:存在性 = `await fs.stat(target) !== undefined`(或 `lstat(path)`)。
|
|
263
|
+
- **没有递归列举**:`listDir` 只返回直接子项,按名字稳定排序、不含内容;递归/glob/分页/搜索要自己实现。
|
|
264
|
+
- **没有 delete / rename / copy / watch**:整个 seam 就是上面 13 个原语(README 明写"只有十三个原语")。
|
|
265
|
+
- **变更只支持文本**:`writeText`/`editText` 遇二进制或非 UTF-8 → `FS_NOT_TEXT`;`readBytes`/`readByteRange` 是原始字节**读**原语,
|
|
266
|
+
**二进制安全的写入不存在**。
|
|
267
|
+
- **路径解析**:相对路径以 `opts.cwd` 为基准(省略用后端默认基准)。`resolve` 跟随符号链接得到稳定身份 → 同一文件不同路径产生
|
|
268
|
+
同一 `targetKey`;"不跟随最后一跳"用 `lstat(path, { cwd })`。`displayPath` 才是给模型/UI 的路径。
|
|
269
|
+
- **原子性**:写入与编辑都原子;编辑在可选 `expected.version` 下先校验新鲜度再字面匹配
|
|
270
|
+
(`FS_STALE_VERSION` / `FS_AMBIGUOUS_EDIT` / `FS_EDIT_NOT_FOUND`)。
|
|
271
|
+
- **上限**:`readBytes` 超 `maxBytes` → `FS_TOO_LARGE`(不截断);`readByteRange` 由调用方对 `length` 负责。
|
|
272
|
+
- **无 I/O deadline**;取消只有每原语上的可选 `AbortSignal`。
|
|
273
|
+
|
|
274
|
+
### 3.2 `fs/*` 策略事件(全部三个)
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
// 单槽决策 waterfall:第一个返回值即决策,绝不调用 next()。next() = 无条件写入。
|
|
278
|
+
'fs/write-intent'(target: FsTarget, actor: object | undefined,
|
|
279
|
+
next: () => FsWriteIntent | undefined | Promise<FsWriteIntent | undefined>): Promise<FsWriteIntent | undefined>
|
|
280
|
+
// 单槽决策 waterfall:next() = 无条件编辑。
|
|
281
|
+
'fs/edit-intent'(target: FsTarget, actor: object | undefined,
|
|
282
|
+
next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined>
|
|
283
|
+
// emit:同步记录;抛错会让工具调用失败,返回的 Promise 不会被 await。
|
|
284
|
+
'fs/observed'(target: FsTarget, observation: FsObservation, actor: object | undefined): void
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`actor` 是发起调用的执行对象(`dsh-tool-fs` 传 `exec`;策略按 `actor.name === 'edit' | 'write'` 判定)。
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## 4. 人类斜杠命令:`@deepseek-ai/dsh-commands`
|
|
292
|
+
|
|
293
|
+
> **本插件不用这一节**:dsh-thesis 2.x 一条斜杠命令都不注册(入口是 §6.4 的自动驾驶)。
|
|
294
|
+
> 保留这一节是因为宿主契约本身没变,别的插件可能用得上。
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
ctx.commands.register(definition: CommandDefinition): () => void
|
|
298
|
+
|
|
299
|
+
interface CommandDefinition {
|
|
300
|
+
readonly name: string // 斜杠后的小写名,不含 '/'
|
|
301
|
+
readonly description: string
|
|
302
|
+
readonly input?: CommandInputDescriptor
|
|
303
|
+
readonly recordInput?: boolean // 默认 true;设 false 让命令自己的领域事件持有输入载荷
|
|
304
|
+
readonly handler: (invocation: CommandInvocation) => CommandResult | Promise<CommandResult>
|
|
305
|
+
}
|
|
306
|
+
interface CommandInputDescriptor { readonly hint: string; readonly attachments?: boolean }
|
|
307
|
+
interface CommandInvocation {
|
|
308
|
+
readonly commandId: CommandId; readonly agent: Agent // 精确的接收 agent
|
|
309
|
+
readonly rawInput: string // 命令名之后的每个字节,含分隔空白
|
|
310
|
+
readonly attachments: readonly (ImageBlock | FileBlock)[]; readonly signal: AbortSignal
|
|
311
|
+
}
|
|
312
|
+
type CommandResult =
|
|
313
|
+
| { kind: 'success'; text?: string; sourceEventSeq?: SessionSeq }
|
|
314
|
+
| { kind: 'error'; text: string }
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**名称规则(运行时强制)**:`const COMMAND_NAME = /^[a-z][a-z0-9_-]*$/u`;不匹配 →
|
|
318
|
+
`TypeError: command name "X" must match /^[a-z][a-z0-9_-]*$/u`。同层重名注册抛错。
|
|
319
|
+
**命令行语法**:第 0 字节必须是 `/`,随后小写名,名后为行尾或**空白**;名后一切(含分隔空白)是 `rawInput`。
|
|
320
|
+
语法非法或名字未知的行由适配器拒绝,不会变成模型提示词。`agent.ctx` 下注册的定义只对该 agent 遮蔽全局同名定义。
|
|
321
|
+
**取当前会话 / cwd**:`CommandInvocation` 没有 session 字段 —— 经 agent 取:
|
|
322
|
+
`const cwd: string | undefined = invocation.agent.session.header.cwd`
|
|
323
|
+
**返回 UI 文本**:`{ kind: 'success', text }` 或 `{ kind: 'error', text }`,由分发 UI 直接渲染,**不进入模型历史**。
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const commands = ctx.get('commands') as { register(d: CommandDefinition): unknown } | undefined
|
|
327
|
+
if (commands === undefined) return // headless/ACP 组合没有命令面,静默跳过
|
|
328
|
+
|
|
329
|
+
commands.register({
|
|
330
|
+
name: 'my-status',
|
|
331
|
+
description: 'Show my plugin state for this session',
|
|
332
|
+
input: { hint: '[verbose]' },
|
|
333
|
+
handler: ({ agent, rawInput }) => {
|
|
334
|
+
const cwd = agent.session.header.cwd
|
|
335
|
+
if (cwd === undefined) return { kind: 'error', text: 'no session workspace' }
|
|
336
|
+
return { kind: 'success', text: `cwd: ${cwd}\ninput: ${rawInput.trim()}` }
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
生命周期:`execute()` 先追加仅写日志的 `command/run`,结算时追加 `command/done`(抛错/取消以 `kind: 'error'` 结算);
|
|
342
|
+
准入未通过的输入不记录任何事件。注册/注销触发 `commands/change`(emit,无参)。
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## 5. Skill:`@deepseek-ai/dsh-skill` + `dsh-skill-filesystem`
|
|
347
|
+
|
|
348
|
+
### 5.1 编程式注册(精确方法名与参数)
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
ctx.skills.register(skill: SkillRegistration): () => void
|
|
352
|
+
|
|
353
|
+
type SkillRegistration = Omit<SkillDefinition, 'invocation' | 'provider'> & {
|
|
354
|
+
readonly invocation?: SkillInvocationPolicy // 省略 = 模型与用户都为 true
|
|
355
|
+
readonly provider?: string // 省略 = 注册表自有的 'runtime'
|
|
356
|
+
}
|
|
357
|
+
interface SkillDefinition extends SkillSummary {
|
|
358
|
+
readonly content: string // 必填:指令正文
|
|
359
|
+
readonly path?: string; readonly metadata?: Readonly<Record<string, unknown>>
|
|
360
|
+
}
|
|
361
|
+
interface SkillSummary {
|
|
362
|
+
readonly name: string // kebab-case
|
|
363
|
+
readonly description: string; readonly whenToUse?: string
|
|
364
|
+
readonly invocation: SkillInvocationPolicy; readonly source: SkillSource
|
|
365
|
+
readonly provider: string; readonly resourceBase?: SkillResourceBase
|
|
366
|
+
}
|
|
367
|
+
interface SkillInvocationPolicy { readonly modelInvocable: boolean; readonly userInvocable: boolean }
|
|
368
|
+
type SkillSource = 'project-dsh' | 'project-agents' | 'runtime' | 'user-dsh' | 'user-agents' | 'custom' | 'bundled' | (string & {})
|
|
369
|
+
|
|
370
|
+
ctx.skills.registerProvider(create: (control: SkillProviderControl) => SkillProvider): () => void
|
|
371
|
+
interface SkillProviderControl { readonly signal: AbortSignal; readonly invalidate: () => void }
|
|
372
|
+
interface SkillProvider {
|
|
373
|
+
readonly name: string
|
|
374
|
+
readonly list: (o: SkillLookupOptions) => Promise<readonly SkillCandidate[] | SkillProviderObservation>
|
|
375
|
+
readonly get: (c: SkillCandidate, o: SkillLookupOptions) => Promise<SkillDefinition | undefined>
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`registerProvider` 同步注册;同层重名或使用保留名 `runtime` 抛错;disposer 或 fiber 卸载即注销并失效缓存。
|
|
380
|
+
同层同名 runtime 注册先到先得(后者得 no-op disposer + 警告)。
|
|
381
|
+
Skill 名文法:`const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/`(可用 `isSkillName(name)`)。
|
|
382
|
+
|
|
383
|
+
### 5.2 发现目录与优先级(rank 小者胜)
|
|
384
|
+
|
|
385
|
+
| rank | `source` | 路径 |
|
|
386
|
+
|---|---|---|
|
|
387
|
+
| 100 | `project-dsh` | `<projectRoot>/.dsh/skills` |
|
|
388
|
+
| 200 | `project-agents` | `<projectRoot>/.agents/skills` |
|
|
389
|
+
| 300 | `custom` | `Config.customSkillDirs` 各项 |
|
|
390
|
+
| 400 | `user-dsh` | `<dshHome>/skills`(跳过其 `.system` 子目录) |
|
|
391
|
+
| 500 | `user-agents` | `<agentsHome>/skills` |
|
|
392
|
+
| 600 | `bundled` | `Config.bundledSkillDir` / `$DSH_BUNDLED_SKILL_DIR`(`BUNDLED_SKILL_RANK`,`trustedHost`) |
|
|
393
|
+
|
|
394
|
+
`projectRoot` = 含 `.git` 的最近祖先目录,无则用查询 cwd;`dshHome` 默认 `$DSH_HOME` 或 `~/.dsh`,
|
|
395
|
+
`agentsHome` 默认 `$DSH_AGENTS_HOME` 或 `~/.agents`。发现深度固定为 1:`<root>/<name>/SKILL.md` 或 `<root>/<name>.md`,
|
|
396
|
+
**不支持嵌套 `**/SKILL.md`**。层间同名由就近层覆盖;**rank 只决定同层内去重**。
|
|
397
|
+
`includeDefaultRoots: false` 同时去掉项目根、用户根与 `$DSH_BUNDLED_SKILL_DIR` 默认值。
|
|
398
|
+
|
|
399
|
+
### 5.3 `SKILL.md` frontmatter 契约
|
|
400
|
+
|
|
401
|
+
```yaml
|
|
402
|
+
---
|
|
403
|
+
name: my-skill # 必填,kebab-case
|
|
404
|
+
description: One-line routing description. # 必填
|
|
405
|
+
whenToUse: Optional extra routing guidance. # 可选
|
|
406
|
+
metadata: { owner: team-x } # 可选,任意对象 → candidate.metadata
|
|
407
|
+
disable-model-invocation: true # 可选:true 则从模型面目录与 loader 排除
|
|
408
|
+
user-invocable: false # 可选:false 则从用户面命令排除
|
|
409
|
+
---
|
|
410
|
+
正文 Markdown。每次加载都重读文件,正文没有版本/缓存协议。
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
布尔键接受 YAML 布尔以及不分大小写的 `true/false`、`yes/no`、`on/off`、`1/0`;其他拼写或非布尔值 → 整个 skill 连警告一起丢弃。
|
|
414
|
+
旧键 `disableModelInvocation` / `modelInvocable` / `userInvocable` → 抛错 `frontmatter field "X" is unsupported; use "Y"`。
|
|
415
|
+
无 frontmatter、缺 `name`/`description`、名字非法、YAML 失败 → 记警告跳过。
|
|
416
|
+
bundle 的相对资源基底是 `<name>/` 目录;渲染给模型用 `renderSkillContent(skill)` 得到规范 `<skill_content>` 块。
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## 6. Agent 循环钩子
|
|
421
|
+
|
|
422
|
+
`dsh-agent` 在 cordis 的 `Events` 上声明钩子。**没有** `agent/post-step`,也**没有** `agent/post-tool`:步骤结束是会话日志事件
|
|
423
|
+
`step/end`;工具结果观测用 `tools/result`(管线)或会话事件 `tool/result`(日志)。
|
|
424
|
+
|
|
425
|
+
### 6.1 `agent/pre-step`(注入消息 / 拒绝步骤)—— 完整示例
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
'agent/pre-step'(
|
|
429
|
+
this: Scoped<Agent>,
|
|
430
|
+
payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal },
|
|
431
|
+
next: () => Promise<PreStepDecision>,
|
|
432
|
+
): Promise<PreStepDecision>
|
|
433
|
+
|
|
434
|
+
type PreStepDecision = { kind: 'reject' } | { kind: 'enter'; messages: UserMessage[]; startsRequestSeries?: true }
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
`next()` 保持当前 messages;返回值替换进入该步骤的消息,或拒绝该步骤(mode `waterfall`,按 agent 作用域过滤派发)。
|
|
438
|
+
注入物必须是 `UserMessage`:`{ id, role: 'user', content: ContentBlock[], source }`,`source` 用
|
|
439
|
+
`{ kind: 'plugin', plugin: '<name>' } & ContextFormed`(`form: 'instructions' | 'catalog' | 'snapshot' | 'notice' | 'relay' | 'recall'`)。
|
|
440
|
+
`form` 决定客户端怎么渲染,且**每个形态只带自己那几个字段**(`ContextFormed` 是判别联合):
|
|
441
|
+
`sections` 只属于 `snapshot`,`summary` 只属于 `notice`,`instructions` 的渲染依据是
|
|
442
|
+
`changes`(工作区指令文件的改动清单,见 `dsh-agent-instructions`)。
|
|
443
|
+
形态带错字段不会报错,但客户端会判为不可读并退回**不透明**渲染(仍然可见、带生产者标签),
|
|
444
|
+
所以「注入了但样式不对」通常是 form/字段配错了。
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
448
|
+
import { randomUUID } from 'node:crypto'
|
|
449
|
+
|
|
450
|
+
export function registerPreStep(ctx: Context, buildCard: (cwd: string) => Promise<string | undefined>): void {
|
|
451
|
+
ctx.on('agent/pre-step', async (payload, next) => {
|
|
452
|
+
const decision = await next()
|
|
453
|
+
if (decision.kind === 'reject' || payload.signal.aborted) return decision
|
|
454
|
+
const cwd = payload.agent.session.header.cwd
|
|
455
|
+
if (cwd === undefined) return decision
|
|
456
|
+
const note = await buildCard(cwd) // 你自己决定"注什么、何时注"
|
|
457
|
+
if (note === undefined) return decision
|
|
458
|
+
return {
|
|
459
|
+
kind: 'enter',
|
|
460
|
+
messages: [...decision.messages, {
|
|
461
|
+
id: randomUUID(), role: 'user', content: [{ type: 'text', text: note }],
|
|
462
|
+
source: { kind: 'plugin', plugin: 'my-plugin', form: 'snapshot', sections: [{ name: 'my-plugin', text: note }] },
|
|
463
|
+
}],
|
|
464
|
+
}
|
|
465
|
+
}, { prepend: true })
|
|
466
|
+
}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
若 bundle 不依赖 `@deepseek-ai/dsh-agent` 类型(本插件的处境),可把 `ctx.on` 断言成
|
|
470
|
+
`(name: string, listener: (payload: unknown, next: () => Promise<PreStepDecision>) => Promise<PreStepDecision>) => unknown`,
|
|
471
|
+
再手动收窄 payload。
|
|
472
|
+
|
|
473
|
+
### 6.2 `tools/pre-execute`(否决 / 审批)—— 完整示例
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
'tools/pre-execute'(
|
|
477
|
+
this: Scoped<ToolRuntime>,
|
|
478
|
+
exec: ToolExecution,
|
|
479
|
+
next: () => Promise<PreToolDecision>,
|
|
480
|
+
): Promise<PreToolDecision>
|
|
481
|
+
|
|
482
|
+
type PreToolDecision = { kind: 'allow' } | { kind: 'deny'; reason: string } | { kind: 'ask'; reason?: string }
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
`next()` 委托为 allow;缺失审批服务时 `ask` 降级为拒绝;参数**不可改写**(`exec.arguments` 已入日志)。
|
|
486
|
+
异步门禁必须观测 `exec.signal`(注册表在门禁结算后重新检查取消)。
|
|
487
|
+
单调否决请用 `ctx.tools.guard(fn)`:同步、只能拒绝、不能被后续监听器翻回允许。
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
491
|
+
|
|
492
|
+
const FORBIDDEN = new Set(['my_dangerous_tool'])
|
|
493
|
+
|
|
494
|
+
export function registerToolGate(ctx: Context): void {
|
|
495
|
+
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
496
|
+
if (FORBIDDEN.has(exec.name)) return { kind: 'deny', reason: `tool "${exec.name}" is disabled by my-plugin` }
|
|
497
|
+
if (exec.name === 'write') return { kind: 'ask', reason: 'my-plugin requires approval for writes' }
|
|
498
|
+
return next()
|
|
499
|
+
})
|
|
500
|
+
ctx.tools.guard((exec) => (FORBIDDEN.has(exec.name) ? 'blocked by my-plugin policy' : undefined))
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### 6.3 同族事件
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
'tools/execute'(exec: ToolDispatchExecution, next: () => Promise<ToolExecutionResult>): Promise<ToolExecutionResult>
|
|
508
|
+
'tools/post-execute'(exec: ToolExecution, result: Readonly<ToolExecutionResult>, next: () => Promise<PostToolDecision>): Promise<PostToolDecision>
|
|
509
|
+
'tools/result'(exec: Readonly<ToolExecution>, result: Readonly<ToolExecutionResult>): undefined // emit
|
|
510
|
+
'tools/change'(): void // emit
|
|
511
|
+
'tools/ptc-dispatch-log'(dispatch: PtcDispatchLog, next: () => Promise<ContentBlock[]>): Promise<ContentBlock[]>
|
|
512
|
+
'agent/session-start'({ agent: Agent; source: 'startup' | 'resume' | 'clear' | 'compact' }): void
|
|
513
|
+
'agent/turn-stopping'({ agent: Agent; turn: number; signal: AbortSignal }): Promise<void> | void // serial
|
|
514
|
+
'agent/created' | 'agent/disposed'({ agent: Agent }): void
|
|
515
|
+
'agent/status'({ agent: Agent; status: 'idle' | 'running' }): void
|
|
516
|
+
'agent/request'({ agent; turn; step; signal }, next: () => Promise<LlmCallConfig>): Promise<LlmCallConfig>
|
|
517
|
+
'agent/request-error'({ agent; turn; step; provider; failure; retryPolicy; signal }, next: () => Promise<RequestErrorAction>): Promise<RequestErrorAction>
|
|
518
|
+
'agent/error'({ agent: Agent; turn: number; step: number; error: unknown }): void
|
|
519
|
+
'commands/change'(): void; 'skills/change'(): void
|
|
520
|
+
|
|
521
|
+
type PostToolDecision =
|
|
522
|
+
| { kind: 'accept'; content?: ContentBlock[]; value?: never; additionalContexts?: UserMessage[] }
|
|
523
|
+
| { kind: 'accept'; value: JsonValue; content?: never; additionalContexts?: UserMessage[] }
|
|
524
|
+
| { kind: 'block'; feedback: ContentBlock[]; additionalContexts?: UserMessage[] }
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
会话日志事件(`SessionEventMap`,经 `ctx.on('session/event', ...)` 观测):`turn/start`、`turn/end`、`step/start`、`step/end`、
|
|
528
|
+
`user/message`、`system/message`、`assistant/message`、`assistant/attempt`、`tool/call`、`tool/result`。
|
|
529
|
+
所有带 `this: Scoped<Agent>` / `Scoped<ToolRuntime>` 的事件都按 agent 作用域过滤派发。
|
|
530
|
+
|
|
531
|
+
### 6.4 自动驾驶用到的四处契约(`thesis 2.x`)
|
|
532
|
+
|
|
533
|
+
插件不再注册命令,改为在 `agent/pre-step` 上主动把流程推起来。它依赖下面四处能力,
|
|
534
|
+
**每一处都是可选的**(缺失即降级,不抛错):
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
// 1) pre-step 的 payload 带 agent 本身(不只是 messages)
|
|
538
|
+
'agent/pre-step'(payload: { agent: Agent; messages: UserMessage[]; turn: number; step: number; signal: AbortSignal }): ...
|
|
539
|
+
|
|
540
|
+
// 2) 问人:结构化选项 + 结构化答案;不需要 tool call id
|
|
541
|
+
ctx.userQuestions.ask({ questions: AskUserQuestionItem[], agent?: Agent, signal?: AbortSignal })
|
|
542
|
+
: Promise<{ answers: { id: string; selected: string[]; custom?: string }[] }>
|
|
543
|
+
interface AskUserQuestionItem { id: string; question: string; header?: string; detail?: string
|
|
544
|
+
options?: { label: string; description?: string }[]; multiSelect?: boolean }
|
|
545
|
+
|
|
546
|
+
// 3) 建目标:宿主的目标轮驱动器(`dsh-goal-round-driver`)会渲染续跑提示并调 agent.followup()
|
|
547
|
+
ctx.goals.create(agent: Agent, request: { objective: string; maxGoalRounds?: number }): GoalView
|
|
548
|
+
|
|
549
|
+
// 4) 委派深度:子 agent 的会话头里 subagentDepth > 0,据此只对主 agent 生效
|
|
550
|
+
delegationDepthOf(agent) = max(agent.session.header.subagentDepth ?? 0, agent.options.subagentDepth ?? 0)
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
两条实测结论:
|
|
554
|
+
|
|
555
|
+
- `ctx.userQuestions.ask` 的 **answerer 是 UI 侧的 waterfall**(`user-questions/request`)。
|
|
556
|
+
传了 `agent` 时它只接受**活的 runtime root**(`CALLER_NOT_LIVE` / `DELEGATED_CALLER` / `NO_PROVIDER`
|
|
557
|
+
都是可能结果),所以插件把「问不到答案」当作正常退化,而不是错误。
|
|
558
|
+
- `askTimed(request, callId, timeoutMs)` 需要**工具调用 id**,插件侧拿不到;因此插件只用 `ask`,
|
|
559
|
+
并接受「用户不答就挂住这一轮、按停止键即取消」的行为。
|
|
560
|
+
|
|
561
|
+
未在真实 agent 会话里验证过的一点:**没有 tool call 承载的问题卡片能否被 Web UI 渲染**。
|
|
562
|
+
若不能,日志会写 `意图选择题没有拿到答案`,插件退化为单轮,不阻塞对话。
|
|
563
|
+
|
|
564
|
+
---
|
|
565
|
+
|
|
566
|
+
## 7. CLI:`@deepseek-ai/dsh`(profile 与插件安装)
|
|
567
|
+
|
|
568
|
+
### 7.1 本地安装的确切命令行
|
|
569
|
+
|
|
570
|
+
`dsh plugin` 是 **pnpm 的薄转发器**(`lib/plugin-Ddi42qoW.js`):按需初始化 profile → 在 profile 目录执行
|
|
571
|
+
`spawnSync('pnpm', args, { cwd: profileDir, stdio: 'inherit' })` → 按安装结果校正 `dsh.profile.bundles`。
|
|
572
|
+
|
|
573
|
+
```sh
|
|
574
|
+
# 本地目录:相对路径会被锚定到"你执行 dsh 的目录",而不是 profile 目录
|
|
575
|
+
dsh plugin --profile web add /abs/path/to/dsh-my-plugin
|
|
576
|
+
dsh plugin --profile web add ../dsh-my-plugin # → <cwd>/../dsh-my-plugin
|
|
577
|
+
dsh plugin --profile web add file:../dsh-my-plugin # file:/link: 前缀保留
|
|
578
|
+
dsh plugin --profile web add link:../dsh-my-plugin
|
|
579
|
+
dsh plugin --profile web remove dsh-my-plugin
|
|
580
|
+
dsh plugin --profile web install
|
|
581
|
+
dsh plugin --profile web why dsh-my-plugin
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
只有**相对路径规格**会被重写为绝对路径:`/^(?<prefix>(?:file|link):)?(?<path>\.{1,2}(?:[/\\].*)?)$/`;
|
|
585
|
+
绝对路径、registry 名、`git+`/`github:`、tarball 规格原样透传。pnpm 退出码非 0 → 只打印
|
|
586
|
+
`pnpm failed in profile directory <dir>`;参数形如 `^git\+|^github:|\.git(?:#|$)` 时另提示需在
|
|
587
|
+
`<profileDir>/pnpm-workspace.yaml` 的 `allowBuilds` 放行 prepare 脚本。`--profile` 是 `plugin` 的 `requiredOption`,
|
|
588
|
+
无参数报错,profile 名 `desktop` 被拒绝(Electron 独占)。**没有** `dsh plugin link` 子命令 —— 用 pnpm 的 `link:` 规格。
|
|
589
|
+
|
|
590
|
+
### 7.2 `dsh.bundle.patch` 如何被消费
|
|
591
|
+
|
|
592
|
+
```json
|
|
593
|
+
{ "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } }
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
`dsh plugin` 跑完 pnpm 后调用 `reconcilePlugins(before, profileDir)`:遍历 `dependencies`,对每个包 `resolveBundleDir()`
|
|
597
|
+
并读 manifest,若 `dsh.bundle.patch !== undefined` 就把包名**追加**进 `dsh.profile.bundles`;不再声明或已从依赖移除的包移出列表。
|
|
598
|
+
未声明 `dsh.bundle` 的依赖只是普通依赖,并打印一条 `declares no dsh.bundle` 警告(不是错误)。校正基于**安装后的实际状态**
|
|
599
|
+
而非依赖 diff,因此 `update` 让某包获得 `dsh.bundle` 后会自动激活。profile 模板自带的内置 bundle 不是依赖,永不被改动。
|
|
600
|
+
`DshBundleManifest.patch` 是相对包目录的路径,`loadProfile` 解析为 `patchPath` 与 `patches: PatchOptions[]`。
|
|
601
|
+
|
|
602
|
+
### 7.3 `cordis.patch.yml` schema
|
|
603
|
+
|
|
604
|
+
文件是**顶层 YAML 数组**,元素为 `PatchOptions`(`@deepseek-ai/cordis-plugin-include`):
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
interface PatchOptions {
|
|
608
|
+
id?: string; insert?: EntryOptions[]; name?: string; config?: any
|
|
609
|
+
group?: boolean | null; disabled?: boolean | null
|
|
610
|
+
inject?: any; intercept?: any; isolate?: any
|
|
611
|
+
[key: string]: any
|
|
612
|
+
}
|
|
613
|
+
interface EntryOptions {
|
|
614
|
+
id: string // 必填:包含树内稳定 id
|
|
615
|
+
name: string // 必填:模块说明符(相对名按配置目录解析,裸包名按 baseUrl)
|
|
616
|
+
config?: any; group?: boolean | null; disabled?: boolean | null; inject?: Inject | null
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
语义(`applyEntryPatches`,逐字对齐 `dsh-app-boot/lib/index.js`):
|
|
621
|
+
|
|
622
|
+
- 有 `insert`:带 `id` → 命中该 entry,要求它是 `group`,把 `insert` 追加进它的 `config` 数组;找不到 → 警告
|
|
623
|
+
`patch insert: entry "X" not found`;非 group → 警告并跳过。无 `id` → 追加到**根数组**。
|
|
624
|
+
- 无 `insert`:`id` 必填;命中目标后,若给了 `name` 且与目标 `name` 不符 → 警告并跳过;否则把其余键逐个覆盖到目标。
|
|
625
|
+
- 输入永不被修改(`structuredClone`);同一列表内后面的 patch 可命中前面 patch 插入的行;未命中任何行的 patch 只警告。
|
|
626
|
+
|
|
627
|
+
```yaml
|
|
628
|
+
# bundle 的最小 patch 层(dsh-thesis 的真实文件)
|
|
629
|
+
- insert:
|
|
630
|
+
- id: ai-learning
|
|
631
|
+
name: dsh-thesis
|
|
632
|
+
# config: { stateDir: .ai-learning }
|
|
633
|
+
---
|
|
634
|
+
- id: tools # 按 id 覆盖已有条目
|
|
635
|
+
config: { mode: both }
|
|
636
|
+
- id: my-row
|
|
637
|
+
disabled: true
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
### 7.4 profile 层叠顺序
|
|
641
|
+
|
|
642
|
+
以空 entry 列表为起点,依次叠加:① `dsh.profile.bundles` 中每个 bundle 的 `cordis.patch.yml`(按列表顺序);
|
|
643
|
+
② profile 目录自己的 `cordis.patch.yml`(`PROFILE_PATCH_FILENAME`);③ `$DSH_HOME/cordis.patch.yml`(home 级);
|
|
644
|
+
④ `--patch <path>` 覆盖层(可重复,按给出顺序)。`patchReload: 'live' | 'startup'`:前者监视 profile 与 home 级 patch 文件
|
|
645
|
+
并事务性重放,后者只在启动时应用一次。`--dump-config` / `--dump-default-config` 可在不启动的情况下查看合成树。
|
|
646
|
+
|
|
647
|
+
### 7.5 运行时依赖解析与 "install farm" 兜底
|
|
648
|
+
|
|
649
|
+
- 共享兜底目录:`$DSH_HOME/profiles/node_modules`。`healProfilesModuleFallback()` 在每次 profile 启动时让它镜像
|
|
650
|
+
**dsh 安装自身的依赖闭包**;普通 Node 写 symlink(junction),打包可执行文件在跨进程锁下写 ESM 代理。
|
|
651
|
+
- 因此插件对 `@deepseek-ai/*` 的运行时 `import` 会沿 Node 父目录行走落到 `$DSH_HOME/profiles/node_modules`,
|
|
652
|
+
**无需自己安装 peer**(`@deepseek-ai/cordis` 即典型)。
|
|
653
|
+
- bundle 名解析**双锚**:先从 dsh 安装目录解析(保证 `@deepseek-ai/dsh-base` 永远来自同一安装),再从 profile 目录解析。
|
|
654
|
+
pnpm 管理的 `profile/node_modules` 条目优先;profile 私有链接只补"仅被所选 bundle 携带"的包,且不影响别的 profile。
|
|
655
|
+
- 建议:`@deepseek-ai/cordis` 放 `peerDependencies`;安装闭包之外的包放 `dependencies`。
|
|
656
|
+
- **必须发布编译后的 JS**:dsh CLI 依赖中没有 TypeScript 运行时加载器(`tsx`/`jiti` 均不在 `@deepseek-ai/dsh` 与
|
|
657
|
+
`dsh-app-boot` 的依赖中),`lib/bin.js` 是纯 ESM import。让 `main` 指向 `lib/index.js`,`exports` 同时给出 `types`
|
|
658
|
+
与 `default`。源码执行(vite/tsx/plain ESM)只适用于在 dsh 仓库内用 `pnpm dsh` 的开发路径。
|
|
659
|
+
|
|
660
|
+
### 7.6 名称与字符规则
|
|
661
|
+
|
|
662
|
+
| 对象 | 规则 | 违反后果 |
|
|
663
|
+
|---|---|---|
|
|
664
|
+
| 命令名 | `/^[a-z][a-z0-9_-]*$/u` | 注册时 `TypeError` |
|
|
665
|
+
| skill 名 | `/^[a-z0-9]+(?:-[a-z0-9]+)*$/` | 注册时 `Error: invalid skill name "X"`;文件发现时警告跳过 |
|
|
666
|
+
| profile 名 | 非空、不含 `/` 或 `\`、非 `.`/`..`/`node_modules`、非 `desktop` | `Error: dsh: invalid profile name "X"` / CLI 拒绝 desktop |
|
|
667
|
+
| bundle 包名 | 必须是 Node 可解析的 npm 包名;作为 `dsh.profile.bundles` 项时必须声明 `dsh.bundle` | `profile bundle "X" declares no dsh.bundle in its package.json` |
|
|
668
|
+
| 工具名 | `run_code` 被 PTC 传输保留 | `tool name "run_code" is reserved ...` |
|
|
669
|
+
| Loader entry `id` | 包含树内稳定唯一、非空 | 由 Loader entry 树裁决 |
|
|
670
|
+
|
|
671
|
+
### 7.7 插件兼容性预检(peer 版本门)—— 最容易被忽略的一条装载规则
|
|
672
|
+
|
|
673
|
+
`dsh-app-boot` 的 `preflight()`(由 `prepareProfileEntries()` 在装载前对**每一行**调用)
|
|
674
|
+
只做一件事:把该行解析到某个 `package.json`(`manifestOf()`:裸包名沿
|
|
675
|
+
`createRequire(parentURL).resolve.paths()` 逐级找 `<dir>/<bare>/package.json`;
|
|
676
|
+
`file:` / 绝对路径则从解析出的真实文件向上找最近的 `package.json`),然后执行
|
|
677
|
+
`evaluatePluginCompatibility(manifest, exemptions)`:
|
|
678
|
+
|
|
679
|
+
```ts
|
|
680
|
+
// 逐字对齐 dsh-app-boot/lib/index.js 的 evaluatePluginCompatibility()
|
|
681
|
+
for (const [name, range] of Object.entries(manifest.peerDependencies ?? {})) {
|
|
682
|
+
if (name !== '@deepseek-ai/dsh' && !name.startsWith('@deepseek-ai/dsh-')) continue // 只查 dsh 系
|
|
683
|
+
const requirement = ['workspace:^', 'workspace:~', 'workspace:*'].includes(range)
|
|
684
|
+
? runtimeVersion : range
|
|
685
|
+
if (!semver.satisfies(runtimeVersion, requirement, { includePrerelease: true })) peers[name] = range
|
|
686
|
+
}
|
|
687
|
+
// peers 非空 → 返回 issue;exempted === false 时该行被 disabled
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
要点(都是踩过的坑):
|
|
691
|
+
|
|
692
|
+
- **运行期版本来自 `@deepseek-ai/dsh-app-boot` 自己的 `package.json`**
|
|
693
|
+
(`getDshRuntimeVersion()`),不是 `dsh` 的版本号,也不是插件的 `version`。
|
|
694
|
+
- **只有 `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` 被检查**:`@deepseek-ai/cordis`、
|
|
695
|
+
`@deepseek-ai/schemastery` 写什么都不影响这条门(第一方插件的惯例是 dsh 系**精确锁版**
|
|
696
|
+
`"0.2.0-rc.2"`、cordis 用 `~4.0.4`;第三方插件无法与 dsh 同步发版,因此本项目用
|
|
697
|
+
`>=0.2.0-rc.2 <0.3.0-0` 这种带下界的 range)。
|
|
698
|
+
- **`peerDependenciesMeta.optional` 不豁免**;`engines.dsh` 也不参与判定(只是声明)。
|
|
699
|
+
- **`includePrerelease: true` 让 `<0.3.0` 也接受 `0.3.0-rc.1`**(预发布版排序在正式版之前),
|
|
700
|
+
要真正卡住小版本边界得写 `<0.3.0-0`。
|
|
701
|
+
- 后果不是异常,而是**该行 `disabled` + 一行 stderr**:
|
|
702
|
+
`dsh: disabling profile plugin row "paper": Plugin <name>@<version> is incompatible with dsh <runtime>: peerDependencies {...}`
|
|
703
|
+
—— 现场表现是「插件装了但完全没反应」。可用 `dsh plugin allow-version` 给
|
|
704
|
+
`包名@版本` × `精确 dsh 版本` 开豁免(写进 profile 的 `compatibility.json`),
|
|
705
|
+
但正解是修 range。
|
|
706
|
+
- **这条门不执行插件代码,也不跑 `--dump-config`**:`--dump-config` 只组合配置树,
|
|
707
|
+
旧 range 也能组合成功。要观察它必须真的启动一次,或者直接用上面的函数判定
|
|
708
|
+
(本仓库的 `scripts/lib/host-compat.ts` 就是这么做的,`npm run test:host-load`
|
|
709
|
+
会同时检查**本仓库 manifest** 与 **profile 里已安装的那一份**)。
|
|
710
|
+
|
|
711
|
+
---
|
|
712
|
+
|
|
713
|
+
## 8. 无法确定的事项(含已检查的内容)
|
|
714
|
+
|
|
715
|
+
1. **工具名的字符文法**:`dsh-tools` 的 `register()` 只检查 `run_code` 保留名与同层重复,**没有**名字正则,`ToolSchema.name` 也无 brand;
|
|
716
|
+
`dsh-tools/lib/index.js` 的 SDK 渲染注释甚至以 `zz-\u{1E4D0}x` 这类名字为合法输入举例。我在已安装产物中找不到工具名字符白名单。
|
|
717
|
+
检查过:`dsh-tools/lib/index.js`(`register`、`defineTool`、`ts-types` 渲染)、`dsh-llm/lib/types/*.d.ts` 的 `ToolSchema`。
|
|
718
|
+
实际约束来自模型 Function Calling 惯例与 `run_code` 保留名,无法从本产物给出权威正则。
|
|
719
|
+
2. **"插件名允许字符"没有独立校验器**:`dsh plugin` 把包名交给 pnpm,加载时靠 Node 模块解析;唯一相近的显式规则是 7.6 的
|
|
720
|
+
profile 名与命令名。检查过:`dsh/lib/plugin-Ddi42qoW.js`、`dsh/lib/bin.js`、`dsh-app-boot/lib/index.js`、
|
|
721
|
+
`dsh-host-plugin-inventory/lib/types/*.d.ts`、`dsh-client-ui-settings-plugins/lib/client.js`。
|
|
722
|
+
3. `ctx.fs` 的存在性检查与递归列举**确实不存在**(合约边界,非搜索遗漏):抽象类成员即第 3 节列出的 13 个原语,
|
|
723
|
+
README.zh.md 亦明写"只有十三个原语""`listDir` 只列出一层"。
|
|
724
|
+
4. **`agent/post-step` 不存在**:`dsh-agent` 事件表止于 `agent/turn-stopping`;对应观测点是 `step/end`。
|
|
725
|
+
检查过 `dsh-agent/lib/types/runtime-types.d.ts` 全文 419 行。
|
|
726
|
+
5. `dsh plugin link` 子命令不存在(见 7.1)。
|
|
727
|
+
6. `ToolOutputError` 构造签名 `.d.ts` 与 `.js` 不一致(见 2.3)。
|
|
728
|
+
7. `defineTool` 的 `parameters` 在 README.zh.md 示例里长得像 JSON Schema,实际是 DSL 属性映射(见 2.1)。
|