dsh-code-ui 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -94
- package/lib/client.js +1780 -1319
- package/lib/index.js +746 -618
- package/package.json +81 -77
- package/README.en.md +0 -38
package/lib/index.js
CHANGED
|
@@ -1,618 +1,746 @@
|
|
|
1
|
-
// Host half of dsh-code-ui.
|
|
2
|
-
// Standard DeepSeek Harness bundle plugin — hand-written JS, no build step.
|
|
3
|
-
// Registers an RPC channel (via the shared Connection service) for the editor
|
|
4
|
-
// UI's file / reference / note operations, and a model tool get_referenced_snippets.
|
|
5
|
-
|
|
6
|
-
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
7
|
-
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
8
|
-
import z from "@deepseek-ai/schemastery";
|
|
9
|
-
import { spawn } from "node:child_process";
|
|
10
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
13
|
-
|
|
14
|
-
const CHANNEL = "/dsh-code-ui";
|
|
15
|
-
|
|
16
|
-
// 引用与备注:模块级内存存储,由 RPC 读写。
|
|
17
|
-
const references = [];
|
|
18
|
-
const notes = [];
|
|
19
|
-
let idSeq = 1;
|
|
20
|
-
|
|
21
|
-
function now() {
|
|
22
|
-
return new Date().toISOString();
|
|
23
|
-
}
|
|
24
|
-
function baseName(p) {
|
|
25
|
-
const parts = String(p || "").split(/[\\/]/).filter(Boolean);
|
|
26
|
-
return parts.length ? parts[parts.length - 1] : String(p || "");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// —— 自更新:版本自识 / registry 检查 / pnpm 安装(照 dsh-updater / dshmarket 实战经验)——
|
|
30
|
-
// 版本单点:name/version 一律从自身 package.json 读取,源码里绝不硬编码同步。
|
|
31
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
32
|
-
function readJsonSafe(p) {
|
|
33
|
-
try { return JSON.parse(readFileSync(p, "utf8")); } catch (_) { return null; }
|
|
34
|
-
}
|
|
35
|
-
const SELF_MANIFEST = readJsonSafe(join(HERE, "..", "package.json")) || { name: "dsh-code-ui", version: "0.0.0" };
|
|
36
|
-
const SELF_NAME = SELF_MANIFEST.name;
|
|
37
|
-
const SELF_VERSION = SELF_MANIFEST.version;
|
|
38
|
-
|
|
39
|
-
// profile 根 = 向上找 package.json 含 dsh.profile.bundles 的目录(不数层级、不硬编码路径)
|
|
40
|
-
function findProfileRoot() {
|
|
41
|
-
let dir = HERE;
|
|
42
|
-
for (let i = 0; i < 8; i++) {
|
|
43
|
-
const m = readJsonSafe(join(dir, "package.json"));
|
|
44
|
-
if (m && m.dsh && m.dsh.profile && Array.isArray(m.dsh.profile.bundles)) return dir;
|
|
45
|
-
const parent = dirname(dir);
|
|
46
|
-
if (parent === dir) return null;
|
|
47
|
-
dir = parent;
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// semver 比较(照 dshmarket updates.js:仅严格更高才算有更新,绝不把降级当更新)
|
|
53
|
-
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
54
|
-
function parseSemver(v) {
|
|
55
|
-
const m = SEMVER_RE.exec(String(v || "").trim());
|
|
56
|
-
if (m === null) return null;
|
|
57
|
-
return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] === undefined ? [] : m[4].split(".") };
|
|
58
|
-
}
|
|
59
|
-
function compareVersions(a, b) {
|
|
60
|
-
const pa = parseSemver(a);
|
|
61
|
-
const pb = parseSemver(b);
|
|
62
|
-
if (pa === null || pb === null) return null;
|
|
63
|
-
for (let i = 0; i < 3; i++) {
|
|
64
|
-
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i];
|
|
65
|
-
}
|
|
66
|
-
if (pa.pre.length === 0 || pb.pre.length === 0) return pb.pre.length - pa.pre.length;
|
|
67
|
-
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
|
68
|
-
const x = pa.pre[i];
|
|
69
|
-
const y = pb.pre[i];
|
|
70
|
-
if (x === undefined) return -1;
|
|
71
|
-
if (y === undefined) return 1;
|
|
72
|
-
if (x === y) continue;
|
|
73
|
-
const nx = /^\d+$/.test(x);
|
|
74
|
-
const ny = /^\d+$/.test(y);
|
|
75
|
-
if (nx && ny) return Number(x) - Number(y);
|
|
76
|
-
if (nx !== ny) return nx ? -1 : 1;
|
|
77
|
-
return x < y ? -1 : 1;
|
|
78
|
-
}
|
|
79
|
-
return 0;
|
|
80
|
-
}
|
|
81
|
-
function isUpgrade(installed, latest) {
|
|
82
|
-
if (!installed || !latest) return false;
|
|
83
|
-
const cmp = compareVersions(latest, installed);
|
|
84
|
-
return cmp !== null && cmp > 0;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const REGISTRY_DEFAULT = "https://registry.npmjs.org";
|
|
88
|
-
const REGISTRY_MIRROR = "https://registry.npmmirror.com";
|
|
89
|
-
|
|
90
|
-
// -- settings 命名空间:「插件配置」页按 host 服务的命名空间分发卡片 --
|
|
91
|
-
// 卡片的 key 必须是一个 host 实际注册的 settings 命名空间,否则卡片永不渲染
|
|
92
|
-
//(ui-settings-plugins 的 tab-store 只分发 key ∈ 已服务命名空间的卡片,照 dshmarket 的 settings.ts 做法)。
|
|
93
|
-
const CODE_UI_SETTINGS_NS = settingsNamespace("dsh-code-ui");
|
|
94
|
-
const CodeUiSettingsSchema = z.object({
|
|
95
|
-
// 检查更新用的 registry(只影响在线「检查」;安装仍走 profile 自己的 .npmrc)
|
|
96
|
-
updateRegistry: z.string().default(REGISTRY_DEFAULT),
|
|
97
|
-
// 编辑器总开关:false 时 client 端不挂载编辑器 UI(配置卡片始终保留,可随时再开)
|
|
98
|
-
enabled: z.boolean().default(true),
|
|
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
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (/
|
|
260
|
-
return "
|
|
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
|
-
updateRun.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
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
|
-
if (
|
|
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
|
-
if (endpoint === "
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const
|
|
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
|
-
return
|
|
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
|
-
|
|
1
|
+
// Host half of dsh-code-ui.
|
|
2
|
+
// Standard DeepSeek Harness bundle plugin — hand-written JS, no build step.
|
|
3
|
+
// Registers an RPC channel (via the shared Connection service) for the editor
|
|
4
|
+
// UI's file / reference / note operations, and a model tool get_referenced_snippets.
|
|
5
|
+
|
|
6
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
7
|
+
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
8
|
+
import z from "@deepseek-ai/schemastery";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { existsSync, readFileSync, watch } from "node:fs";
|
|
11
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const CHANNEL = "/dsh-code-ui";
|
|
15
|
+
|
|
16
|
+
// 引用与备注:模块级内存存储,由 RPC 读写。
|
|
17
|
+
const references = [];
|
|
18
|
+
const notes = [];
|
|
19
|
+
let idSeq = 1;
|
|
20
|
+
|
|
21
|
+
function now() {
|
|
22
|
+
return new Date().toISOString();
|
|
23
|
+
}
|
|
24
|
+
function baseName(p) {
|
|
25
|
+
const parts = String(p || "").split(/[\\/]/).filter(Boolean);
|
|
26
|
+
return parts.length ? parts[parts.length - 1] : String(p || "");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// —— 自更新:版本自识 / registry 检查 / pnpm 安装(照 dsh-updater / dshmarket 实战经验)——
|
|
30
|
+
// 版本单点:name/version 一律从自身 package.json 读取,源码里绝不硬编码同步。
|
|
31
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
function readJsonSafe(p) {
|
|
33
|
+
try { return JSON.parse(readFileSync(p, "utf8")); } catch (_) { return null; }
|
|
34
|
+
}
|
|
35
|
+
const SELF_MANIFEST = readJsonSafe(join(HERE, "..", "package.json")) || { name: "dsh-code-ui", version: "0.0.0" };
|
|
36
|
+
const SELF_NAME = SELF_MANIFEST.name;
|
|
37
|
+
const SELF_VERSION = SELF_MANIFEST.version;
|
|
38
|
+
|
|
39
|
+
// profile 根 = 向上找 package.json 含 dsh.profile.bundles 的目录(不数层级、不硬编码路径)
|
|
40
|
+
function findProfileRoot() {
|
|
41
|
+
let dir = HERE;
|
|
42
|
+
for (let i = 0; i < 8; i++) {
|
|
43
|
+
const m = readJsonSafe(join(dir, "package.json"));
|
|
44
|
+
if (m && m.dsh && m.dsh.profile && Array.isArray(m.dsh.profile.bundles)) return dir;
|
|
45
|
+
const parent = dirname(dir);
|
|
46
|
+
if (parent === dir) return null;
|
|
47
|
+
dir = parent;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// semver 比较(照 dshmarket updates.js:仅严格更高才算有更新,绝不把降级当更新)
|
|
53
|
+
const SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
54
|
+
function parseSemver(v) {
|
|
55
|
+
const m = SEMVER_RE.exec(String(v || "").trim());
|
|
56
|
+
if (m === null) return null;
|
|
57
|
+
return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre: m[4] === undefined ? [] : m[4].split(".") };
|
|
58
|
+
}
|
|
59
|
+
function compareVersions(a, b) {
|
|
60
|
+
const pa = parseSemver(a);
|
|
61
|
+
const pb = parseSemver(b);
|
|
62
|
+
if (pa === null || pb === null) return null;
|
|
63
|
+
for (let i = 0; i < 3; i++) {
|
|
64
|
+
if (pa.core[i] !== pb.core[i]) return pa.core[i] - pb.core[i];
|
|
65
|
+
}
|
|
66
|
+
if (pa.pre.length === 0 || pb.pre.length === 0) return pb.pre.length - pa.pre.length;
|
|
67
|
+
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
|
68
|
+
const x = pa.pre[i];
|
|
69
|
+
const y = pb.pre[i];
|
|
70
|
+
if (x === undefined) return -1;
|
|
71
|
+
if (y === undefined) return 1;
|
|
72
|
+
if (x === y) continue;
|
|
73
|
+
const nx = /^\d+$/.test(x);
|
|
74
|
+
const ny = /^\d+$/.test(y);
|
|
75
|
+
if (nx && ny) return Number(x) - Number(y);
|
|
76
|
+
if (nx !== ny) return nx ? -1 : 1;
|
|
77
|
+
return x < y ? -1 : 1;
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
function isUpgrade(installed, latest) {
|
|
82
|
+
if (!installed || !latest) return false;
|
|
83
|
+
const cmp = compareVersions(latest, installed);
|
|
84
|
+
return cmp !== null && cmp > 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const REGISTRY_DEFAULT = "https://registry.npmjs.org";
|
|
88
|
+
const REGISTRY_MIRROR = "https://registry.npmmirror.com";
|
|
89
|
+
|
|
90
|
+
// -- settings 命名空间:「插件配置」页按 host 服务的命名空间分发卡片 --
|
|
91
|
+
// 卡片的 key 必须是一个 host 实际注册的 settings 命名空间,否则卡片永不渲染
|
|
92
|
+
//(ui-settings-plugins 的 tab-store 只分发 key ∈ 已服务命名空间的卡片,照 dshmarket 的 settings.ts 做法)。
|
|
93
|
+
const CODE_UI_SETTINGS_NS = settingsNamespace("dsh-code-ui");
|
|
94
|
+
const CodeUiSettingsSchema = z.object({
|
|
95
|
+
// 检查更新用的 registry(只影响在线「检查」;安装仍走 profile 自己的 .npmrc)
|
|
96
|
+
updateRegistry: z.string().default(REGISTRY_DEFAULT),
|
|
97
|
+
// 编辑器总开关:false 时 client 端不挂载编辑器 UI(配置卡片始终保留,可随时再开)
|
|
98
|
+
enabled: z.boolean().default(true),
|
|
99
|
+
// 编辑器主题:dsh(默认,跟随 DSH 宿主外观) / system(跟随操作系统) / light(浅色) / dark(深色)
|
|
100
|
+
theme: z.string().default("dsh"),
|
|
101
|
+
// 布局模式:dock(右侧停靠,默认) / float(悬浮窗口)
|
|
102
|
+
layout: z.string().default("dock"),
|
|
103
|
+
// 界面模式:codeui(显示编辑器,默认) / native(原始风格-ui,仅留入口按钮)
|
|
104
|
+
uiMode: z.string().default("codeui"),
|
|
105
|
+
// 切换按钮样式:header(会话头 ⌨ 按钮,默认) / bar(悬浮模式条)
|
|
106
|
+
toggle: z.string().default("header"),
|
|
107
|
+
});
|
|
108
|
+
// 解析后的当前值:settings 服务未挂载/未写入时保持默认
|
|
109
|
+
const selfSettings = { updateRegistry: REGISTRY_DEFAULT, enabled: true, theme: "dsh", layout: "dock", uiMode: "codeui", toggle: "header" };
|
|
110
|
+
// settings 服务引用(「启用/禁用」开关写入用;由 installSelfSettings 捕获)
|
|
111
|
+
let settingsServiceRef = null;
|
|
112
|
+
function installSelfSettings(ctx) {
|
|
113
|
+
let source = () => ({ updateRegistry: selfSettings.updateRegistry, enabled: selfSettings.enabled, theme: selfSettings.theme, layout: selfSettings.layout, uiMode: selfSettings.uiMode, toggle: selfSettings.toggle });
|
|
114
|
+
installSettingsSection(
|
|
115
|
+
ctx,
|
|
116
|
+
CODE_UI_SETTINGS_NS,
|
|
117
|
+
CodeUiSettingsSchema,
|
|
118
|
+
{ updateRegistry: selfSettings.updateRegistry, enabled: selfSettings.enabled, theme: selfSettings.theme, layout: selfSettings.layout, uiMode: selfSettings.uiMode, toggle: selfSettings.toggle },
|
|
119
|
+
{
|
|
120
|
+
setSource: (current) => { source = current; },
|
|
121
|
+
onChange: () => {
|
|
122
|
+
try {
|
|
123
|
+
const v = source() || {};
|
|
124
|
+
if (typeof v.updateRegistry === "string" && /^https?:\/\//.test(v.updateRegistry)) {
|
|
125
|
+
selfSettings.updateRegistry = v.updateRegistry.replace(/\/+$/, "");
|
|
126
|
+
}
|
|
127
|
+
if (typeof v.enabled === "boolean") selfSettings.enabled = v.enabled;
|
|
128
|
+
if (v.theme === "dsh" || v.theme === "system" || v.theme === "light" || v.theme === "dark") selfSettings.theme = v.theme;
|
|
129
|
+
if (v.layout === "dock" || v.layout === "float") selfSettings.layout = v.layout;
|
|
130
|
+
if (v.uiMode === "codeui" || v.uiMode === "native") selfSettings.uiMode = v.uiMode;
|
|
131
|
+
if (v.toggle === "header" || v.toggle === "bar") selfSettings.toggle = v.toggle;
|
|
132
|
+
} catch (_) {}
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
// 捕获 settings 服务实例:卡片的「启用/禁用」开关经 set-enabled RPC 写入本命名空间
|
|
137
|
+
ctx.inject(["settings"], (sctx) => { settingsServiceRef = sctx.settings; });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function fetchLatestVersion(name, registry) {
|
|
141
|
+
const res = await fetch(registry.replace(/\/+$/, "") + "/" + encodeURIComponent(name) + "/latest", {
|
|
142
|
+
headers: { accept: "application/json" },
|
|
143
|
+
signal: AbortSignal.timeout(12000),
|
|
144
|
+
});
|
|
145
|
+
if (res.status === 404) return { notFound: true };
|
|
146
|
+
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
147
|
+
const meta = await res.json();
|
|
148
|
+
return { version: typeof meta.version === "string" ? meta.version : null };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 更新任务单例:perform-update 立即返回,client 轮询 get-update-progress
|
|
152
|
+
//(connection.rpc 底层是独立 HTTP POST,天然并发,长任务不会阻塞轮询)
|
|
153
|
+
const updateRun = {
|
|
154
|
+
active: false, startedAt: 0, source: null, spec: null,
|
|
155
|
+
phase: "idle", lines: [],
|
|
156
|
+
done: false, ok: false, timedOut: false, exitCode: null,
|
|
157
|
+
beforeVersion: null, afterVersion: null, error: null, hint: null,
|
|
158
|
+
};
|
|
159
|
+
function feedLine(text) {
|
|
160
|
+
updateRun.lines.push(String(text).slice(0, 300));
|
|
161
|
+
if (updateRun.lines.length > 150) updateRun.lines.shift();
|
|
162
|
+
updateRun.phase = String(text).slice(0, 160);
|
|
163
|
+
}
|
|
164
|
+
// pnpm --reporter=ndjson 逐行事件 -> 可读文本。字段名以 pnpm 11 实测为准:
|
|
165
|
+
// progress/fetching-progress 用 packageId,pnpm:stage 用 stage,_dependency_resolved 用 resolution;
|
|
166
|
+
// scope/context/execution-time 等无信息量 debug 事件直接丢弃;非 JSON 行(老版 pnpm)原样保留。
|
|
167
|
+
function makeLineFeeder() {
|
|
168
|
+
let buffer = "";
|
|
169
|
+
return (chunk) => {
|
|
170
|
+
buffer += chunk;
|
|
171
|
+
let nl;
|
|
172
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
173
|
+
const line = buffer.slice(0, nl).trim();
|
|
174
|
+
buffer = buffer.slice(nl + 1);
|
|
175
|
+
if (line === "") continue;
|
|
176
|
+
if (line.startsWith("{")) {
|
|
177
|
+
let t = null;
|
|
178
|
+
let parsed = false;
|
|
179
|
+
try {
|
|
180
|
+
const ev = JSON.parse(line);
|
|
181
|
+
parsed = true;
|
|
182
|
+
const n = ev && ev.name ? String(ev.name) : "";
|
|
183
|
+
const pkgId = (ev && (ev.pkgId || ev.packageId)) || "";
|
|
184
|
+
if (n === "pnpm:stage" && ev.stage) t = "[阶段] " + ev.stage;
|
|
185
|
+
else if ((n === "progress" || n === "pnpm:progress") && pkgId) t = "[解析] " + pkgId;
|
|
186
|
+
else if ((n === "fetching-progress" || n === "pnpm:fetching-progress") && pkgId) t = "[下载] " + pkgId;
|
|
187
|
+
else if (n === "pnpm:_dependency_resolved" && ev.resolution) t = "[依赖] " + ev.resolution;
|
|
188
|
+
else if (n === "lifecycle" && pkgId) t = "[构建] " + pkgId + " " + (ev.script || "");
|
|
189
|
+
else if ((n === "stats" || n === "pnpm:stats") && ev.added !== undefined) t = "[链接] 新增 " + ev.added + " 个包";
|
|
190
|
+
else if (n === "error" || (ev && ev.level === "error")) t = "[错误] " + (ev.message || (ev.err && ev.err.message) || "");
|
|
191
|
+
else if (n === "pnpm" && ev.msg) t = "[pnpm] " + ev.msg;
|
|
192
|
+
else if (n === "pnpm:summary") t = "[统计] 安装流程结束";
|
|
193
|
+
} catch (_) {}
|
|
194
|
+
if (parsed) {
|
|
195
|
+
if (t !== null) feedLine(t);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
feedLine(line);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Windows:pnpm 是 .cmd shim,必须走 cmd.exe /d /s /c + 逐 token 引号(避开 DEP0190 的 shell:true)
|
|
205
|
+
const WIN_CMD = process.platform === "win32";
|
|
206
|
+
const COMSPEC = process.env.ComSpec || "cmd.exe";
|
|
207
|
+
const CMD_METACHARS = /[\s"&|<>^()%!]/;
|
|
208
|
+
function quoteCmdArg(arg) {
|
|
209
|
+
return CMD_METACHARS.test(arg) ? '"' + arg.replace(/"/g, '""') + '"' : arg;
|
|
210
|
+
}
|
|
211
|
+
function selfSpawnEnv() {
|
|
212
|
+
const sep = WIN_CMD ? ";" : ":";
|
|
213
|
+
const nodeBin = dirname(process.execPath);
|
|
214
|
+
const parts = (process.env.PATH || "").split(sep).filter((x) => x !== "");
|
|
215
|
+
if (!parts.includes(nodeBin)) parts.push(nodeBin);
|
|
216
|
+
// CI 模式:pnpm 在无 TTY 下遇到交互提示会永久挂起
|
|
217
|
+
return Object.assign({}, process.env, { CI: "true", PATH: parts.join(sep) });
|
|
218
|
+
}
|
|
219
|
+
function killTree(child) {
|
|
220
|
+
if (WIN_CMD && child.pid !== undefined) {
|
|
221
|
+
try { spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], { stdio: "ignore" }); return; } catch (_) {}
|
|
222
|
+
}
|
|
223
|
+
try { child.kill("SIGKILL"); } catch (_) {}
|
|
224
|
+
}
|
|
225
|
+
function pnpmAdd(spec, profileRoot, extraArgs) {
|
|
226
|
+
return new Promise((resolveP) => {
|
|
227
|
+
const workspace = existsSync(join(profileRoot, "pnpm-workspace.yaml"));
|
|
228
|
+
const args = ["add"].concat(workspace ? ["-w"] : [], [spec, "--reporter=ndjson"], extraArgs);
|
|
229
|
+
feedLine("$ pnpm " + args.join(" "));
|
|
230
|
+
const stdio = ["ignore", "pipe", "pipe"];
|
|
231
|
+
let child;
|
|
232
|
+
if (!WIN_CMD) {
|
|
233
|
+
child = spawn("pnpm", args, { cwd: profileRoot, env: selfSpawnEnv(), stdio, shell: false });
|
|
234
|
+
} else {
|
|
235
|
+
const cmdline = ["pnpm"].concat(args).map(quoteCmdArg).join(" ");
|
|
236
|
+
child = spawn(COMSPEC, ["/d", "/s", "/c", '"' + cmdline + '"'], {
|
|
237
|
+
cwd: profileRoot, env: selfSpawnEnv(), stdio, shell: false, windowsVerbatimArguments: true,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
let timedOut = false;
|
|
241
|
+
const timer = setTimeout(() => { timedOut = true; killTree(child); }, 10 * 60 * 1000);
|
|
242
|
+
const feed = makeLineFeeder();
|
|
243
|
+
if (child.stdout) child.stdout.on("data", feed);
|
|
244
|
+
if (child.stderr) child.stderr.on("data", feed);
|
|
245
|
+
child.on("error", (err) => {
|
|
246
|
+
clearTimeout(timer);
|
|
247
|
+
feedLine("[spawn-error] " + err.message);
|
|
248
|
+
resolveP({ ok: false, exitCode: 127, timedOut: false });
|
|
249
|
+
});
|
|
250
|
+
child.on("close", (code) => {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
resolveP({ ok: code === 0 && !timedOut, exitCode: code == null ? -1 : code, timedOut });
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
// 失败归类 + 可操作建议(不把 pnpm 原始错误直接甩给用户)
|
|
257
|
+
function diagnoseUpdateFailure(output) {
|
|
258
|
+
const s = String(output || "");
|
|
259
|
+
if (/EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|network|proxy|socket hang up|fetch/i.test(s))
|
|
260
|
+
return "网络或 registry 连接出错,多为瞬时故障,点「重试」往往能成功;长期失败请检查代理/镜像设置。";
|
|
261
|
+
if (/npmmirror|No matching version|not found while fetching|404/i.test(s))
|
|
262
|
+
return "镜像可能尚未同步该版本。profile 的 .npmrc 已指向官方源;若刚发布,稍等几分钟再试。";
|
|
263
|
+
if (/EPERM|EACCES|EBUSY|permission denied/i.test(s))
|
|
264
|
+
return "文件被占用或权限不足(Windows 上常见于文件句柄未释放)。重启 DSH 后再点一次更新。";
|
|
265
|
+
if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts|allowBuilds/i.test(s))
|
|
266
|
+
return "pnpm 拦截了构建脚本(allowBuilds)。本插件无原生构建,通常可忽略;如反复出现请检查 profile 的 pnpm 配置。";
|
|
267
|
+
if (/ERR_PNPM_UNEXPECTED_STORE|store-dir/i.test(s))
|
|
268
|
+
return "pnpm store 位置不一致,可在 profile 目录执行 pnpm install 重建后再试。";
|
|
269
|
+
if (/ERR_PNPM_PUBLIC_HOIST_PATTERN_DIFF/i.test(s))
|
|
270
|
+
return "profile 的 node_modules 由旧版 pnpm 创建,先在 profile 目录执行一次 pnpm install 重建,再回来更新。";
|
|
271
|
+
if (/ERR_PNPM_ADDING_TO_ROOT|workspace root/i.test(s))
|
|
272
|
+
return "pnpm 拒绝在 workspace 根安装(-w 参数问题),请把此反馈给插件作者。";
|
|
273
|
+
return "未能识别的失败。可展开输出查看详情后重试,或手动在 profile 目录执行 pnpm add " + SELF_NAME + "@latest。";
|
|
274
|
+
}
|
|
275
|
+
async function runSelfUpdate(spec, profileRoot, source) {
|
|
276
|
+
const before = readJsonSafe(join(profileRoot, "node_modules", SELF_NAME, "package.json"));
|
|
277
|
+
Object.assign(updateRun, {
|
|
278
|
+
active: true, startedAt: Date.now(), source: source, spec: spec,
|
|
279
|
+
phase: "starting", lines: [], done: false, ok: false, timedOut: false, exitCode: null,
|
|
280
|
+
beforeVersion: before && before.version ? before.version : null,
|
|
281
|
+
afterVersion: null, error: null, hint: null,
|
|
282
|
+
});
|
|
283
|
+
let result = await pnpmAdd(spec, profileRoot, []);
|
|
284
|
+
let output = updateRun.lines.join("\n");
|
|
285
|
+
// 已知坑自动恢复:minimumReleaseAge 拦截 / 瞬时网络失败(各自动重试一次)
|
|
286
|
+
if (!result.ok && !result.timedOut && /ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION|NO_MATURE_MATCHING_VERSION|minimumReleaseAge/i.test(output)) {
|
|
287
|
+
feedLine("[code-ui] 命中 pnpm minimumReleaseAge 拦截,自动附加 --config.minimumReleaseAge=0 重试一次");
|
|
288
|
+
result = await pnpmAdd(spec, profileRoot, ["--config.minimumReleaseAge=0"]);
|
|
289
|
+
} else if (!result.ok && !result.timedOut && /EAI_AGAIN|ECONNREFUSED|ECONNRESET|ETIMEDOUT|ENOTFOUND|socket hang up|network/i.test(output)) {
|
|
290
|
+
feedLine("[code-ui] 瞬时网络失败,自动重试一次");
|
|
291
|
+
result = await pnpmAdd(spec, profileRoot, []);
|
|
292
|
+
}
|
|
293
|
+
output = updateRun.lines.join("\n");
|
|
294
|
+
if (result.ok) {
|
|
295
|
+
const after = readJsonSafe(join(profileRoot, "node_modules", SELF_NAME, "package.json"));
|
|
296
|
+
updateRun.afterVersion = after && after.version ? after.version : null;
|
|
297
|
+
if (updateRun.beforeVersion && updateRun.afterVersion === updateRun.beforeVersion) {
|
|
298
|
+
// pnpm 的 minimumReleaseAge 会静默保持旧版并退出 0——干净退出不等于更新成功
|
|
299
|
+
updateRun.ok = false;
|
|
300
|
+
updateRun.error = "pnpm 成功退出但版本没有变化";
|
|
301
|
+
updateRun.hint = "新版本可能被 minimumReleaseAge 静默保持旧版,或安装源与当前版本相同;可稍后重试。";
|
|
302
|
+
} else {
|
|
303
|
+
updateRun.ok = true;
|
|
304
|
+
feedLine("[完成] " + (updateRun.beforeVersion || "?") + " -> " + (updateRun.afterVersion || "?") + ",重启 DSH 后生效");
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
updateRun.error = result.timedOut ? "更新超时,已终止(可重试)" : "pnpm 失败(退出码 " + (result.exitCode == null ? "unknown" : result.exitCode) + ")";
|
|
308
|
+
updateRun.hint = diagnoseUpdateFailure(output);
|
|
309
|
+
}
|
|
310
|
+
updateRun.done = true;
|
|
311
|
+
updateRun.active = false;
|
|
312
|
+
updateRun.phase = updateRun.ok ? "done" : "failed";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// —— RPC 处理:文件系统 + 引用/备注 ——
|
|
316
|
+
// -- 文件监听:对 Code UI 已打开的文件建 fs.watch,外部修改记入事件队列 --
|
|
317
|
+
// client 每 1 秒 take-file-events 拉空队列(插件 RPC 是单向请求/响应,没有 host->浏览器推送通道,
|
|
318
|
+
// 用「真事件 + 轻量取事件」实现外部修改自动刷新)。
|
|
319
|
+
const fileWatchers = new Map(); // path -> FSWatcher
|
|
320
|
+
const fileEvents = []; // { path, ts },上限截断防膨胀
|
|
321
|
+
const writeSuppress = new Map(); // path -> 时间戳:Code UI 自己保存引发的回声事件不进队列
|
|
322
|
+
const FILE_EVENTS_MAX = 200;
|
|
323
|
+
function watchFilePath(path) {
|
|
324
|
+
if (typeof path !== "string" || !path || fileWatchers.has(path)) return;
|
|
325
|
+
let lastTs = 0;
|
|
326
|
+
let w = null;
|
|
327
|
+
try { w = watch(path, () => { }); } catch (_) { return; }
|
|
328
|
+
const onEvent = () => {
|
|
329
|
+
const now = Date.now();
|
|
330
|
+
if (now <= (writeSuppress.get(path) || 0)) return; // 自己保存的回声
|
|
331
|
+
if (now - lastTs < 300) return; // 去抖:Windows 一次保存常触发多次回调
|
|
332
|
+
lastTs = now;
|
|
333
|
+
fileEvents.push({ path: path, ts: now });
|
|
334
|
+
if (fileEvents.length > FILE_EVENTS_MAX) fileEvents.splice(0, fileEvents.length - FILE_EVENTS_MAX);
|
|
335
|
+
};
|
|
336
|
+
w.on("change", onEvent);
|
|
337
|
+
w.on("error", () => { unwatchFilePath(path); }); // 文件被删除/重命名:释放 watcher
|
|
338
|
+
fileWatchers.set(path, w);
|
|
339
|
+
}
|
|
340
|
+
function unwatchFilePath(path) {
|
|
341
|
+
const w = fileWatchers.get(path);
|
|
342
|
+
if (!w) return;
|
|
343
|
+
fileWatchers.delete(path);
|
|
344
|
+
try { w.close(); } catch (_) {}
|
|
345
|
+
}
|
|
346
|
+
function unwatchAllFiles() {
|
|
347
|
+
for (const p of [...fileWatchers.keys()]) unwatchFilePath(p);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function rpcHandler(ctx) {
|
|
351
|
+
const fs = ctx.get("fs");
|
|
352
|
+
let currentRootRef = detectWorkspaceRoot(ctx);
|
|
353
|
+
|
|
354
|
+
// 每次 get-root 动态回读会话工作区根,避免一次缓存导致拿不到当前项目目录
|
|
355
|
+
function baseRoot() {
|
|
356
|
+
if (currentRootRef) return currentRootRef;
|
|
357
|
+
return detectWorkspaceRoot(ctx);
|
|
358
|
+
}
|
|
359
|
+
function detectWorkspaceRoot(_ctx) {
|
|
360
|
+
// 1) 当前会话的真实工作区根(项目根):优先取正在发起调用的 agent 的 session cwd。
|
|
361
|
+
// sandboxPolicy.workspaceRoot 只是「部署回退根」,常是 DSH 进程 cwd 而非项目目录。
|
|
362
|
+
let root = null;
|
|
363
|
+
const agents = _ctx.get && _ctx.get("agents");
|
|
364
|
+
if (agents) {
|
|
365
|
+
try {
|
|
366
|
+
const init = agents.currentInitiator();
|
|
367
|
+
if (init && init.session && init.session.header && init.session.header.cwd) {
|
|
368
|
+
root = init.session.header.cwd;
|
|
369
|
+
}
|
|
370
|
+
} catch (_) {}
|
|
371
|
+
}
|
|
372
|
+
// 2) 回退到 sandboxPolicy 的部署回退根
|
|
373
|
+
if (!root) {
|
|
374
|
+
const sp = _ctx.get && _ctx.get("sandboxPolicy");
|
|
375
|
+
if (sp && typeof sp.workspaceRoot === "string" && sp.workspaceRoot) {
|
|
376
|
+
root = sp.workspaceRoot;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// 3) 再回退到进程 cwd
|
|
380
|
+
if (!root && typeof process !== "undefined" && process.cwd) {
|
|
381
|
+
try { root = process.cwd(); } catch (_) {}
|
|
382
|
+
}
|
|
383
|
+
return root;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return async (endpoint, payload, _signal) => {
|
|
387
|
+
try {
|
|
388
|
+
const args = payload && typeof payload === "object" ? payload : {};
|
|
389
|
+
|
|
390
|
+
if (endpoint === "get-root") {
|
|
391
|
+
const root = baseRoot();
|
|
392
|
+
return ok({ root });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (endpoint === "set-root") {
|
|
396
|
+
if (!args.path) return ok({ error: "no-path" });
|
|
397
|
+
currentRootRef = String(args.path);
|
|
398
|
+
return ok({ root: currentRootRef });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (endpoint === "list-dir") {
|
|
402
|
+
const path = args.path || currentRootRef;
|
|
403
|
+
if (!path) return ok({ error: "no-root" });
|
|
404
|
+
const target = await fs.resolve(path);
|
|
405
|
+
const entries = await fs.listDir(target);
|
|
406
|
+
const sep = String(path).includes("\\") ? "\\" : "/";
|
|
407
|
+
const list = entries.map((e) => {
|
|
408
|
+
let name = null;
|
|
409
|
+
let isDir = false;
|
|
410
|
+
if (e && typeof e === "object") {
|
|
411
|
+
name = e.name;
|
|
412
|
+
if (!name && e.path) name = baseName(e.path);
|
|
413
|
+
const flag = e.isDirectory !== undefined ? e.isDirectory : e.isDir;
|
|
414
|
+
if (typeof flag === "boolean") isDir = flag;
|
|
415
|
+
else {
|
|
416
|
+
const t = String(e.type || e.kind || "").toLowerCase();
|
|
417
|
+
isDir = t.indexOf("dir") >= 0 || t === "folder";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
name = name || "unknown";
|
|
421
|
+
const childPath = String(path).replace(/[\\/]+$/, "") + sep + name;
|
|
422
|
+
return { name, path: childPath, isDir };
|
|
423
|
+
});
|
|
424
|
+
list.sort(
|
|
425
|
+
(a, b) => (b.isDir ? 1 : 0) - (a.isDir ? 1 : 0) || a.name.localeCompare(b.name),
|
|
426
|
+
);
|
|
427
|
+
return ok({ path, entries: list });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (endpoint === "stat-path") {
|
|
431
|
+
if (!args.path) return ok({ error: "no-path" });
|
|
432
|
+
const target = await fs.resolve(args.path);
|
|
433
|
+
const info = await fs.stat(target);
|
|
434
|
+
// DSH fs.stat 返回 FsInfo { type: 'file' | 'directory' | 'other' },
|
|
435
|
+
// 没有 isDirectory/isFile 布尔字段;必须按 type 判定(原实现恒为 false,
|
|
436
|
+
// 导致目录路径被误报为「不存在」)。保留遗留字段兼容。
|
|
437
|
+
const t = info ? String(info.type || "") : "";
|
|
438
|
+
const isDir = !!(info && (info.isDirectory || info.isDir)) || t === "directory";
|
|
439
|
+
const isFile = !isDir && (!!(info && (info.isFile || info.type === "file")) || t === "file");
|
|
440
|
+
return ok({ path: args.path, isDir, isFile, exists: !!info });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (endpoint === "read-file") {
|
|
444
|
+
if (!args.path) return ok({ error: "no-path" });
|
|
445
|
+
const target = await fs.resolve(args.path);
|
|
446
|
+
const text = await fs.readText(target);
|
|
447
|
+
const MAX = 600000;
|
|
448
|
+
const truncated = text.length > MAX;
|
|
449
|
+
return ok({ path: args.path, content: truncated ? text.slice(0, MAX) : text, truncated });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (endpoint === "write-file") {
|
|
453
|
+
if (!args.path) return ok({ error: "no-path" });
|
|
454
|
+
if (typeof args.content !== "string") return ok({ error: "no-content" });
|
|
455
|
+
writeSuppress.set(args.path, Date.now() + 1500); // 自己的保存不进事件队列(回声抑制)
|
|
456
|
+
await fs.writeText(await fs.resolve(args.path), args.content);
|
|
457
|
+
return ok({ ok: true });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (endpoint === "watch-file") {
|
|
461
|
+
if (typeof args.path !== "string" || !existsSync(args.path)) return ok({ error: "无效路径" });
|
|
462
|
+
watchFilePath(args.path);
|
|
463
|
+
return ok({ ok: true, watching: fileWatchers.has(args.path) });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (endpoint === "unwatch-file") {
|
|
467
|
+
if (typeof args.path === "string") unwatchFilePath(args.path);
|
|
468
|
+
return ok({ ok: true });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (endpoint === "take-file-events") {
|
|
472
|
+
const events = fileEvents.splice(0, fileEvents.length);
|
|
473
|
+
return ok({ events: events });
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (endpoint === "add-reference") {
|
|
477
|
+
const id = "r" + (idSeq++);
|
|
478
|
+
const ref = {
|
|
479
|
+
id,
|
|
480
|
+
path: args.path || "",
|
|
481
|
+
language: args.language || "",
|
|
482
|
+
lineStart: args.lineStart || null,
|
|
483
|
+
lineEnd: args.lineEnd || null,
|
|
484
|
+
text: args.text || "",
|
|
485
|
+
kind: args.kind || "cite",
|
|
486
|
+
createdAt: now(),
|
|
487
|
+
};
|
|
488
|
+
references.push(ref);
|
|
489
|
+
return ok({ id, count: references.length, reference: ref });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (endpoint === "list-references") {
|
|
493
|
+
return ok({ references });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (endpoint === "delete-reference") {
|
|
497
|
+
const i = references.findIndex((r) => r.id === args.id);
|
|
498
|
+
if (i >= 0) references.splice(i, 1);
|
|
499
|
+
return ok({ ok: true });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (endpoint === "add-note") {
|
|
503
|
+
const id = "n" + (idSeq++);
|
|
504
|
+
const n = {
|
|
505
|
+
id,
|
|
506
|
+
path: args.path || "",
|
|
507
|
+
lineStart: args.lineStart || null,
|
|
508
|
+
lineEnd: args.lineEnd || null,
|
|
509
|
+
note: args.note || "",
|
|
510
|
+
createdAt: now(),
|
|
511
|
+
};
|
|
512
|
+
notes.push(n);
|
|
513
|
+
return ok({ id, note: n });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (endpoint === "list-notes") {
|
|
517
|
+
const path = args.path;
|
|
518
|
+
const list = path ? notes.filter((n) => n.path === path) : notes;
|
|
519
|
+
return ok({ notes: list });
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (endpoint === "delete-note") {
|
|
523
|
+
const i = notes.findIndex((n) => n.id === args.id);
|
|
524
|
+
if (i >= 0) notes.splice(i, 1);
|
|
525
|
+
return ok({ ok: true });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (endpoint === "get-self") {
|
|
529
|
+
const profileRoot = findProfileRoot();
|
|
530
|
+
return ok({
|
|
531
|
+
name: SELF_NAME,
|
|
532
|
+
version: SELF_VERSION,
|
|
533
|
+
profileRoot: profileRoot,
|
|
534
|
+
workspace: profileRoot ? existsSync(join(profileRoot, "pnpm-workspace.yaml")) : false,
|
|
535
|
+
updateRegistry: selfSettings.updateRegistry,
|
|
536
|
+
enabled: selfSettings.enabled !== false,
|
|
537
|
+
theme: selfSettings.theme,
|
|
538
|
+
layout: selfSettings.layout,
|
|
539
|
+
uiMode: selfSettings.uiMode,
|
|
540
|
+
toggle: selfSettings.toggle,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (endpoint === "set-toggle") {
|
|
545
|
+
const v = args.toggle;
|
|
546
|
+
if (v !== "header" && v !== "bar") return ok({ error: "toggle 需为 header / bar" });
|
|
547
|
+
if (!settingsServiceRef || typeof settingsServiceRef.update !== "function") {
|
|
548
|
+
return ok({ error: "settings 服务不可用,重启 DSH 后再试" });
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
await settingsServiceRef.update(CODE_UI_SETTINGS_NS, { toggle: v });
|
|
552
|
+
selfSettings.toggle = v;
|
|
553
|
+
return ok({ ok: true, toggle: v });
|
|
554
|
+
} catch (err) {
|
|
555
|
+
return ok({ error: "写入设置失败:" + ((err && err.message) || String(err)) });
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (endpoint === "set-uimode") {
|
|
560
|
+
const v = args.uiMode;
|
|
561
|
+
if (v !== "codeui" && v !== "native") return ok({ error: "uiMode 需为 codeui / native" });
|
|
562
|
+
if (!settingsServiceRef || typeof settingsServiceRef.update !== "function") {
|
|
563
|
+
return ok({ error: "settings 服务不可用,重启 DSH 后再试" });
|
|
564
|
+
}
|
|
565
|
+
try {
|
|
566
|
+
await settingsServiceRef.update(CODE_UI_SETTINGS_NS, { uiMode: v });
|
|
567
|
+
selfSettings.uiMode = v;
|
|
568
|
+
return ok({ ok: true, uiMode: v });
|
|
569
|
+
} catch (err) {
|
|
570
|
+
return ok({ error: "写入设置失败:" + ((err && err.message) || String(err)) });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (endpoint === "set-layout") {
|
|
575
|
+
const v = args.layout;
|
|
576
|
+
if (v !== "dock" && v !== "float") return ok({ error: "layout 需为 dock / float" });
|
|
577
|
+
if (!settingsServiceRef || typeof settingsServiceRef.update !== "function") {
|
|
578
|
+
return ok({ error: "settings 服务不可用,重启 DSH 后再试" });
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
await settingsServiceRef.update(CODE_UI_SETTINGS_NS, { layout: v });
|
|
582
|
+
selfSettings.layout = v;
|
|
583
|
+
return ok({ ok: true, layout: v });
|
|
584
|
+
} catch (err) {
|
|
585
|
+
return ok({ error: "写入设置失败:" + ((err && err.message) || String(err)) });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (endpoint === "set-theme") {
|
|
590
|
+
const v = args.theme;
|
|
591
|
+
if (v !== "dsh" && v !== "system" && v !== "light" && v !== "dark") return ok({ error: "theme 需为 dsh / system / light / dark" });
|
|
592
|
+
if (!settingsServiceRef || typeof settingsServiceRef.update !== "function") {
|
|
593
|
+
return ok({ error: "settings 服务不可用,重启 DSH 后再试" });
|
|
594
|
+
}
|
|
595
|
+
try {
|
|
596
|
+
await settingsServiceRef.update(CODE_UI_SETTINGS_NS, { theme: v });
|
|
597
|
+
selfSettings.theme = v;
|
|
598
|
+
return ok({ ok: true, theme: v });
|
|
599
|
+
} catch (err) {
|
|
600
|
+
return ok({ error: "写入设置失败:" + ((err && err.message) || String(err)) });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (endpoint === "set-enabled") {
|
|
605
|
+
if (typeof args.enabled !== "boolean") return ok({ error: "enabled 参数需为布尔值" });
|
|
606
|
+
if (!settingsServiceRef || typeof settingsServiceRef.update !== "function") {
|
|
607
|
+
return ok({ error: "settings 服务不可用,重启 DSH 后再试" });
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
// 写入命名空间(schema 校验 + 持久化;watch 会经 onChange 再同步一次 selfSettings)
|
|
611
|
+
await settingsServiceRef.update(CODE_UI_SETTINGS_NS, { enabled: args.enabled });
|
|
612
|
+
selfSettings.enabled = args.enabled;
|
|
613
|
+
return ok({ ok: true, enabled: args.enabled });
|
|
614
|
+
} catch (err) {
|
|
615
|
+
return ok({ error: "写入设置失败:" + ((err && err.message) || String(err)) });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (endpoint === "check-update") {
|
|
620
|
+
const out = { current: SELF_VERSION, latest: null, updateAvailable: false, notPublished: false, registryUsed: null, error: null };
|
|
621
|
+
// 主源 = 设置里的 updateRegistry(默认官方源,镜像有同步滞后);主源不可达时用另一个兜底做「检查」
|
|
622
|
+
const primary = selfSettings.updateRegistry || REGISTRY_DEFAULT;
|
|
623
|
+
const fallback = primary === REGISTRY_MIRROR ? REGISTRY_DEFAULT : REGISTRY_MIRROR;
|
|
624
|
+
try {
|
|
625
|
+
const r = await fetchLatestVersion(SELF_NAME, primary);
|
|
626
|
+
out.registryUsed = primary;
|
|
627
|
+
if (r.notFound) out.notPublished = true;
|
|
628
|
+
else if (r.version) { out.latest = r.version; out.updateAvailable = isUpgrade(out.current, r.version); }
|
|
629
|
+
} catch (errPrimary) {
|
|
630
|
+
try {
|
|
631
|
+
const r2 = await fetchLatestVersion(SELF_NAME, fallback);
|
|
632
|
+
out.registryUsed = fallback;
|
|
633
|
+
if (r2.notFound) out.notPublished = true;
|
|
634
|
+
else if (r2.version) { out.latest = r2.version; out.updateAvailable = isUpgrade(out.current, r2.version); }
|
|
635
|
+
} catch (errSecond) {
|
|
636
|
+
out.error = "registry 连接失败:" + ((errSecond && errSecond.message) || String(errSecond));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return ok(out);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (endpoint === "perform-update") {
|
|
643
|
+
if (updateRun.active) return ok({ error: "已有更新任务在进行中" });
|
|
644
|
+
const profileRoot = findProfileRoot();
|
|
645
|
+
if (!profileRoot) return ok({ error: "未找到 profile 根目录(package.json 缺少 dsh.profile.bundles),无法自动更新" });
|
|
646
|
+
const source = args.source === "local" ? "local" : "registry";
|
|
647
|
+
let spec = null;
|
|
648
|
+
if (source === "registry") {
|
|
649
|
+
const v = String(args.version || "");
|
|
650
|
+
if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(v)) return ok({ error: "缺少有效版本号(version-invalid)" });
|
|
651
|
+
spec = SELF_NAME + "@" + v;
|
|
652
|
+
} else {
|
|
653
|
+
const p = String(args.path || "");
|
|
654
|
+
if (!isAbsolute(p) || !/\.t?gz$/i.test(p) || !existsSync(p)) {
|
|
655
|
+
return ok({ error: "请提供本地 .tgz 包的绝对路径(文件需存在)" });
|
|
656
|
+
}
|
|
657
|
+
spec = p;
|
|
658
|
+
}
|
|
659
|
+
runSelfUpdate(spec, profileRoot, source); // 异步执行;进度经 get-update-progress 轮询
|
|
660
|
+
return ok({ started: true });
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (endpoint === "get-update-progress") {
|
|
664
|
+
return ok({
|
|
665
|
+
active: updateRun.active, done: updateRun.done, ok: updateRun.ok,
|
|
666
|
+
phase: updateRun.phase, source: updateRun.source,
|
|
667
|
+
beforeVersion: updateRun.beforeVersion, afterVersion: updateRun.afterVersion,
|
|
668
|
+
error: updateRun.error, hint: updateRun.hint,
|
|
669
|
+
lines: updateRun.lines.slice(-40),
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return fail("未知端点: " + endpoint);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
return fail(String((err && err.message) || err));
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function ok(value) {
|
|
681
|
+
return { ok: true, value };
|
|
682
|
+
}
|
|
683
|
+
function fail(message) {
|
|
684
|
+
return { ok: false, error: { code: "internal", message, details: {} } };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// —— 模型工具:读取用户标记的代码片段 ——
|
|
688
|
+
function registerSnippetsTool(tools) {
|
|
689
|
+
if (!tools || typeof tools.register !== "function") return;
|
|
690
|
+
tools.register(
|
|
691
|
+
defineTool({
|
|
692
|
+
name: "get_referenced_snippets",
|
|
693
|
+
description:
|
|
694
|
+
"读取用户在代码编辑器中通过「引用/翻译」标记的代码片段(含文件路径、行号、语言与内容),供 AI 直接使用。当用户在对话框提到「我引用的代码」「翻译我引用的内容」时调用。",
|
|
695
|
+
parameters: {
|
|
696
|
+
kind: { type: "string", description: "筛选类型:cite(代码引用) / translate(翻译请求) / all(全部,默认)。" },
|
|
697
|
+
},
|
|
698
|
+
output: {
|
|
699
|
+
schema: { type: "json" },
|
|
700
|
+
render: (_args, value) => {
|
|
701
|
+
const v = value || {};
|
|
702
|
+
const refs = Array.isArray(v.references) ? v.references : [];
|
|
703
|
+
if (refs.length === 0) return [{ type: "text", text: "当前没有已标记的代码片段。" }];
|
|
704
|
+
const parts = refs.map((r) => {
|
|
705
|
+
const label = r.kind === "translate" ? "翻译请求" : "代码引用";
|
|
706
|
+
const loc = (r.path || "") + (r.lineStart ? ":" + r.lineStart + "-" + r.lineEnd : "");
|
|
707
|
+
const lang = r.language ? " [" + r.language + "]" : "";
|
|
708
|
+
return "【" + label + "】" + loc + lang + "\n" + (r.text || "");
|
|
709
|
+
});
|
|
710
|
+
return [{ type: "text", text: "共 " + refs.length + " 条标记:\n\n" + parts.join("\n\n") }];
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
async execute(args) {
|
|
714
|
+
const kind = args && args.kind;
|
|
715
|
+
let list = references;
|
|
716
|
+
if (kind && kind !== "all") list = references.filter((r) => r.kind === kind);
|
|
717
|
+
return { count: list.length, references: list };
|
|
718
|
+
},
|
|
719
|
+
}),
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export const name = "dsh-code-ui";
|
|
724
|
+
export const inject = ["tools"];
|
|
725
|
+
|
|
726
|
+
export function apply(ctx) {
|
|
727
|
+
// 注册 settings 命名空间(插件配置页按命名空间分发卡片;内部自带 ctx.inject(['settings']),
|
|
728
|
+
// 无 settings 服务的宿主上静默不运行,不影响其它功能)
|
|
729
|
+
installSelfSettings(ctx);
|
|
730
|
+
|
|
731
|
+
// 注册模型工具:通过 ctx.inject 拿 tools 服务(避免 Cordis guard 直接访问 ctx.tools)
|
|
732
|
+
ctx.inject(["tools"], (c) => {
|
|
733
|
+
registerSnippetsTool(c.tools);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// 等待 Connection 服务就绪后注册 RPC 通道
|
|
737
|
+
ctx.inject(["connection"], (c) => {
|
|
738
|
+
const connection = c.connection;
|
|
739
|
+
if (connection === undefined) return;
|
|
740
|
+
const dispose = connection.rpc.handle(CHANNEL, rpcHandler(ctx), { authority: "loopback" });
|
|
741
|
+
c.effect(() => dispose, "dsh-code-ui: rpc channel");
|
|
742
|
+
c.effect(() => () => unwatchAllFiles(), "dsh-code-ui: file watchers");
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
export default apply;
|