@max-null/dsh-quick-toolbar 0.1.0 → 0.3.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/README.md CHANGED
@@ -1,8 +1,12 @@
1
- # @max-null/dsh-quick-toolbar — 插件按钮聚合器
1
+ # @max-null/dsh-quick-toolbar
2
2
 
3
- 把三方插件散乱的按钮(插件中心/侧栏/底栏/会话管理…)聚合到**统一入口**:
4
- SSiD 壳 → 标题栏按钮组;DSH web → **iOS 小白点式悬浮球**(自由定位拖拽、球↔面板 morph 展开、球永远锁定面板屏幕外侧角)。
5
- 适配器驱动:内置适配集开箱即识;**用户环境下 LLM 按模板生成适配**(驻场工程师模式)——不要求三方插件配合。
3
+ 本插件属于 **`@max-null/*` 插件系列**——这一系列共同构成 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 桌面体验。SSID 是整合它们的盒:`dsh-capture` · `dsh-chat-rail` · `dsh-chinese-thinking` · `dsh-draft-polish` · `dsh-guardian` · `dsh-habit` · `dsh-memory` · `dsh-node-appearance` · `dsh-plugin-center` · `dsh-quick-toolbar` · `dsh-skill-mcp-center` · `dsh-ssid-panels` · `dsh-ssid-zh-ui`。
4
+
5
+ This plugin belongs to the **`@max-null/*` family** — a set of plugins that together form the **[SSID (思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** desktop experience.
6
+
7
+ 面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**插件按钮聚合器**:把三方插件散乱的按钮(插件中心/侧栏/底栏/会话管理…)聚合到统一入口——SSiD 壳 → 标题栏按钮组;DSH web → **iOS 小白点式悬浮球**(自由定位拖拽、球↔面板 morph 展开、球永远锁定面板屏幕外侧角)。适配器驱动:内置适配集开箱即识;**用户环境下 LLM 按模板生成适配**(驻场工程师模式)——不要求三方插件配合。
8
+
9
+ A **plugin-button aggregator** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): gathers scattered third-party plugin buttons (plugin center / sidebar / bottom bar / session manager …) into one entry — an SSiD title-bar button group in the SSID shell, and an iOS assistive-touch-style floating ball on plain DSH web (free positioning & dragging, ball↔panel morph expand, ball always locked to the panel's screen-outer corner). Adapter-driven: bundled adapters work out of the box; in any user environment, an LLM generates new adapters from a template (the "on-site engineer" mode) — no cooperation required from third-party plugins.
6
10
 
7
11
  ## 截图
8
12
 
@@ -21,18 +25,21 @@ dsh plugin --profile web add @max-null/dsh-quick-toolbar
21
25
 
22
26
  ## 使用
23
27
 
24
- - **聚合按钮**:标题栏/悬浮球点开面板 → 一键触发各插件功能(再点关闭——会话管理探测弹窗关闭按钮)。
25
- - **用户适配器**:复制 `adapters.prompt.md` + 环境描述 让 LLM 生成 `{ "adapters": [...] }` → 写入 `~/.dsh/quick-toolbar-adapters.json` → 重启/热载生效(schema 校验,非法条目丢弃并报明细)。
28
+ - **聚合按钮**:标题栏/悬浮球点开面板 → 一键触发各插件功能(再点关闭——会话管理探测弹窗关闭按钮;设置面板语义锚点打开/再点关闭)。
29
+ - **内置入口**:悬浮球含「设置」(官方设置面板:引擎按语义锚点定位 footer 触发器,双 locale;再点关闭——DSH trigger 原生只开不关)。
30
+ - **壳环境(SSiD)**:悬浮球默认隐藏(标题栏接管入口);标题栏「悬浮球」开关按钮(圆圈+圆点图标)可开启。
31
+ - **用户适配器**:复制 `adapters.prompt.md` + 环境描述 → 让 LLM 生成 `{ "adapters": [...] }` → 写入 `~/.dsh/quick-toolbar-adapters.json` → 刷新页面生效(host API 校验,非法条目丢弃并报明细)。
26
32
  - 定位失败/插件未装/被禁用 → 静默跳过(绝不误伤、绝不误点)。
33
+ - **状态持久化**:位置/钉住/折叠/壳开关走 host(`/quick-toolbar/api/state` → `~/.dsh/quick-toolbar-state.json`,SSiD 开发手册 §7.10 规则)——内核动态端口不再丢状态。
27
34
 
28
35
  ## 架构
29
36
 
30
37
  - `src/adapters.ts`:适配器 schema + 内置适配器集(黄金示例)
31
- - `src/behaviors.ts`:行为库(click / toggle-panel / dispatch-event / open-settings / command
38
+ - `src/behaviors.ts`:行为库(click / toggle-panel / dispatch-event / open-settings(语义锚点+再点关闭)/ command(composer 注入)——全部实现,v2 M1
32
39
  - `src/engine.ts`:执行器(防御执行)
33
40
  - `src/schema.ts`:用户配置 zod 校验(LLM 产物防线)
34
- - `src/index.ts`:host `GET /quick-toolbar/api/adapters`
35
- - 设计文档:`doc/设计/2026-08-30-quick-toolbar-独立化设计方案.md`
41
+ - `src/index.ts`(host 半):`GET /quick-toolbar/api/adapters`(用户适配器)+ `GET/POST /quick-toolbar/api/state`(状态 host 化,原子写)
42
+ - 设计文档:`doc/设计/2026-08-30-quick-toolbar-独立化设计方案.md`、`doc/设计/2026-08-30-quick-toolbar-v2设计.md`
36
43
 
37
44
  ## 开发
38
45
 
Binary file
package/lib/client.js CHANGED
@@ -51,9 +51,139 @@ const BUILTIN_ADAPTERS = [
51
51
  close: ".sm-modal .close"
52
52
  },
53
53
  hide: true
54
+ },
55
+ {
56
+ id: "dsh-settings",
57
+ button: "button[aria-label=\"设置\"], button[aria-label=\"Settings\"]",
58
+ icon: {
59
+ source: "custom",
60
+ value: "settings"
61
+ },
62
+ label: "设置",
63
+ act: { kind: "open-settings" },
64
+ hide: false
54
65
  }
55
66
  ];
56
67
  //#endregion
68
+ //#region src/behaviors.ts
69
+ /**
70
+ * 直接点击目标(定位失败/禁用 → 静默跳过;称「防御执行」)。
71
+ */
72
+ function actClick(target) {
73
+ if (target === null || target === void 0 || target.disabled === true) return false;
74
+ target.click();
75
+ return true;
76
+ }
77
+ /**
78
+ * 面板开/关:再点关闭(探测 close 选择器内的「关闭」按钮——原生无
79
+ * 再点关闭的插件(如 dsh-session-manager 0.4.x)走此行为。
80
+ */
81
+ function actTogglePanel(panel, closeTarget) {
82
+ if (panel.isOpen()) {
83
+ if (closeTarget !== null && closeTarget !== void 0 && closeTarget.disabled !== true) {
84
+ closeTarget.click();
85
+ return true;
86
+ }
87
+ return false;
88
+ }
89
+ panel.open();
90
+ return true;
91
+ }
92
+ /** 官方设置触发器锚点链(v2 调研点① 结论:DSH 面板 open 状态由 shell 私有,
93
+ * 公开语义入口 = sidebar.settings 槽的触发按钮——SettingsRoot.tsx onClick
94
+ * setOpen(true),footer 触发器类后缀 `_trigger`、无 aria。 */
95
+ const SETTINGS_ANCHORS = [
96
+ "[class$=\"_trigger\"]",
97
+ "button[aria-label=\"设置\"]",
98
+ "button[aria-label=\"Settings\"]",
99
+ "[role=\"button\"][aria-label=\"设置\"]",
100
+ "[role=\"button\"][aria-label=\"Settings\"]",
101
+ "button[title=\"设置\"]",
102
+ "button[title=\"Settings\"]"
103
+ ];
104
+ /** 官方设置关闭目标锚点链(面板开着时二次点击应关闭——trigger onClick 只
105
+ * setOpen(true) 原生不 toggle,2026-08-30 用户实测「再点关闭失败」)。 */
106
+ const SETTINGS_CLOSE_ANCHORS = [
107
+ "button[class$=\"_close\"]",
108
+ "button[aria-label=\"关闭\"]",
109
+ "button[title=\"关闭\"]"
110
+ ];
111
+ /** 设置面板打开判定(modal mask;SettingsRoot.tsx:mask div onClick=onClose)。 */
112
+ const SETTINGS_MASK_SELECTOR = "[class$=\"_mask\"]";
113
+ /** 打开/关闭官方设置面板:开着(mask 存在)→ 关闭(close 按钮 → mask 兜底);
114
+ * 关着 → 文本语义定位优先(footer trigger)→ 锚点链兜底。 */
115
+ function actOpenSettings(env) {
116
+ const mask = env.find(SETTINGS_MASK_SELECTOR);
117
+ if (mask !== null && mask !== void 0) {
118
+ for (let i = 0; i < SETTINGS_CLOSE_ANCHORS.length; i++) {
119
+ const closeBtn = env.find(SETTINGS_CLOSE_ANCHORS[i]);
120
+ if (closeBtn !== null && closeBtn !== void 0 && closeBtn.disabled !== true) {
121
+ closeBtn.click();
122
+ return true;
123
+ }
124
+ }
125
+ mask.click();
126
+ return true;
127
+ }
128
+ if (env.findByText !== void 0) {
129
+ const byText = env.findByText(["设置", "Settings"]);
130
+ if (byText !== null && byText !== void 0 && byText.disabled !== true) {
131
+ byText.click();
132
+ return true;
133
+ }
134
+ }
135
+ for (let i = 0; i < SETTINGS_ANCHORS.length; i++) {
136
+ const target = env.find(SETTINGS_ANCHORS[i]);
137
+ if (target !== null && target !== void 0 && target.disabled !== true) {
138
+ target.click();
139
+ return true;
140
+ }
141
+ }
142
+ return false;
143
+ }
144
+ /** 触发 dsh-commands 文本命令(空白名防御;执行语义由环境实现——v2 调研点②)。 */
145
+ function actCommand(env, name) {
146
+ if (name === void 0 || name === null || name.trim() === "") return false;
147
+ return env.execute(name.trim());
148
+ }
149
+ //#endregion
150
+ //#region src/engine.ts
151
+ /**
152
+ * @max-null/dsh-quick-toolbar — 适配器执行器(DOM 绑定层)
153
+ *
154
+ * 引擎运行时:按适配器定位目标元素 → 调行为库 → 防御执行。
155
+ * 行为库保持窄接口(可测);本层唯一负责 DOM 绑定(browser 环境,
156
+ * 不在 node:test 的范围里——DOM 交互留给 L2 环境验证)。
157
+ */
158
+ /** 选择器转义(不依赖 CSS 全局——node:test 无 DOM/CSS,行为库测试可用) */
159
+ function cssEscape(name) {
160
+ return name.replace(/[^a-zA-Z0-9_-]/g, (c) => `\\${c}`);
161
+ }
162
+ /** 按内置定义执行一条适配器(防御执行:定位失败/禁用 → false,绝不误伤) */
163
+ function runAdapter(adapter, env) {
164
+ switch (adapter.act.kind) {
165
+ case "click": return actClick(env.find(adapter.button));
166
+ case "dispatch-event": return env.dispatch(adapter.act.event, adapter.act.detail);
167
+ case "toggle-panel": {
168
+ const panel = env.findPanel(`#${cssEscape(adapter.id)}-panel`);
169
+ if (panel !== null) return actTogglePanel(panel, adapter.act.close !== void 0 ? env.find(adapter.act.close) : null);
170
+ return false;
171
+ }
172
+ case "open-settings":
173
+ if (adapter.act.path !== void 0) console.warn(`quick-toolbar: open-settings path '${adapter.act.path}' 暂不支持(v2 深链待入)`);
174
+ return actOpenSettings({
175
+ find: env.find,
176
+ ...env.findByText !== void 0 ? { findByText: env.findByText } : {}
177
+ });
178
+ case "command":
179
+ if (env.runCommand === void 0) {
180
+ console.warn("quick-toolbar: command 环境无 runCommand 通道(旧版 DSH/未注入)");
181
+ return false;
182
+ }
183
+ return actCommand({ execute: env.runCommand }, adapter.act.name);
184
+ }
185
+ }
186
+ //#endregion
57
187
  //#region src/client.ts
58
188
  /**
59
189
  * @max-null/dsh-quick-toolbar — browser half.
@@ -258,9 +388,40 @@ window.__ModuleLoader__.load({
258
388
  applyLocale();
259
389
  }
260
390
  var TOOLBAR_ID = "ssid-toolbar";
261
- var TOOLBAR_POS_KEY = "ssid-toolbar-pos";
262
- var TOOLBAR_COLLAPSED_KEY = "ssid-toolbar-collapsed";
263
- var TOOLBAR_PINNED_KEY = "ssid-toolbar-pinned";
391
+ var qtState = {
392
+ pos: null,
393
+ collapsed: true,
394
+ pinned: false,
395
+ shellVisible: false
396
+ };
397
+ var loadState = function(done) {
398
+ fetch("/quick-toolbar/api/state").then(function(r) {
399
+ return r.json();
400
+ }).then(function(data) {
401
+ var s = data !== null && typeof data === "object" && data.ok === true ? data.state : void 0;
402
+ if (s !== void 0 && s !== null) {
403
+ if (s.pos !== null && s.pos !== void 0 && typeof s.pos.x === "number" && typeof s.pos.y === "number") qtState.pos = {
404
+ x: s.pos.x,
405
+ y: s.pos.y
406
+ };
407
+ if (typeof s.collapsed === "boolean") qtState.collapsed = s.collapsed;
408
+ if (typeof s.pinned === "boolean") qtState.pinned = s.pinned;
409
+ if (typeof s.shellVisible === "boolean") qtState.shellVisible = s.shellVisible;
410
+ }
411
+ done();
412
+ }).catch(function() {
413
+ done();
414
+ });
415
+ };
416
+ var saveState = function() {
417
+ try {
418
+ fetch("/quick-toolbar/api/state", {
419
+ method: "POST",
420
+ headers: { "Content-Type": "application/json" },
421
+ body: JSON.stringify(qtState)
422
+ }).catch(function() {});
423
+ } catch (_e) {}
424
+ };
264
425
  var TOOLBAR_CSS = [
265
426
  "#ssid-toolbar{position:fixed;z-index:9999;font-family:system-ui,\"Segoe UI\",sans-serif;user-select:none;-webkit-user-select:none;box-sizing:border-box;width:36px;height:36px;border-radius:18px;background:var(--dsw-alias-bg-layer-3,#10151f);border:1px solid var(--dsw-alias-border-l2,#1e2836);box-shadow:0 4px 16px rgba(0,0,0,.3);overflow:hidden;transition:width .28s cubic-bezier(.25,.8,.25,1),height .28s cubic-bezier(.25,.8,.25,1),left .28s cubic-bezier(.25,.8,.25,1),top .28s cubic-bezier(.25,.8,.25,1),border-radius .28s cubic-bezier(.25,.8,.25,1)}",
266
427
  "#ssid-toolbar *{box-sizing:border-box}",
@@ -288,10 +449,61 @@ window.__ModuleLoader__.load({
288
449
  sessions: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><rect x=\"2\" y=\"3\" width=\"12\" height=\"10\" rx=\"1.5\"/><path d=\"M5 6.5h6M5 9.5h4\" stroke-linecap=\"round\"/></svg>",
289
450
  collapse: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M3 5.5h10M6.5 8.5h3M8 11.5h1\" stroke-linecap=\"round\"/></svg>",
290
451
  menu: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M3 5h10M3 8h10M3 11h10\" stroke-linecap=\"round\"/></svg>",
291
- pin: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9.8 2.2l4 4-2.6 1.4-1.8 1.8.4 2.6-1.4 1.4-2.6-3L3.9 13l-1-1 3-3.9-3-2.6 1.4-1.4 2.6.4 1.8-1.8z\"/></svg>"
452
+ pin: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9.8 2.2l4 4-2.6 1.4-1.8 1.8.4 2.6-1.4 1.4-2.6-3L3.9 13l-1-1 3-3.9-3-2.6 1.4-1.4 2.6.4 1.8-1.8z\"/></svg>",
453
+ settings: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"8\" cy=\"8\" r=\"2.2\"/><path d=\"M8 1.8v2M8 12.2v2M1.8 8h2M12.2 8h2M3.6 3.6l1.4 1.4M11 11l1.4 1.4M12.4 3.6L11 5M5 11l-1.4 1.4\"/></svg>"
292
454
  };
293
455
  return ICONS[name] || ICONS.grid;
294
456
  }
457
+ var toolbarEnv = function() {
458
+ return {
459
+ find: function(s) {
460
+ return document.querySelector(s);
461
+ },
462
+ findPanel: function(s) {
463
+ return null;
464
+ },
465
+ dispatch: function(event, detail) {
466
+ window.dispatchEvent(new CustomEvent(event, { detail }));
467
+ return true;
468
+ },
469
+ findByText: function(texts) {
470
+ var buttons = document.querySelectorAll("button");
471
+ for (var bi = 0; bi < buttons.length; bi++) {
472
+ var label = (buttons[bi].textContent || "").trim();
473
+ for (var ti = 0; ti < texts.length; ti++) if (label === texts[ti]) return buttons[bi];
474
+ }
475
+ return null;
476
+ },
477
+ runCommand: function(name) {
478
+ return typeCommandIntoComposer(name);
479
+ }
480
+ };
481
+ };
482
+ function typeCommandIntoComposer(name) {
483
+ var seat = document.querySelector("[data-composer-seat]");
484
+ var el = seat !== null && seat !== void 0 ? seat.querySelector("textarea, [contenteditable=\"true\"]") : document.querySelector("textarea, [contenteditable=\"true\"]");
485
+ if (el === null || el === void 0) return false;
486
+ var text = "/" + name;
487
+ if (el instanceof HTMLTextAreaElement) {
488
+ var setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
489
+ if (setter !== void 0 && setter.set !== void 0) setter.set.call(el, text);
490
+ else el.value = text;
491
+ } else if (el.isContentEditable) el.textContent = text;
492
+ else return false;
493
+ el.dispatchEvent(new InputEvent("input", {
494
+ bubbles: true,
495
+ inputType: "insertText",
496
+ data: text
497
+ }));
498
+ el.focus();
499
+ el.dispatchEvent(new KeyboardEvent("keydown", {
500
+ key: "Enter",
501
+ code: "Enter",
502
+ bubbles: true,
503
+ cancelable: true
504
+ }));
505
+ return true;
506
+ }
295
507
  function toolbarAction(kind) {
296
508
  if (kind === "plugin") {
297
509
  closeSidePanelsBeforePluginCenter();
@@ -317,7 +529,7 @@ window.__ModuleLoader__.load({
317
529
  }
318
530
  }
319
531
  function createToolbar() {
320
- if (win.__SSID_SHELL__ === true) return;
532
+ if (win.__SSID_SHELL__ === true && !qtState.shellVisible) return;
321
533
  if (document.getElementById(TOOLBAR_ID) !== null) return;
322
534
  var root = document.createElement("div");
323
535
  root.id = TOOLBAR_ID;
@@ -341,33 +553,66 @@ window.__ModuleLoader__.load({
341
553
  "dsh-better-sidebar.bottom": "bottom",
342
554
  "dsh-session-manager": "sessions"
343
555
  };
344
- var items = [];
345
- for (var ai = 0; ai < BUILTIN_ADAPTERS.length; ai++) {
346
- var adapter = BUILTIN_ADAPTERS[ai];
347
- var kind = TOOLBAR_KIND_BY_ADAPTER[adapter.id];
348
- if (kind === void 0) continue;
349
- items.push({
350
- kind,
351
- adapter
352
- });
353
- }
354
- for (var i = 0; i < items.length; i++) {
556
+ var adapterIconHtml = function(adapter, kind) {
557
+ if (adapter.icon.source === "custom") {
558
+ var value = adapter.icon.value;
559
+ if (value.indexOf("<") === 0) return value;
560
+ return toolbarIcon(value);
561
+ }
562
+ return kind !== null ? toolbarIcon(kind) : toolbarIcon("grid");
563
+ };
564
+ var renderButton = function(adapter, kind) {
355
565
  var b = document.createElement("button");
356
566
  b.type = "button";
357
567
  b.className = "ssid-tb-btn";
358
- b.innerHTML = toolbarIcon(items[i].kind) + "<span></span>";
359
- b.setAttribute("aria-label", "");
360
- b.title = "";
361
- trackLocale(b, "tb." + items[i].kind, "text-span");
362
- trackLocale(b, "tb." + items[i].kind, "aria");
363
- trackLocale(b, "tb." + items[i].kind, "title");
364
- (function(kind) {
365
- b.addEventListener("click", function() {
568
+ b.setAttribute("data-adapter-id", adapter.id);
569
+ b.innerHTML = adapterIconHtml(adapter, kind) + "<span></span>";
570
+ if (kind !== null) {
571
+ b.setAttribute("aria-label", "");
572
+ b.title = "";
573
+ trackLocale(b, "tb." + kind, "text-span");
574
+ trackLocale(b, "tb." + kind, "aria");
575
+ trackLocale(b, "tb." + kind, "title");
576
+ } else {
577
+ var label = adapter.label !== void 0 ? adapter.label : adapter.id;
578
+ var textSpan = b.querySelector("span");
579
+ if (textSpan !== null) textSpan.textContent = label;
580
+ b.setAttribute("aria-label", label);
581
+ b.title = label;
582
+ }
583
+ b.addEventListener("click", function() {
584
+ if (kind !== null) {
366
585
  toolbarAction(kind);
367
- });
368
- })(items[i].kind);
586
+ return;
587
+ }
588
+ runAdapter(adapter, toolbarEnv());
589
+ });
369
590
  panel.appendChild(b);
591
+ };
592
+ for (var ai = 0; ai < BUILTIN_ADAPTERS.length; ai++) {
593
+ var adapter = BUILTIN_ADAPTERS[ai];
594
+ var kind = TOOLBAR_KIND_BY_ADAPTER[adapter.id];
595
+ renderButton(adapter, kind !== void 0 ? kind : null);
370
596
  }
597
+ var fetchUserAdapters = function() {
598
+ fetch("/quick-toolbar/api/adapters").then(function(r) {
599
+ return r.json();
600
+ }).then(function(data) {
601
+ var envelope = data !== null && typeof data === "object" ? data : void 0;
602
+ var rows = envelope !== void 0 && envelope.ok === true && envelope.value !== null && typeof envelope.value === "object" ? envelope.value.adapters : void 0;
603
+ if (!Array.isArray(rows)) return;
604
+ for (var ui = 0; ui < rows.length; ui++) {
605
+ var user = rows[ui];
606
+ if (user === null || typeof user !== "object") continue;
607
+ var userKind = TOOLBAR_KIND_BY_ADAPTER[user.id];
608
+ try {
609
+ var old = panel.querySelector("[data-adapter-id=\"" + user.id.replace(/"/g, "\\\"") + "\"]");
610
+ if (old !== null && old.parentNode === panel) panel.removeChild(old);
611
+ } catch (_e) {}
612
+ renderButton(user, userKind !== void 0 ? userKind : null);
613
+ }
614
+ }).catch(function() {});
615
+ };
371
616
  var ball = document.createElement("button");
372
617
  ball.type = "button";
373
618
  ball.id = TOOLBAR_ID + "-ball";
@@ -384,13 +629,10 @@ window.__ModuleLoader__.load({
384
629
  var BALL_R = 18;
385
630
  var expanded = false;
386
631
  var ballX = 0, ballY = 0;
387
- try {
388
- var savedPos = JSON.parse(String(localStorage.getItem(TOOLBAR_POS_KEY) || "null"));
389
- if (savedPos !== null && typeof savedPos.x === "number" && typeof savedPos.y === "number") {
390
- ballX = savedPos.x;
391
- ballY = savedPos.y;
392
- }
393
- } catch (_e) {}
632
+ if (qtState.pos !== null) {
633
+ ballX = qtState.pos.x;
634
+ ballY = qtState.pos.y;
635
+ }
394
636
  if (ballX === 0 && ballY === 0) {
395
637
  ballX = window.innerWidth - BALL_SIZE - 16;
396
638
  ballY = window.innerHeight - BALL_SIZE - 16;
@@ -475,14 +717,22 @@ window.__ModuleLoader__.load({
475
717
  }
476
718
  var kids = panel.children;
477
719
  for (var ki = 0; ki < kids.length; ki++) kids[ki].style.transitionDelay = collapsed ? "0ms" : 40 + ki * 24 + "ms";
478
- try {
479
- localStorage.setItem(TOOLBAR_COLLAPSED_KEY, collapsed ? "1" : "0");
480
- } catch (_e) {}
720
+ qtState.collapsed = collapsed;
721
+ saveState();
481
722
  };
482
- var pinned = false;
483
723
  try {
484
- pinned = localStorage.getItem(TOOLBAR_PINNED_KEY) === "1";
724
+ new ResizeObserver(function() {
725
+ if (!expanded) return;
726
+ var w2 = panel.offsetWidth + 2;
727
+ var h2 = panel.offsetHeight + 2;
728
+ if (root.offsetWidth !== w2 || root.offsetHeight !== h2) {
729
+ root.style.width = w2 + "px";
730
+ root.style.height = h2 + "px";
731
+ }
732
+ }).observe(panel);
485
733
  } catch (_e) {}
734
+ var pinned = false;
735
+ pinned = qtState.pinned;
486
736
  var applyPin = function() {
487
737
  pinBtn.style.color = pinned ? "var(--dsw-alias-interactive-accent, #4d9fff)" : "";
488
738
  pinBtn.style.opacity = pinned ? "1" : "";
@@ -500,11 +750,11 @@ window.__ModuleLoader__.load({
500
750
  setCollapsed(pinned ? false : true);
501
751
  applyPin();
502
752
  applyLocale();
753
+ fetchUserAdapters();
503
754
  pinBtn.addEventListener("click", function() {
504
755
  pinned = !pinned;
505
- try {
506
- localStorage.setItem(TOOLBAR_PINNED_KEY, pinned ? "1" : "0");
507
- } catch (_e) {}
756
+ qtState.pinned = pinned;
757
+ saveState();
508
758
  applyPin();
509
759
  setCollapsed(pinned ? false : true);
510
760
  });
@@ -575,12 +825,11 @@ window.__ModuleLoader__.load({
575
825
  ballY = Math.max(4, Math.min(Math.round(sp.by), vh - BALL_SIZE - 4));
576
826
  ball.style.left = ballX + "px";
577
827
  ball.style.top = ballY + "px";
578
- try {
579
- localStorage.setItem(TOOLBAR_POS_KEY, JSON.stringify({
580
- x: ballX,
581
- y: ballY
582
- }));
583
- } catch (_e) {}
828
+ qtState.pos = {
829
+ x: ballX,
830
+ y: ballY
831
+ };
832
+ saveState();
584
833
  };
585
834
  document.addEventListener("mousemove", onMove);
586
835
  document.addEventListener("mouseup", onUp);
@@ -598,16 +847,25 @@ window.__ModuleLoader__.load({
598
847
  tbStyle.setAttribute("data-dsh-quick-toolbar-toolbar", "");
599
848
  tbStyle.textContent = TOOLBAR_CSS;
600
849
  document.head.appendChild(tbStyle);
601
- createToolbar();
602
- window.addEventListener("load", function() {
603
- if (win.__SSID_SHELL__ !== true) return;
850
+ loadState(function() {
851
+ if (win.__SSID_SHELL__ === true && !qtState.shellVisible) return;
852
+ createToolbar();
853
+ });
854
+ var hideIfShell = function() {
855
+ if (win.__SSID_SHELL__ !== true) return false;
856
+ if (qtState.shellVisible) return false;
604
857
  var tb = document.getElementById(TOOLBAR_ID);
605
858
  if (tb !== null) tb.remove();
606
859
  var tbBall = document.getElementById(TOOLBAR_ID + "-ball");
607
860
  if (tbBall !== null) tbBall.remove();
608
- var st = document.querySelector("style[data-dsh-quick-toolbar]");
609
- if (st !== null) st.textContent = BASE_CSS + "\n" + SHELL_CSS.join("\n");
610
- });
861
+ return true;
862
+ };
863
+ window.addEventListener("load", hideIfShell);
864
+ var shellTries = 0;
865
+ var shellTimer = setInterval(function() {
866
+ shellTries++;
867
+ if (hideIfShell() || shellTries >= 5) clearInterval(shellTimer);
868
+ }, 1500);
611
869
  mountSmHeaderButton();
612
870
  new MutationObserver(function() {
613
871
  applyLocale();
@@ -621,6 +879,19 @@ window.__ModuleLoader__.load({
621
879
  });
622
880
  window.addEventListener("ssid:titlebar", function(event) {
623
881
  var detail = event !== null && typeof event === "object" ? event.detail : void 0;
882
+ if (detail === "quick-toolbar-toggle") {
883
+ var nextOn = !qtState.shellVisible;
884
+ qtState.shellVisible = nextOn;
885
+ saveState();
886
+ if (nextOn) createToolbar();
887
+ else {
888
+ var tbEl = document.getElementById(TOOLBAR_ID);
889
+ if (tbEl !== null) tbEl.remove();
890
+ var tbBallEl = document.getElementById(TOOLBAR_ID + "-ball");
891
+ if (tbBallEl !== null) tbBallEl.remove();
892
+ }
893
+ return;
894
+ }
624
895
  if (detail === "session-manager") {
625
896
  clickSmOpenButton();
626
897
  return;
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join } from "node:path";
3
- import { readFileSync } from "node:fs";
3
+ import { readFileSync, renameSync, writeFileSync } from "node:fs";
4
4
  import { z } from "zod";
5
5
  //#region src/schema.ts
6
6
  /**
@@ -114,10 +114,89 @@ const adaptersRouteDefinition = {
114
114
  }
115
115
  }
116
116
  };
117
+ const STATE_PATH = join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "quick-toolbar-state.json");
118
+ const STATE_ROUTE = "/quick-toolbar/api/state";
119
+ /** 默认状态(文件缺失/字段缺失时取默认,宽松向下兼容)。 */
120
+ function defaultState() {
121
+ return {
122
+ pos: null,
123
+ collapsed: true,
124
+ pinned: false,
125
+ shellVisible: false
126
+ };
127
+ }
128
+ /** 归一化(防御非法文件:字段级回退默认,不因一条坏字段全丢)。 */
129
+ function normalizeState(raw) {
130
+ const d = defaultState();
131
+ const s = raw !== null && typeof raw === "object" ? raw : {};
132
+ const pos = s.pos;
133
+ if (pos !== null && pos !== void 0 && typeof pos === "object" && pos !== null) {
134
+ const p = pos;
135
+ if (typeof p.x === "number" && typeof p.y === "number") d.pos = {
136
+ x: p.x,
137
+ y: p.y
138
+ };
139
+ }
140
+ if (typeof s.collapsed === "boolean") d.collapsed = s.collapsed;
141
+ if (typeof s.pinned === "boolean") d.pinned = s.pinned;
142
+ if (typeof s.shellVisible === "boolean") d.shellVisible = s.shellVisible;
143
+ return d;
144
+ }
145
+ /** 读状态文件(缺失/非法 → 默认;不抛)。 */
146
+ function readStateFile() {
147
+ try {
148
+ return normalizeState(JSON.parse(readFileSync(STATE_PATH, "utf8")));
149
+ } catch {
150
+ return defaultState();
151
+ }
152
+ }
153
+ /** 原子写状态文件(tmp + rename,防中断半写)。 */
154
+ function writeStateFile(state) {
155
+ const tmp = STATE_PATH + ".tmp";
156
+ writeFileSync(tmp, JSON.stringify(state));
157
+ renameSync(tmp, STATE_PATH);
158
+ }
159
+ const stateRouteDefinition = {
160
+ kind: "exact",
161
+ path: STATE_ROUTE,
162
+ handler: async (req, res) => {
163
+ if (req.method === "GET") {
164
+ sendJson(res, 200, {
165
+ ok: true,
166
+ state: readStateFile()
167
+ });
168
+ return;
169
+ }
170
+ if (req.method === "POST") {
171
+ let raw = "";
172
+ req.on("data", (chunk) => {
173
+ raw += chunk;
174
+ });
175
+ await new Promise((resolve) => req.on("end", () => {
176
+ resolve();
177
+ }));
178
+ try {
179
+ writeStateFile(normalizeState(JSON.parse(raw)));
180
+ sendJson(res, 200, { ok: true });
181
+ } catch {
182
+ sendJson(res, 200, {
183
+ ok: false,
184
+ error: "invalid-state"
185
+ });
186
+ }
187
+ return;
188
+ }
189
+ sendJson(res, 405, {
190
+ ok: false,
191
+ error: "method-not-allowed"
192
+ });
193
+ }
194
+ };
117
195
  function apply(ctx) {
118
196
  ctx.inject(["webServer"], ((wsCtx) => {
119
197
  wsCtx.webServer.register(adaptersRouteDefinition);
198
+ wsCtx.webServer.register(stateRouteDefinition);
120
199
  }));
121
200
  }
122
201
  //#endregion
123
- export { apply, name };
202
+ export { apply, defaultState, name, normalizeState, readStateFile, writeStateFile };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-quick-toolbar",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
4
4
  "description": "插件按钮聚合器:聚合三方插件散乱按钮(插件中心/侧栏/底栏/会话管理…)到统一入口——SSiD 标题栏 / web 悬浮球(iOS 小白点式);适配器驱动,内置适配集;LLM 可生成适配(驻场工程师模式)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",