dsh-code-ui 1.3.2 → 1.4.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 +11 -4
- package/lib/client.js +307 -25
- package/lib/index.js +170 -40
- package/lib/vendor/highlight.full.min.js +5306 -0
- package/lib/vendor/hljs-themes.css +711 -0
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// UI's file / reference / note operations, and a model tool get_referenced_snippets.
|
|
5
5
|
|
|
6
6
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
7
|
-
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
8
7
|
import z from "@deepseek-ai/schemastery";
|
|
9
8
|
import { spawn } from "node:child_process";
|
|
10
9
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
@@ -36,6 +35,65 @@ const SELF_MANIFEST = readJsonSafe(join(HERE, "..", "package.json")) || { name:
|
|
|
36
35
|
const SELF_NAME = SELF_MANIFEST.name;
|
|
37
36
|
const SELF_VERSION = SELF_MANIFEST.version;
|
|
38
37
|
|
|
38
|
+
// —— highlight.js 静态资产:宿主 webServer exact 路由下发 ——
|
|
39
|
+
// /plugins 组合脚本只承载 client.js 本体,vendor 产物(全量包 + 作用域化主题 CSS)无从经其
|
|
40
|
+
// 下发;exact 路由无路径参数,天然无目录穿越面。产物由 tools/vendor-hljs.mjs 生成
|
|
41
|
+
// (升级:node tools/vendor-hljs.mjs <版本号>;只补主题:--themes-only)。
|
|
42
|
+
const VENDOR_ASSETS = {
|
|
43
|
+
"/dsh-code-ui/vendor/highlight.full.min.js": {
|
|
44
|
+
file: join(HERE, "vendor", "highlight.full.min.js"),
|
|
45
|
+
type: "text/javascript; charset=utf-8",
|
|
46
|
+
},
|
|
47
|
+
"/dsh-code-ui/vendor/hljs-themes.css": {
|
|
48
|
+
file: join(HERE, "vendor", "hljs-themes.css"),
|
|
49
|
+
type: "text/css; charset=utf-8",
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
// 语法配色可取值(与 lib/client.js 的 SYN_THEMES 表、tools/vendor-hljs.mjs 的 THEMES 一一对应)
|
|
53
|
+
const SYN_THEME_IDS = [
|
|
54
|
+
"auto", "github", "github-dark", "atom-one-dark", "atom-one-light",
|
|
55
|
+
"monokai", "dracula", "nord", "vs", "vs2015", "tokyo-night-dark",
|
|
56
|
+
];
|
|
57
|
+
function registerVendorRoutes(ctx) {
|
|
58
|
+
// webServer 为可选宿主服务(web profile 提供):无该服务的宿主上回调不运行,
|
|
59
|
+
// 编辑器自动回退自研 tokenizer,不影响其它功能
|
|
60
|
+
ctx.inject(["webServer"], (c) => {
|
|
61
|
+
const disposers = [];
|
|
62
|
+
for (const [path, asset] of Object.entries(VENDOR_ASSETS)) {
|
|
63
|
+
disposers.push(
|
|
64
|
+
c.webServer.register({
|
|
65
|
+
kind: "exact",
|
|
66
|
+
path,
|
|
67
|
+
handler: (req, res) => {
|
|
68
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
69
|
+
res.writeHead(405, { allow: "GET, HEAD", "content-length": "0" });
|
|
70
|
+
res.end();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let body = null;
|
|
74
|
+
try {
|
|
75
|
+
if (existsSync(asset.file)) body = readFileSync(asset.file);
|
|
76
|
+
} catch (_) {}
|
|
77
|
+
if (body === null) {
|
|
78
|
+
res.writeHead(404, { "cache-control": "no-store" });
|
|
79
|
+
res.end();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
res.writeHead(200, {
|
|
83
|
+
"content-type": asset.type,
|
|
84
|
+
"content-length": String(body.length),
|
|
85
|
+
// 客户端以 ?v=<插件版本> 作 cache-bust:同版本字节不变,可放心长缓存
|
|
86
|
+
"cache-control": "public, max-age=31536000, immutable",
|
|
87
|
+
});
|
|
88
|
+
res.end(req.method === "HEAD" ? undefined : body);
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
c.effect(() => () => { for (const d of disposers) { try { d(); } catch (_) {} } }, "dsh-code-ui: vendor assets");
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
39
97
|
// profile 根 = 向上找 package.json 含 dsh.profile.bundles 的目录(不数层级、不硬编码路径)
|
|
40
98
|
function findProfileRoot() {
|
|
41
99
|
let dir = HERE;
|
|
@@ -90,7 +148,15 @@ const REGISTRY_MIRROR = "https://registry.npmmirror.com";
|
|
|
90
148
|
// -- settings 命名空间:「插件配置」页按 host 服务的命名空间分发卡片 --
|
|
91
149
|
// 卡片的 key 必须是一个 host 实际注册的 settings 命名空间,否则卡片永不渲染
|
|
92
150
|
//(ui-settings-plugins 的 tab-store 只分发 key ∈ 已服务命名空间的卡片,照 dshmarket 的 settings.ts 做法)。
|
|
93
|
-
|
|
151
|
+
// 不 import @deepseek-ai/dsh-settings:该包 0.1.2 起删除了 installSettingsSection /
|
|
152
|
+
// settingsNamespace 具名导出(ESM 缺失具名导出 = 模块求值期 SyntaxError,宿主直接退出 1),
|
|
153
|
+
// 而服务本身 sctx.settings.register(ns, schema, { base }) 从 0.1.0-rc.7 到 0.1.2 完全一致。
|
|
154
|
+
// 照 dshmarket settings.js 的做法内联等价接线;下为旧 settingsNamespace() 的校验规则。
|
|
155
|
+
const SETTINGS_NS_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
156
|
+
const CODE_UI_SETTINGS_NS = "dsh-code-ui";
|
|
157
|
+
if (!SETTINGS_NS_PATTERN.test(CODE_UI_SETTINGS_NS)) {
|
|
158
|
+
throw new TypeError(`settings namespace "${CODE_UI_SETTINGS_NS}" must match ${SETTINGS_NS_PATTERN}`);
|
|
159
|
+
}
|
|
94
160
|
const CodeUiSettingsSchema = z.object({
|
|
95
161
|
// 检查更新用的 registry(只影响在线「检查」;安装仍走 profile 自己的 .npmrc)
|
|
96
162
|
updateRegistry: z.string().default(REGISTRY_DEFAULT),
|
|
@@ -104,37 +170,42 @@ const CodeUiSettingsSchema = z.object({
|
|
|
104
170
|
uiMode: z.string().default("native"),
|
|
105
171
|
// 切换按钮样式:bar(悬浮模式条,默认) / header(会话头 ⌨ 按钮)
|
|
106
172
|
toggle: z.string().default("bar"),
|
|
173
|
+
// 语法配色:auto(默认,随编辑器明暗 GitHub/GitHub Dark) 或 SYN_THEME_IDS 之一
|
|
174
|
+
synTheme: z.string().default("auto"),
|
|
107
175
|
});
|
|
108
176
|
// 解析后的当前值:settings 服务未挂载/未写入时保持默认
|
|
109
|
-
const selfSettings = { updateRegistry: REGISTRY_DEFAULT, enabled: true, theme: "dsh", layout: "dock", uiMode: "native", toggle: "bar" };
|
|
177
|
+
const selfSettings = { updateRegistry: REGISTRY_DEFAULT, enabled: true, theme: "dsh", layout: "dock", uiMode: "native", toggle: "bar", synTheme: "auto" };
|
|
110
178
|
// settings 服务引用(「启用/禁用」开关写入用;由 installSelfSettings 捕获)
|
|
111
179
|
let settingsServiceRef = null;
|
|
112
180
|
function installSelfSettings(ctx) {
|
|
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
|
-
|
|
181
|
+
// 组合基线(base):settings 服务缺席/卸载时的回落取值
|
|
182
|
+
const entry = { updateRegistry: selfSettings.updateRegistry, enabled: selfSettings.enabled, theme: selfSettings.theme, layout: selfSettings.layout, uiMode: selfSettings.uiMode, toggle: selfSettings.toggle, synTheme: selfSettings.synTheme };
|
|
183
|
+
let source = () => entry;
|
|
184
|
+
const onChange = () => {
|
|
185
|
+
try {
|
|
186
|
+
const v = source() || {};
|
|
187
|
+
if (typeof v.updateRegistry === "string" && /^https?:\/\//.test(v.updateRegistry)) {
|
|
188
|
+
selfSettings.updateRegistry = v.updateRegistry.replace(/\/+$/, "");
|
|
189
|
+
}
|
|
190
|
+
if (typeof v.enabled === "boolean") selfSettings.enabled = v.enabled;
|
|
191
|
+
if (v.theme === "dsh" || v.theme === "system" || v.theme === "light" || v.theme === "dark") selfSettings.theme = v.theme;
|
|
192
|
+
if (v.layout === "dock" || v.layout === "float") selfSettings.layout = v.layout;
|
|
193
|
+
if (v.uiMode === "codeui" || v.uiMode === "native") selfSettings.uiMode = v.uiMode;
|
|
194
|
+
if (v.toggle === "header" || v.toggle === "bar") selfSettings.toggle = v.toggle;
|
|
195
|
+
if (typeof v.synTheme === "string" && SYN_THEME_IDS.indexOf(v.synTheme) >= 0) selfSettings.synTheme = v.synTheme;
|
|
196
|
+
} catch (_) {}
|
|
197
|
+
};
|
|
198
|
+
// inject 是优雅降级边界:无 settings 服务的宿主上回调不运行,基线值照常生效
|
|
199
|
+
//(等价于旧 installSettingsSection:inject + register + watch + 卸载回落,照 dshmarket settings.js)
|
|
200
|
+
ctx.inject(["settings"], (sctx) => {
|
|
201
|
+
settingsServiceRef = sctx.settings; // 写路径(set-* RPC)用服务级 update(ns, patch),新旧版签名一致
|
|
202
|
+
const scope = sctx.settings.register(CODE_UI_SETTINGS_NS, CodeUiSettingsSchema, { base: entry });
|
|
203
|
+
source = () => scope.get();
|
|
204
|
+
// 卸载恢复基线取值,避免路由读到无人可见/可改的值
|
|
205
|
+
sctx.effect(() => () => { source = () => entry; onChange(); });
|
|
206
|
+
onChange();
|
|
207
|
+
scope.watch(onChange);
|
|
208
|
+
});
|
|
138
209
|
}
|
|
139
210
|
|
|
140
211
|
async function fetchLatestVersion(name, registry) {
|
|
@@ -350,7 +421,10 @@ function unwatchAllFiles() {
|
|
|
350
421
|
function rpcHandler(ctx) {
|
|
351
422
|
const fs = ctx.get("fs");
|
|
352
423
|
let currentRootRef = detectWorkspaceRoot(ctx); // 最近一次解析的根:list-dir 的兜底
|
|
353
|
-
|
|
424
|
+
// 每会话根目录记忆:sessionId -> 手动设置的根(「以此文件夹为根」)。
|
|
425
|
+
// 按会话隔离——手动根只影响设置它的那个会话,不再全局污染其它会话的自动解析;
|
|
426
|
+
// 重启 DSH 即清空,各会话回到各自 cwd 解析。自动解析结果不缓存(会话 cwd 可变,保持实时)。
|
|
427
|
+
const sessionRoots = new Map();
|
|
354
428
|
|
|
355
429
|
function detectWorkspaceRoot(_ctx) {
|
|
356
430
|
// 1) 当前会话的真实工作区根(项目根):优先取正在发起调用的 agent 的 session cwd。
|
|
@@ -384,20 +458,45 @@ function rpcHandler(ctx) {
|
|
|
384
458
|
const args = payload && typeof payload === "object" ? payload : {};
|
|
385
459
|
|
|
386
460
|
if (endpoint === "get-root") {
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
461
|
+
// 优先级(按会话隔离):该会话手动设置的根 > 该会话的 agent cwd(agent 已启动,
|
|
462
|
+
// agent/session id 共享,agents.get 可查)> 该会话的 SessionHeader.cwd(新会话未发
|
|
463
|
+
// 消息时 agents 里还没有记录,但会话创建时 cwd 已从所选工作区写入不可变 header,
|
|
464
|
+
// 经 sessions 服务可查)> initiator 会话 cwd > 部署回退根 > 进程 cwd。
|
|
465
|
+
// 手动根仅对本会话生效,其它会话不受污染。
|
|
466
|
+
const sid = args.sessionId ? String(args.sessionId) : "";
|
|
467
|
+
let root = sid ? sessionRoots.get(sid) || null : null;
|
|
468
|
+
if (!root && sid) {
|
|
391
469
|
const agentsSvc = ctx.get("agents");
|
|
392
470
|
if (agentsSvc) {
|
|
393
471
|
try {
|
|
394
|
-
const a = agentsSvc.get(
|
|
472
|
+
const a = agentsSvc.get(sid);
|
|
395
473
|
if (a && a.session && a.session.header && a.session.header.cwd) {
|
|
396
474
|
root = a.session.header.cwd;
|
|
397
475
|
}
|
|
398
476
|
} catch (_) {}
|
|
399
477
|
}
|
|
400
478
|
}
|
|
479
|
+
// 新会话(尚未发消息、agent 未启动):从会话存储取 SessionHeader.cwd
|
|
480
|
+
if (!root && sid) {
|
|
481
|
+
const sessionsSvc = ctx.get("sessions");
|
|
482
|
+
if (sessionsSvc && typeof sessionsSvc.get === "function") {
|
|
483
|
+
try {
|
|
484
|
+
const s = sessionsSvc.get(sid);
|
|
485
|
+
if (s && s.header && s.header.cwd) root = s.header.cwd;
|
|
486
|
+
} catch (_) {}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// 会话已持久化但未在内存存储(如重启 DSH 后重开旧对话、agent 尚未恢复):
|
|
490
|
+
// 经 sessionController.inspect 读持久化 header(官方语义:不激活 agent)
|
|
491
|
+
if (!root && sid) {
|
|
492
|
+
const sc = ctx.get("sessionController");
|
|
493
|
+
if (sc && typeof sc.inspect === "function") {
|
|
494
|
+
try {
|
|
495
|
+
const r = await sc.inspect(sid);
|
|
496
|
+
if (r && r.meta && r.meta.cwd) root = r.meta.cwd;
|
|
497
|
+
} catch (_) {}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
401
500
|
if (!root) root = detectWorkspaceRoot(ctx);
|
|
402
501
|
if (root) currentRootRef = root;
|
|
403
502
|
return ok({ root });
|
|
@@ -405,9 +504,12 @@ function rpcHandler(ctx) {
|
|
|
405
504
|
|
|
406
505
|
if (endpoint === "set-root") {
|
|
407
506
|
if (!args.path) return ok({ error: "no-path" });
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
507
|
+
// 手动根按会话记录:只影响发起设置的会话;无会话上下文时退化为全局兜底
|
|
508
|
+
const path = String(args.path);
|
|
509
|
+
const sid = args.sessionId ? String(args.sessionId) : "";
|
|
510
|
+
if (sid) sessionRoots.set(sid, path);
|
|
511
|
+
currentRootRef = path;
|
|
512
|
+
return ok({ root: path });
|
|
411
513
|
}
|
|
412
514
|
|
|
413
515
|
if (endpoint === "list-dir") {
|
|
@@ -464,9 +566,17 @@ function rpcHandler(ctx) {
|
|
|
464
566
|
if (endpoint === "write-file") {
|
|
465
567
|
if (!args.path) return ok({ error: "no-path" });
|
|
466
568
|
if (typeof args.content !== "string") return ok({ error: "no-content" });
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
569
|
+
try {
|
|
570
|
+
writeSuppress.set(args.path, Date.now() + 1500); // 自己的保存不进事件队列(回声抑制)
|
|
571
|
+
// 原生写入:code UI 的 Ctrl+S 是用户显式编辑动作,不受会话沙箱限制
|
|
572
|
+
// (与文件树右键新建/重命名/删除一致;沙箱 fs 只允许写会话工作区,
|
|
573
|
+
// 工作区之外的文件会报 file access denied under workspace-write mode)
|
|
574
|
+
writeFileSync(args.path, args.content, "utf8");
|
|
575
|
+
return ok({ ok: true });
|
|
576
|
+
} catch (err) {
|
|
577
|
+
writeSuppress.delete(args.path);
|
|
578
|
+
return ok({ error: "保存失败:" + ((err && err.message) || String(err)) });
|
|
579
|
+
}
|
|
470
580
|
}
|
|
471
581
|
|
|
472
582
|
if (endpoint === "watch-file") {
|
|
@@ -618,6 +728,7 @@ function rpcHandler(ctx) {
|
|
|
618
728
|
layout: selfSettings.layout,
|
|
619
729
|
uiMode: selfSettings.uiMode,
|
|
620
730
|
toggle: selfSettings.toggle,
|
|
731
|
+
synTheme: selfSettings.synTheme,
|
|
621
732
|
});
|
|
622
733
|
}
|
|
623
734
|
|
|
@@ -681,6 +792,21 @@ function rpcHandler(ctx) {
|
|
|
681
792
|
}
|
|
682
793
|
}
|
|
683
794
|
|
|
795
|
+
if (endpoint === "set-syn-theme") {
|
|
796
|
+
const v = args.synTheme;
|
|
797
|
+
if (SYN_THEME_IDS.indexOf(v) < 0) return ok({ error: "synTheme 需为 auto 或受支持的主题 id" });
|
|
798
|
+
if (!settingsServiceRef || typeof settingsServiceRef.update !== "function") {
|
|
799
|
+
return ok({ error: "settings 服务不可用,重启 DSH 后再试" });
|
|
800
|
+
}
|
|
801
|
+
try {
|
|
802
|
+
await settingsServiceRef.update(CODE_UI_SETTINGS_NS, { synTheme: v });
|
|
803
|
+
selfSettings.synTheme = v;
|
|
804
|
+
return ok({ ok: true, synTheme: v });
|
|
805
|
+
} catch (err) {
|
|
806
|
+
return ok({ error: "写入设置失败:" + ((err && err.message) || String(err)) });
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
684
810
|
if (endpoint === "set-enabled") {
|
|
685
811
|
if (typeof args.enabled !== "boolean") return ok({ error: "enabled 参数需为布尔值" });
|
|
686
812
|
if (!settingsServiceRef || typeof settingsServiceRef.update !== "function") {
|
|
@@ -812,6 +938,10 @@ export function apply(ctx) {
|
|
|
812
938
|
// 无 settings 服务的宿主上静默不运行,不影响其它功能)
|
|
813
939
|
installSelfSettings(ctx);
|
|
814
940
|
|
|
941
|
+
// vendor 静态资产路由(highlight.js 全量包 + 主题 CSS):webServer 服务存在时注册 exact 路由,
|
|
942
|
+
// 客户端编辑器懒加载;无 webServer 的宿主上静默跳过(编辑器回退自带 tokenizer)
|
|
943
|
+
registerVendorRoutes(ctx);
|
|
944
|
+
|
|
815
945
|
// 注册模型工具:通过 ctx.inject 拿 tools 服务(避免 Cordis guard 直接访问 ctx.tools)
|
|
816
946
|
ctx.inject(["tools"], (c) => {
|
|
817
947
|
registerSnippetsTool(c.tools);
|