@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 +16 -9
- package/docs/shots/web-panel.png +0 -0
- package/lib/client.js +324 -53
- package/lib/index.js +81 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
# @max-null/dsh-quick-toolbar
|
|
1
|
+
# @max-null/dsh-quick-toolbar
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
package/docs/shots/web-panel.png
CHANGED
|
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
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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.
|
|
359
|
-
b.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
b.
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
} catch (_e) {}
|
|
720
|
+
qtState.collapsed = collapsed;
|
|
721
|
+
saveState();
|
|
481
722
|
};
|
|
482
|
-
var pinned = false;
|
|
483
723
|
try {
|
|
484
|
-
|
|
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
|
-
|
|
506
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
609
|
-
|
|
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