dsh-code-ui 1.3.1 → 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 +15 -7
- package/lib/client.js +329 -29
- package/lib/index.js +184 -42
- 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),
|
|
@@ -102,39 +168,44 @@ const CodeUiSettingsSchema = z.object({
|
|
|
102
168
|
layout: z.string().default("dock"),
|
|
103
169
|
// 界面模式:native(隐藏编辑器,默认——新装不自动占屏,由 ⌨ 按钮唤出) / codeui(显示编辑器)
|
|
104
170
|
uiMode: z.string().default("native"),
|
|
105
|
-
// 切换按钮样式:header(会话头 ⌨
|
|
106
|
-
toggle: z.string().default("
|
|
171
|
+
// 切换按钮样式:bar(悬浮模式条,默认) / header(会话头 ⌨ 按钮)
|
|
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: "
|
|
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) {
|
|
@@ -349,13 +420,12 @@ function unwatchAllFiles() {
|
|
|
349
420
|
|
|
350
421
|
function rpcHandler(ctx) {
|
|
351
422
|
const fs = ctx.get("fs");
|
|
352
|
-
let currentRootRef = detectWorkspaceRoot(ctx);
|
|
423
|
+
let currentRootRef = detectWorkspaceRoot(ctx); // 最近一次解析的根:list-dir 的兜底
|
|
424
|
+
// 每会话根目录记忆:sessionId -> 手动设置的根(「以此文件夹为根」)。
|
|
425
|
+
// 按会话隔离——手动根只影响设置它的那个会话,不再全局污染其它会话的自动解析;
|
|
426
|
+
// 重启 DSH 即清空,各会话回到各自 cwd 解析。自动解析结果不缓存(会话 cwd 可变,保持实时)。
|
|
427
|
+
const sessionRoots = new Map();
|
|
353
428
|
|
|
354
|
-
// 每次 get-root 动态回读会话工作区根,避免一次缓存导致拿不到当前项目目录
|
|
355
|
-
function baseRoot() {
|
|
356
|
-
if (currentRootRef) return currentRootRef;
|
|
357
|
-
return detectWorkspaceRoot(ctx);
|
|
358
|
-
}
|
|
359
429
|
function detectWorkspaceRoot(_ctx) {
|
|
360
430
|
// 1) 当前会话的真实工作区根(项目根):优先取正在发起调用的 agent 的 session cwd。
|
|
361
431
|
// sandboxPolicy.workspaceRoot 只是「部署回退根」,常是 DSH 进程 cwd 而非项目目录。
|
|
@@ -388,14 +458,58 @@ function rpcHandler(ctx) {
|
|
|
388
458
|
const args = payload && typeof payload === "object" ? payload : {};
|
|
389
459
|
|
|
390
460
|
if (endpoint === "get-root") {
|
|
391
|
-
|
|
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) {
|
|
469
|
+
const agentsSvc = ctx.get("agents");
|
|
470
|
+
if (agentsSvc) {
|
|
471
|
+
try {
|
|
472
|
+
const a = agentsSvc.get(sid);
|
|
473
|
+
if (a && a.session && a.session.header && a.session.header.cwd) {
|
|
474
|
+
root = a.session.header.cwd;
|
|
475
|
+
}
|
|
476
|
+
} catch (_) {}
|
|
477
|
+
}
|
|
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
|
+
}
|
|
500
|
+
if (!root) root = detectWorkspaceRoot(ctx);
|
|
501
|
+
if (root) currentRootRef = root;
|
|
392
502
|
return ok({ root });
|
|
393
503
|
}
|
|
394
504
|
|
|
395
505
|
if (endpoint === "set-root") {
|
|
396
506
|
if (!args.path) return ok({ error: "no-path" });
|
|
397
|
-
|
|
398
|
-
|
|
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 });
|
|
399
513
|
}
|
|
400
514
|
|
|
401
515
|
if (endpoint === "list-dir") {
|
|
@@ -452,9 +566,17 @@ function rpcHandler(ctx) {
|
|
|
452
566
|
if (endpoint === "write-file") {
|
|
453
567
|
if (!args.path) return ok({ error: "no-path" });
|
|
454
568
|
if (typeof args.content !== "string") return ok({ error: "no-content" });
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
+
}
|
|
458
580
|
}
|
|
459
581
|
|
|
460
582
|
if (endpoint === "watch-file") {
|
|
@@ -606,6 +728,7 @@ function rpcHandler(ctx) {
|
|
|
606
728
|
layout: selfSettings.layout,
|
|
607
729
|
uiMode: selfSettings.uiMode,
|
|
608
730
|
toggle: selfSettings.toggle,
|
|
731
|
+
synTheme: selfSettings.synTheme,
|
|
609
732
|
});
|
|
610
733
|
}
|
|
611
734
|
|
|
@@ -669,6 +792,21 @@ function rpcHandler(ctx) {
|
|
|
669
792
|
}
|
|
670
793
|
}
|
|
671
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
|
+
|
|
672
810
|
if (endpoint === "set-enabled") {
|
|
673
811
|
if (typeof args.enabled !== "boolean") return ok({ error: "enabled 参数需为布尔值" });
|
|
674
812
|
if (!settingsServiceRef || typeof settingsServiceRef.update !== "function") {
|
|
@@ -800,6 +938,10 @@ export function apply(ctx) {
|
|
|
800
938
|
// 无 settings 服务的宿主上静默不运行,不影响其它功能)
|
|
801
939
|
installSelfSettings(ctx);
|
|
802
940
|
|
|
941
|
+
// vendor 静态资产路由(highlight.js 全量包 + 主题 CSS):webServer 服务存在时注册 exact 路由,
|
|
942
|
+
// 客户端编辑器懒加载;无 webServer 的宿主上静默跳过(编辑器回退自带 tokenizer)
|
|
943
|
+
registerVendorRoutes(ctx);
|
|
944
|
+
|
|
803
945
|
// 注册模型工具:通过 ctx.inject 拿 tools 服务(避免 Cordis guard 直接访问 ctx.tools)
|
|
804
946
|
ctx.inject(["tools"], (c) => {
|
|
805
947
|
registerSnippetsTool(c.tools);
|