@max-null/dsh-quick-toolbar 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MaxNull
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @max-null/dsh-quick-toolbar — 插件按钮聚合器
2
+
3
+ 把三方插件散乱的按钮(插件中心/侧栏/底栏/会话管理…)聚合到**统一入口**:
4
+ SSiD 壳 → 标题栏按钮组;DSH web → **iOS 小白点式悬浮球**(自由定位拖拽、球↔面板 morph 展开、球永远锁定面板屏幕外侧角)。
5
+ 适配器驱动:内置适配集开箱即识;**用户环境下 LLM 按模板生成适配**(驻场工程师模式)——不要求三方插件配合。
6
+
7
+ ## 截图
8
+
9
+ | SSiD 标题栏(壳) | web 悬浮球 | 聚合面板 |
10
+ |---|---|---|
11
+ | ![SSiD 标题栏](docs/shots/ssid-titlebar.png) | ![web 悬浮球](docs/shots/web-float.png) | ![聚合面板](docs/shots/web-panel.png) |
12
+
13
+ > 截图环境:SSiD 0.1.15 壳(标题栏)与 DSH master web(悬浮球/面板);聚合面板为 web 右下角悬浮球 morph 展开态。
14
+
15
+ ## 安装
16
+
17
+ ```sh
18
+ # 走 DSH 插件安装(或 SSiD 预制 vendor)
19
+ dsh plugin --profile web add @max-null/dsh-quick-toolbar
20
+ ```
21
+
22
+ ## 使用
23
+
24
+ - **聚合按钮**:标题栏/悬浮球点开面板 → 一键触发各插件功能(再点关闭——会话管理探测弹窗关闭按钮)。
25
+ - **用户适配器**:复制 `adapters.prompt.md` + 环境描述 → 让 LLM 生成 `{ "adapters": [...] }` → 写入 `~/.dsh/quick-toolbar-adapters.json` → 重启/热载生效(schema 校验,非法条目丢弃并报明细)。
26
+ - 定位失败/插件未装/被禁用 → 静默跳过(绝不误伤、绝不误点)。
27
+
28
+ ## 架构
29
+
30
+ - `src/adapters.ts`:适配器 schema + 内置适配器集(黄金示例)
31
+ - `src/behaviors.ts`:行为库(click / toggle-panel / dispatch-event / open-settings / command)
32
+ - `src/engine.ts`:执行器(防御执行)
33
+ - `src/schema.ts`:用户配置 zod 校验(LLM 产物防线)
34
+ - `src/index.ts`:host 半 `GET /quick-toolbar/api/adapters`
35
+ - 设计文档:`doc/设计/2026-08-30-quick-toolbar-独立化设计方案.md`
36
+
37
+ ## 开发
38
+
39
+ ```sh
40
+ pnpm install
41
+ pnpm typecheck && pnpm test && pnpm build # L1 门槛(20+ 用例)
42
+ ```
43
+
44
+ ## SSID 系列
45
+
46
+ SSiD 全家桶([max-null-plugins](https://github.com/Max-Null))的一员;SSiD 壳内与标题栏桥接协作(`__SSID_SHELL__` 分支)。
@@ -0,0 +1,6 @@
1
+ # @max-null/dsh-quick-toolbar bundle patch: one insert over the profile root.
2
+ # Host half is a no-op; the client half injects CSS aligning the
3
+ # dsh-better-sidebar toggle cluster with the DSH session header utilities row.
4
+ - insert:
5
+ - id: dsh-quick-toolbar
6
+ name: '@max-null/dsh-quick-toolbar'
Binary file
Binary file
Binary file
package/lib/client.js ADDED
@@ -0,0 +1,654 @@
1
+ //#region src/adapters.ts
2
+ /**
3
+ * 内置适配器集(黄金示例 = few-shot 语料 + 兜底;2026-08-30 现状按钮)。
4
+ * 注意:v0.1.x 时代这些按钮经「标题栏事件」驱动(ssid:titlebar),本表的
5
+ * act 描述同一行为的适配器形态;引擎执行器接入后逐步切换为适配器消费。
6
+ */
7
+ const BUILTIN_ADAPTERS = [
8
+ {
9
+ id: "dsh-plugin-center",
10
+ button: "[class*=\"pc-headerbtn\"]",
11
+ icon: { source: "from-button" },
12
+ label: "插件中心",
13
+ act: {
14
+ kind: "dispatch-event",
15
+ event: "ssid:titlebar",
16
+ detail: "plugin-center"
17
+ },
18
+ hide: true
19
+ },
20
+ {
21
+ id: "dsh-better-sidebar.sidebar",
22
+ button: "[class*=\"toggleCluster\"]",
23
+ icon: { source: "from-button" },
24
+ label: "侧栏",
25
+ act: {
26
+ kind: "dispatch-event",
27
+ event: "ssid:titlebar",
28
+ detail: "sidebar"
29
+ },
30
+ hide: true
31
+ },
32
+ {
33
+ id: "dsh-better-sidebar.bottom",
34
+ button: "[class*=\"toggleCluster\"]",
35
+ icon: { source: "from-button" },
36
+ label: "底栏",
37
+ act: {
38
+ kind: "dispatch-event",
39
+ event: "ssid:titlebar",
40
+ detail: "bottom"
41
+ },
42
+ hide: true
43
+ },
44
+ {
45
+ id: "dsh-session-manager",
46
+ button: ".sm-footerBtn",
47
+ icon: { source: "from-button" },
48
+ label: "会话管理",
49
+ act: {
50
+ kind: "toggle-panel",
51
+ close: ".sm-modal .close"
52
+ },
53
+ hide: true
54
+ }
55
+ ];
56
+ //#endregion
57
+ //#region src/client.ts
58
+ /**
59
+ * @max-null/dsh-quick-toolbar — browser half.
60
+ *
61
+ * SSiD 标题栏统一按钮组的 DSH 侧执行器(v0.4.0):
62
+ *
63
+ * 1. 隐藏 DSH 内的原按钮(插件中心 header 按钮 + better-sidebar 的
64
+ * toggleCluster),消除双入口与错位——统一由自绘标题栏按钮组接管。
65
+ * 2. 监听 main 进程经 `mainView.webContents.executeJavaScript` 派发的
66
+ * `ssid:titlebar` CustomEvent:
67
+ * detail = 'plugin-center' → win.__pluginCenterToggle?.()
68
+ * (plugin-center v0.1.7+ 全局控制器:再点关闭;
69
+ * 老版回退 __pluginCenterOpen)
70
+ * detail = 'sidebar' → 先 __pluginCenterClose?.()(互斥:模态让位
71
+ * 工具面板),再 click toggleCluster 最后一个按钮
72
+ * detail = 'bottom' → 同上,click 第一个按钮(窄屏无底栏则跳过)
73
+ * detail = 'session-manager' → 打开会话管理面板(桥接 click footer 按钮)
74
+ * 3. 会话管理入口统一:隐藏 dsh-session-manager 的 footer 按钮
75
+ * (.sm-footerBtn,display:none 后 JS .click() 仍触发 React onClick),
76
+ * 在会话 header(.sm-header,归档/移动按钮行)内嵌「会话管理」按钮;
77
+ * shell 标题栏按钮组经 ssid:titlebar 事件也可打开(web 无标题栏时
78
+ * 内嵌按钮兜底)。
79
+ * 4. 悬浮快捷工具栏(v0.4.0):被屏蔽/接管按钮(插件中心、侧栏、底栏、
80
+ * 会话管理)的常驻出口——可拖拽移动(localStorage 持久化)、可展开/
81
+ * 收起;壳环境与无壳 web 均显示(与标题栏按钮组互为冗余,用户可收起)。
82
+ * 5. i18n(v0.4.2):全部用户可见文案(工具栏标题/按钮/aria/title)跟随
83
+ * document.documentElement.lang(zh* → 中文,其余 → 英文),并监听
84
+ * lang 变化动态切换(DSH 异步设置语言,初始可能为静态 HTML 的 en)。
85
+ *
86
+ * 选择器只用 CSS Modules 的原始段(编译后如 nArs4W_toggleCluster),
87
+ * 与哈希前缀无关:better-sidebar / plugin-center 升级只要不改类名即有效。
88
+ * 隐藏元素仍可被 JS 的 .click() 触发(无需可见),与壳已有的
89
+ * 「侧边栏自动诊断」同模式。
90
+ */
91
+ window.__ModuleLoader__.load({
92
+ id: "@max-null/dsh-quick-toolbar",
93
+ factory: (require) => {
94
+ var module = { exports: {} };
95
+ var exports = module.exports;
96
+ var win = window;
97
+ var BASE_CSS = [
98
+ "/* SSiD 标题栏统一按钮组:隐藏 DSH 内原按钮,避免双入口与错位 */",
99
+ ".sm-footerBtn { display: none !important; }",
100
+ "[class*=\"pc-headerbtn\"] { display: none !important; }",
101
+ "[class*=\"toggleCluster\"] { display: none !important; }",
102
+ "#__open-sea-skin-btn__ { visibility: hidden !important; pointer-events: none !important; }"
103
+ ].join("\n");
104
+ var SHELL_CSS = [];
105
+ /**
106
+ * 从 toggleCluster 按钮的 aria-label 反推面板状态——不依赖 CSS 类名
107
+ * 通配(DSH 官方 UI 也有 css.panel 类,[class*="panel"] 会误匹配,
108
+ * 2026-08-19 用户实测「打开插件中心同时打开右栏」)。
109
+ * better-sidebar 按钮语义(src/client/locales.ts 实证):
110
+ * 侧栏按钮 aria-label:开着='折叠侧边栏'(collapse) / 关着='展开侧边栏'(expand)
111
+ * 底栏按钮 aria-label:开着='折叠底部面板'(collapseBottomPanel) / 关着='展开底部面板'(expandBottomPanel)
112
+ * 面板开着 = 对应按钮 label 是「折叠」语义(collapse/折叠)。
113
+ */
114
+ function clusterSideButtons() {
115
+ var cluster = document.querySelector("[class*=\"toggleCluster\"]");
116
+ if (cluster === null) return {
117
+ sidebar: null,
118
+ bottom: null
119
+ };
120
+ var buttons = cluster.querySelectorAll("button");
121
+ var sidebar = null, bottom = null;
122
+ for (var i = 0; i < buttons.length; i++) {
123
+ var label = (buttons[i].getAttribute("aria-label") || "").toLowerCase();
124
+ if (label.indexOf("bottom") !== -1 || label.indexOf("底部") !== -1) bottom = buttons[i];
125
+ else sidebar = buttons[i];
126
+ }
127
+ return {
128
+ sidebar,
129
+ bottom
130
+ };
131
+ }
132
+ function isPanelOpen(button) {
133
+ if (button === null || button === void 0) return false;
134
+ var label = (button.getAttribute("aria-label") || "").toLowerCase();
135
+ return label.indexOf("collapse") !== -1 || label.indexOf("折叠") !== -1;
136
+ }
137
+ function clickButton(button) {
138
+ if (button !== null && button !== void 0 && !button.disabled) button.click();
139
+ }
140
+ /**
141
+ * 反向互斥(2026-08-19 用户补充):打开插件中心前,若侧栏/底栏
142
+ * 开着则先收起(点其 toggleCluster 按钮),避免弹窗被面板遮挡。
143
+ * 两个独立判断:右栏+底栏同时开着时都要收起(不能用 if/else if,
144
+ * 否则短路漏掉一个——用户实测「双开时底栏保持打开」)。
145
+ */
146
+ function closeSidePanelsBeforePluginCenter() {
147
+ var btns = clusterSideButtons();
148
+ if (isPanelOpen(btns.sidebar)) clickButton(btns.sidebar);
149
+ if (isPanelOpen(btns.bottom)) clickButton(btns.bottom);
150
+ }
151
+ var LOCALE_TARGETS = [];
152
+ function localeIsZh() {
153
+ return (document.documentElement.lang || "").toLowerCase().indexOf("zh") === 0;
154
+ }
155
+ var LOCALE_DICT = {
156
+ "tb.title": ["快捷工具栏", "Quick Toolbar"],
157
+ "tb.pin": ["钉住", "Pin"],
158
+ "tb.unpin": ["取消钉住", "Unpin"],
159
+ "tb.pinAria": ["钉住/取消钉住", "Pin/Unpin"],
160
+ "tb.expandAria": ["展开快捷工具栏", "Expand quick toolbar"],
161
+ "tb.plugin": ["插件中心", "Plugin center"],
162
+ "tb.sidebar": ["侧栏", "Sidebar"],
163
+ "tb.bottom": ["底栏", "Bottom panel"],
164
+ "tb.sessions": ["会话管理", "Sessions"],
165
+ "sm.open": ["会话管理", "Sessions"],
166
+ "sm.openTitle": ["打开会话管理面板", "Open session manager"]
167
+ };
168
+ function applyLocale() {
169
+ var zh = localeIsZh();
170
+ for (var i = 0; i < LOCALE_TARGETS.length; i++) {
171
+ var tgt = LOCALE_TARGETS[i];
172
+ var pair = LOCALE_DICT[tgt.key];
173
+ if (pair === void 0) continue;
174
+ var text = pair[zh ? 0 : 1];
175
+ if (tgt.attr === "text") tgt.el.textContent = text;
176
+ else if (tgt.attr === "text-span") {
177
+ var s = tgt.el.querySelector("span");
178
+ if (s !== null) s.textContent = text;
179
+ } else if (tgt.attr === "title") tgt.el.title = text;
180
+ else tgt.el.setAttribute("aria-label", text);
181
+ }
182
+ }
183
+ function trackLocale(el, key, attr) {
184
+ LOCALE_TARGETS.push({
185
+ el,
186
+ key,
187
+ attr
188
+ });
189
+ }
190
+ /**
191
+ * 会话管理面板开关桥(v0.4.1,支持 toggle):dsh-session-manager 0.4.x
192
+ * 的面板打开是单向的(footer 按钮 onClick 恒定 panelStore.set(true),
193
+ * store 在插件模块闭包内,外部 bundle 无法直接调用),关闭只有面板内
194
+ * 「关闭」按钮(onClose → set(false))。桥接按钮做成 opened → 关 /
195
+ * closed → 开 的切换:
196
+ * ① 已打开(.sm-panel 可见)→ 点击面板内「关闭」按钮
197
+ * ② 未打开 → 按承载版本二选一:
198
+ * - master(dsh-session-manager 0.4.x):footer 按钮已被 display:none,
199
+ * 但 .click() 仍触发其 React onClick(panelStore.set(true));面板
200
+ * 是 SafePanel 模态,不在按钮内,display:none 不影响其显示。
201
+ * - rc.2(0.2.x):header utilities 槽的「会话管理」按钮带稳定标识
202
+ * [data-dsh-header-button],click 它 → setDrawer({open:true,view:'manage'})。
203
+ */
204
+ function isSmPanelOpen() {
205
+ var p = document.querySelector(".sm-panel");
206
+ return p !== null && p.offsetParent !== null;
207
+ }
208
+ function clickSmPanelClose() {
209
+ var p = document.querySelector(".sm-panel");
210
+ if (p === null) return false;
211
+ var dialog = p.closest("[role=\"dialog\"]");
212
+ var scopes = dialog !== null ? [dialog, p] : [p];
213
+ for (var s = 0; s < scopes.length; s++) {
214
+ var btns = scopes[s].querySelectorAll("button");
215
+ for (var i = 0; i < btns.length; i++) {
216
+ var label = btns[i].getAttribute("aria-label") || btns[i].getAttribute("title") || btns[i].textContent.trim();
217
+ if (label === "关闭" || label === "Close") {
218
+ btns[i].click();
219
+ return true;
220
+ }
221
+ }
222
+ }
223
+ return false;
224
+ }
225
+ function clickSmOpenButton() {
226
+ if (isSmPanelOpen()) {
227
+ clickSmPanelClose();
228
+ return;
229
+ }
230
+ var footer = document.querySelector(".sm-footerBtn");
231
+ if (footer !== null && !footer.disabled) {
232
+ footer.click();
233
+ return;
234
+ }
235
+ var manage = document.querySelector("[data-dsh-header-button]");
236
+ if (manage !== null && !manage.disabled) manage.click();
237
+ }
238
+ /**
239
+ * 会话 header(.sm-header:归档/移动至工作区按钮行)内嵌「会话管理」
240
+ * 按钮,web 端(无自绘标题栏)也能打开面板;React 切换会话会重渲染
241
+ * .sm-header,靠 MutationObserver 保活(幂等:id 查重)。
242
+ */
243
+ function mountSmHeaderButton() {
244
+ var header = document.querySelector(".sm-header");
245
+ if (header === null) return;
246
+ if (document.getElementById("ssid-sm-open-btn") !== null) return;
247
+ var btn = document.createElement("button");
248
+ btn.type = "button";
249
+ btn.id = "ssid-sm-open-btn";
250
+ btn.className = "sm-headerBtn";
251
+ btn.setAttribute("aria-label", "会话管理");
252
+ btn.title = "打开会话管理面板";
253
+ btn.addEventListener("click", clickSmOpenButton);
254
+ trackLocale(btn, "sm.open", "text");
255
+ trackLocale(btn, "sm.open", "aria");
256
+ trackLocale(btn, "sm.openTitle", "title");
257
+ header.appendChild(btn);
258
+ applyLocale();
259
+ }
260
+ 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";
264
+ var TOOLBAR_CSS = [
265
+ "#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
+ "#ssid-toolbar *{box-sizing:border-box}",
267
+ "#ssid-toolbar.ssid-tb-expanded{border-radius:12px}",
268
+ "#ssid-toolbar-ball{position:fixed;z-index:10000;width:36px;height:36px;margin:0;padding:0;border:0;background:transparent;box-shadow:none;appearance:none;-webkit-appearance:none;pointer-events:none;display:flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-primary,#d8e0ea);opacity:.9;transition:opacity .18s ease}",
269
+ "#ssid-toolbar-ball svg{width:16px;height:16px}",
270
+ "#ssid-toolbar .ssid-tb-panel{position:absolute;left:0;top:0;display:flex;flex-direction:column;gap:4px;padding:6px;color:var(--dsw-alias-label-primary,#d8e0ea)}",
271
+ "#ssid-toolbar .ssid-tb-panel>*{opacity:0;transform:translateY(4px);transition:opacity .16s ease,transform .16s ease}",
272
+ "#ssid-toolbar.ssid-tb-expanded .ssid-tb-panel>*{opacity:1;transform:none}",
273
+ "#ssid-toolbar .ssid-tb-head{position:relative;height:22px;cursor:grab}",
274
+ "#ssid-toolbar .ssid-tb-head:active{cursor:grabbing}",
275
+ "#ssid-toolbar .ssid-tb-pin{position:absolute;top:2px;right:2px;width:26px;height:26px;border:0;background:transparent;color:var(--dsw-alias-label-tertiary,#7b8494);border-radius:7px;display:flex;align-items:center;justify-content:center;cursor:pointer;opacity:.75;transition:opacity .15s,background .15s}",
276
+ "#ssid-toolbar .ssid-tb-pin:hover{opacity:1;background:var(--dsw-alias-interactive-bg-hover,rgba(128,148,168,.14))}",
277
+ "#ssid-toolbar .ssid-tb-pin svg{width:15px;height:15px}",
278
+ "#ssid-toolbar .ssid-tb-btn{border:0;background:transparent;color:var(--dsw-alias-label-primary,#d8e0ea);border-radius:8px;height:30px;display:flex;align-items:center;gap:8px;padding:0 10px;font-size:12px;line-height:18px;cursor:pointer;white-space:nowrap;text-align:left}",
279
+ "#ssid-toolbar .ssid-tb-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,148,168,.14))}",
280
+ "#ssid-toolbar .ssid-tb-btn svg{flex:none;width:15px;height:15px;color:var(--dsw-alias-label-secondary,#98a2b3)}"
281
+ ].join("\n");
282
+ function toolbarIcon(name) {
283
+ var ICONS = {
284
+ grid: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" rx=\"1\"/><rect x=\"9\" y=\"2\" width=\"5\" height=\"5\" rx=\"1\"/><rect x=\"2\" y=\"9\" width=\"5\" height=\"5\" rx=\"1\"/><rect x=\"9\" y=\"9\" width=\"5\" height=\"5\" rx=\"1\"/></svg>",
285
+ plugin: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><rect x=\"2\" y=\"2\" width=\"5\" height=\"5\" rx=\"1\"/><rect x=\"9\" y=\"2\" width=\"5\" height=\"5\" rx=\"1\"/><rect x=\"2\" y=\"9\" width=\"5\" height=\"5\" rx=\"1\"/><rect x=\"9\" y=\"9\" width=\"5\" height=\"5\" rx=\"1\"/></svg>",
286
+ sidebar: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><rect x=\"2\" y=\"2\" width=\"12\" height=\"12\" rx=\"1\"/><path d=\"M10 2v12\"/></svg>",
287
+ bottom: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><rect x=\"2\" y=\"2\" width=\"12\" height=\"12\" rx=\"1\"/><path d=\"M2 10h12\"/></svg>",
288
+ 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
+ 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
+ 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>"
292
+ };
293
+ return ICONS[name] || ICONS.grid;
294
+ }
295
+ function toolbarAction(kind) {
296
+ if (kind === "plugin") {
297
+ closeSidePanelsBeforePluginCenter();
298
+ var toggle = win.__pluginCenterToggle;
299
+ if (typeof toggle === "function") {
300
+ toggle();
301
+ return;
302
+ }
303
+ var open = win.__pluginCenterOpen;
304
+ if (typeof open === "function") open();
305
+ return;
306
+ }
307
+ if (kind === "sidebar" || kind === "bottom") {
308
+ var close = win.__pluginCenterClose;
309
+ if (typeof close === "function") close();
310
+ var btns = clusterSideButtons();
311
+ clickButton(kind === "sidebar" ? btns.sidebar : btns.bottom);
312
+ return;
313
+ }
314
+ if (kind === "sessions") {
315
+ clickSmOpenButton();
316
+ return;
317
+ }
318
+ }
319
+ function createToolbar() {
320
+ if (win.__SSID_SHELL__ === true) return;
321
+ if (document.getElementById(TOOLBAR_ID) !== null) return;
322
+ var root = document.createElement("div");
323
+ root.id = TOOLBAR_ID;
324
+ var panel = document.createElement("div");
325
+ panel.className = "ssid-tb-panel";
326
+ var head = document.createElement("div");
327
+ head.className = "ssid-tb-head";
328
+ var pinBtn = document.createElement("button");
329
+ pinBtn.type = "button";
330
+ pinBtn.className = "ssid-tb-pin";
331
+ pinBtn.setAttribute("aria-label", "钉住/取消钉住");
332
+ pinBtn.title = "钉住";
333
+ pinBtn.innerHTML = toolbarIcon("pin");
334
+ trackLocale(pinBtn, "tb.pinAria", "aria");
335
+ trackLocale(pinBtn, "tb.pin", "title");
336
+ head.appendChild(pinBtn);
337
+ panel.appendChild(head);
338
+ var TOOLBAR_KIND_BY_ADAPTER = {
339
+ "dsh-plugin-center": "plugin",
340
+ "dsh-better-sidebar.sidebar": "sidebar",
341
+ "dsh-better-sidebar.bottom": "bottom",
342
+ "dsh-session-manager": "sessions"
343
+ };
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++) {
355
+ var b = document.createElement("button");
356
+ b.type = "button";
357
+ 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() {
366
+ toolbarAction(kind);
367
+ });
368
+ })(items[i].kind);
369
+ panel.appendChild(b);
370
+ }
371
+ var ball = document.createElement("button");
372
+ ball.type = "button";
373
+ ball.id = TOOLBAR_ID + "-ball";
374
+ ball.className = "ssid-tb-ball";
375
+ ball.setAttribute("aria-label", "展开快捷工具栏");
376
+ ball.title = "SSiD 快捷工具栏";
377
+ ball.innerHTML = toolbarIcon("menu");
378
+ trackLocale(ball, "tb.expandAria", "aria");
379
+ trackLocale(ball, "tb.title", "title");
380
+ root.appendChild(panel);
381
+ document.body.appendChild(root);
382
+ document.body.appendChild(ball);
383
+ var BALL_SIZE = 36;
384
+ var BALL_R = 18;
385
+ var expanded = false;
386
+ 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) {}
394
+ if (ballX === 0 && ballY === 0) {
395
+ ballX = window.innerWidth - BALL_SIZE - 16;
396
+ ballY = window.innerHeight - BALL_SIZE - 16;
397
+ }
398
+ ballX = Math.max(4, Math.min(ballX, window.innerWidth - BALL_SIZE - 4));
399
+ ballY = Math.max(4, Math.min(ballY, window.innerHeight - BALL_SIZE - 4));
400
+ root.style.left = ballX + "px";
401
+ root.style.top = ballY + "px";
402
+ ball.style.left = ballX + "px";
403
+ ball.style.top = ballY + "px";
404
+ var panelPlacement = function(W, H) {
405
+ var vw = window.innerWidth, vh = window.innerHeight;
406
+ var cx = ballX + BALL_R, cy = ballY + BALL_R;
407
+ var hor = cx + BALL_R + W <= vw - 8 ? "right" : "left";
408
+ var vert = cy + BALL_R + H <= vh - 8 ? "down" : "up";
409
+ var left = hor === "right" ? ballX : ballX + BALL_SIZE - W;
410
+ var top = vert === "down" ? ballY : ballY + BALL_SIZE - H;
411
+ left = Math.max(4, Math.min(left, vw - W - 4));
412
+ top = Math.max(4, Math.min(top, vh - H - 4));
413
+ return {
414
+ left,
415
+ top
416
+ };
417
+ };
418
+ var invertPlacement = function(r) {
419
+ var vw = window.innerWidth, vh = window.innerHeight;
420
+ var W = r.width, H = r.height;
421
+ var combos = [
422
+ {
423
+ hor: "right",
424
+ vert: "down"
425
+ },
426
+ {
427
+ hor: "left",
428
+ vert: "up"
429
+ },
430
+ {
431
+ hor: "right",
432
+ vert: "up"
433
+ },
434
+ {
435
+ hor: "left",
436
+ vert: "down"
437
+ }
438
+ ];
439
+ for (var ci = 0; ci < combos.length; ci++) {
440
+ var hor = combos[ci].hor;
441
+ var vert = combos[ci].vert;
442
+ var bx = hor === "right" ? r.left : r.left + W - BALL_SIZE;
443
+ var by = vert === "down" ? r.top : r.top + H - BALL_SIZE;
444
+ var cx2 = bx + BALL_R, cy2 = by + BALL_R;
445
+ var h2 = cx2 + BALL_R + W <= vw - 8 ? "right" : "left";
446
+ var v2 = cy2 + BALL_R + H <= vh - 8 ? "down" : "up";
447
+ if (h2 === hor && v2 === vert) return {
448
+ bx,
449
+ by
450
+ };
451
+ }
452
+ return {
453
+ bx: r.left,
454
+ by: r.top
455
+ };
456
+ };
457
+ var setCollapsed = function(collapsed) {
458
+ expanded = !collapsed;
459
+ ball.style.opacity = collapsed ? "" : "0";
460
+ if (collapsed) {
461
+ root.classList.remove("ssid-tb-expanded");
462
+ root.style.width = BALL_SIZE + "px";
463
+ root.style.height = BALL_SIZE + "px";
464
+ root.style.left = ballX + "px";
465
+ root.style.top = ballY + "px";
466
+ } else {
467
+ var W = panel.offsetWidth + 2;
468
+ var H = panel.offsetHeight + 2;
469
+ var p = panelPlacement(W, H);
470
+ root.style.width = W + "px";
471
+ root.style.height = H + "px";
472
+ root.style.left = p.left + "px";
473
+ root.style.top = p.top + "px";
474
+ root.classList.add("ssid-tb-expanded");
475
+ }
476
+ var kids = panel.children;
477
+ 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) {}
481
+ };
482
+ var pinned = false;
483
+ try {
484
+ pinned = localStorage.getItem(TOOLBAR_PINNED_KEY) === "1";
485
+ } catch (_e) {}
486
+ var applyPin = function() {
487
+ pinBtn.style.color = pinned ? "var(--dsw-alias-interactive-accent, #4d9fff)" : "";
488
+ pinBtn.style.opacity = pinned ? "1" : "";
489
+ trackLocale(pinBtn, pinned ? "tb.unpin" : "tb.pin", "title");
490
+ };
491
+ var applyPos = function(x, y) {
492
+ var vw = window.innerWidth, vh = window.innerHeight;
493
+ var w = root.offsetWidth || BALL_SIZE;
494
+ var h = root.offsetHeight || BALL_SIZE;
495
+ x = Math.max(4, Math.min(x, vw - w - 4));
496
+ y = Math.max(4, Math.min(y, vh - h - 4));
497
+ root.style.left = x + "px";
498
+ root.style.top = y + "px";
499
+ };
500
+ setCollapsed(pinned ? false : true);
501
+ applyPin();
502
+ applyLocale();
503
+ pinBtn.addEventListener("click", function() {
504
+ pinned = !pinned;
505
+ try {
506
+ localStorage.setItem(TOOLBAR_PINNED_KEY, pinned ? "1" : "0");
507
+ } catch (_e) {}
508
+ applyPin();
509
+ setCollapsed(pinned ? false : true);
510
+ });
511
+ var hideTimer = null;
512
+ var lastMouse = {
513
+ x: -1,
514
+ y: -1
515
+ };
516
+ var inShellArea = function() {
517
+ var r = root.getBoundingClientRect();
518
+ var m = 18;
519
+ return lastMouse.x >= r.left - m && lastMouse.x <= r.right + m && lastMouse.y >= r.top - m && lastMouse.y <= r.bottom + m;
520
+ };
521
+ var scheduleCollapse = function() {
522
+ if (pinned || !expanded) return;
523
+ if (hideTimer !== null) clearTimeout(hideTimer);
524
+ hideTimer = setTimeout(function() {
525
+ setCollapsed(true);
526
+ }, 220);
527
+ };
528
+ var cancelCollapse = function() {
529
+ if (hideTimer !== null) {
530
+ clearTimeout(hideTimer);
531
+ hideTimer = null;
532
+ }
533
+ };
534
+ document.addEventListener("mousemove", function(ev) {
535
+ lastMouse = {
536
+ x: ev.clientX,
537
+ y: ev.clientY
538
+ };
539
+ if (pinned) return;
540
+ var inside = inShellArea();
541
+ if (expanded) {
542
+ if (inside) cancelCollapse();
543
+ else scheduleCollapse();
544
+ } else if (inside) {
545
+ cancelCollapse();
546
+ setCollapsed(false);
547
+ }
548
+ });
549
+ var dragging = null;
550
+ head.addEventListener("mousedown", function(ev) {
551
+ if (ev.target === pinBtn) return;
552
+ var shellRect = root.getBoundingClientRect();
553
+ dragging = {
554
+ dx: ev.clientX - shellRect.left,
555
+ dy: ev.clientY - shellRect.top
556
+ };
557
+ ev.preventDefault();
558
+ var onMove = function(mev) {
559
+ if (!dragging) return;
560
+ applyPos(mev.clientX - dragging.dx, mev.clientY - dragging.dy);
561
+ };
562
+ var onUp = function() {
563
+ dragging = null;
564
+ document.removeEventListener("mousemove", onMove);
565
+ document.removeEventListener("mouseup", onUp);
566
+ var r = root.getBoundingClientRect();
567
+ var sp = invertPlacement({
568
+ left: r.left,
569
+ top: r.top,
570
+ width: r.width,
571
+ height: r.height
572
+ });
573
+ var vw = window.innerWidth, vh = window.innerHeight;
574
+ ballX = Math.max(4, Math.min(Math.round(sp.bx), vw - BALL_SIZE - 4));
575
+ ballY = Math.max(4, Math.min(Math.round(sp.by), vh - BALL_SIZE - 4));
576
+ ball.style.left = ballX + "px";
577
+ 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) {}
584
+ };
585
+ document.addEventListener("mousemove", onMove);
586
+ document.addEventListener("mouseup", onUp);
587
+ });
588
+ }
589
+ exports.inject = [];
590
+ exports.apply = function(ctx) {
591
+ if (win.__dshQuickToolbarInstalled === true) return;
592
+ win.__dshQuickToolbarInstalled = true;
593
+ var style = document.createElement("style");
594
+ style.setAttribute("data-dsh-quick-toolbar", "");
595
+ style.textContent = BASE_CSS + (SHELL_CSS.length > 0 && win.__SSID_SHELL__ === true ? "\n" + SHELL_CSS.join("\n") : "");
596
+ document.head.appendChild(style);
597
+ var tbStyle = document.createElement("style");
598
+ tbStyle.setAttribute("data-dsh-quick-toolbar-toolbar", "");
599
+ tbStyle.textContent = TOOLBAR_CSS;
600
+ document.head.appendChild(tbStyle);
601
+ createToolbar();
602
+ window.addEventListener("load", function() {
603
+ if (win.__SSID_SHELL__ !== true) return;
604
+ var tb = document.getElementById(TOOLBAR_ID);
605
+ if (tb !== null) tb.remove();
606
+ var tbBall = document.getElementById(TOOLBAR_ID + "-ball");
607
+ 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
+ });
611
+ mountSmHeaderButton();
612
+ new MutationObserver(function() {
613
+ applyLocale();
614
+ }).observe(document.documentElement, {
615
+ attributes: true,
616
+ attributeFilter: ["lang"]
617
+ });
618
+ new MutationObserver(mountSmHeaderButton).observe(document.body, {
619
+ childList: true,
620
+ subtree: true
621
+ });
622
+ window.addEventListener("ssid:titlebar", function(event) {
623
+ var detail = event !== null && typeof event === "object" ? event.detail : void 0;
624
+ if (detail === "session-manager") {
625
+ clickSmOpenButton();
626
+ return;
627
+ }
628
+ if (detail === "plugin-center") {
629
+ closeSidePanelsBeforePluginCenter();
630
+ var toggle = win.__pluginCenterToggle;
631
+ if (typeof toggle === "function") toggle();
632
+ else {
633
+ var open = win.__pluginCenterOpen;
634
+ if (typeof open === "function") open();
635
+ }
636
+ return;
637
+ }
638
+ if (detail === "open-sea-skin") {
639
+ var ossBtn = document.getElementById("__open-sea-skin-btn__");
640
+ if (ossBtn !== null && !ossBtn.disabled) ossBtn.click();
641
+ return;
642
+ }
643
+ if (detail === "sidebar" || detail === "bottom") {
644
+ var close = win.__pluginCenterClose;
645
+ if (typeof close === "function") close();
646
+ var btns = clusterSideButtons();
647
+ clickButton(detail === "sidebar" ? btns.sidebar : btns.bottom);
648
+ }
649
+ });
650
+ };
651
+ return module.exports;
652
+ }
653
+ });
654
+ //#endregion
package/lib/index.js ADDED
@@ -0,0 +1,123 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { readFileSync } from "node:fs";
4
+ import { z } from "zod";
5
+ //#region src/schema.ts
6
+ /**
7
+ * @max-null/dsh-quick-toolbar — 用户适配器配置 schema(zod)
8
+ *
9
+ * 用户配置(~/.dsh/quick-toolbar-adapters.json)与内置适配器同构;
10
+ * zod 校验是「LLM 生成输出」的第一道防线:非法条目丢弃并报 detail
11
+ * (不吞全表——其余合法条目继续生效)。
12
+ */
13
+ const adapterSchema = z.object({
14
+ id: z.string().min(1),
15
+ button: z.string().min(1),
16
+ icon: z.discriminatedUnion("source", [z.object({ source: z.literal("from-button") }), z.object({
17
+ source: z.literal("custom"),
18
+ value: z.string().min(1)
19
+ })]),
20
+ label: z.string().optional(),
21
+ act: z.discriminatedUnion("kind", [
22
+ z.object({ kind: z.literal("click") }),
23
+ z.object({
24
+ kind: z.literal("toggle-panel"),
25
+ close: z.string().optional()
26
+ }),
27
+ z.object({
28
+ kind: z.literal("dispatch-event"),
29
+ event: z.string(),
30
+ detail: z.string().optional()
31
+ }),
32
+ z.object({
33
+ kind: z.literal("open-settings"),
34
+ path: z.string().optional()
35
+ }),
36
+ z.object({
37
+ kind: z.literal("command"),
38
+ name: z.string()
39
+ })
40
+ ]),
41
+ hide: z.boolean().optional(),
42
+ enabled: z.boolean().optional()
43
+ });
44
+ const adaptersFileSchema = z.object({ adapters: z.array(adapterSchema) });
45
+ /** 解析用户配置文件:返回 { ok, value } 或 { ok: false, issues } */
46
+ function parseUserAdapters(raw) {
47
+ let parsed;
48
+ try {
49
+ parsed = JSON.parse(raw);
50
+ } catch {
51
+ return {
52
+ ok: false,
53
+ issues: ["invalid-json"]
54
+ };
55
+ }
56
+ const result = adaptersFileSchema.safeParse(parsed);
57
+ if (!result.success) return {
58
+ ok: false,
59
+ issues: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
60
+ };
61
+ return {
62
+ ok: true,
63
+ value: result.data
64
+ };
65
+ }
66
+ //#endregion
67
+ //#region src/index.ts
68
+ /**
69
+ * @max-null/dsh-quick-toolbar — host half.
70
+ *
71
+ * v1.5:暴露用户适配器配置读取 API(与 chat-rail 收藏 host 化同模式):
72
+ * GET /quick-toolbar/api/adapters → 读 ~/.dsh/quick-toolbar-adapters.json
73
+ * + zod 校验 → { ok: true, value } 或 { ok: false, error, detail }
74
+ * 客户端据此合并「内置适配器集 + 用户适配器」(用户配置 = 驻场 LLM 产出)。
75
+ */
76
+ const name = "@max-null/dsh-quick-toolbar";
77
+ /** 配置文件路径(用户级,profile 无关) */
78
+ const ADAPTERS_PATH = join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "quick-toolbar-adapters.json");
79
+ const ROUTE_PATH = "/quick-toolbar/api/adapters";
80
+ function sendJson(res, status, body) {
81
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
82
+ res.end(JSON.stringify(body));
83
+ }
84
+ const adaptersRouteDefinition = {
85
+ kind: "exact",
86
+ path: ROUTE_PATH,
87
+ handler: async (req, res) => {
88
+ if (req.method !== "GET") {
89
+ sendJson(res, 405, {
90
+ ok: false,
91
+ error: "method-not-allowed"
92
+ });
93
+ return;
94
+ }
95
+ try {
96
+ const parsed = parseUserAdapters(readFileSync(ADAPTERS_PATH, "utf8"));
97
+ if (!parsed.ok) {
98
+ sendJson(res, 200, {
99
+ ok: false,
100
+ error: "invalid-schema",
101
+ detail: parsed.issues
102
+ });
103
+ return;
104
+ }
105
+ sendJson(res, 200, {
106
+ ok: true,
107
+ value: parsed.value
108
+ });
109
+ } catch {
110
+ sendJson(res, 200, {
111
+ ok: true,
112
+ value: { adapters: [] }
113
+ });
114
+ }
115
+ }
116
+ };
117
+ function apply(ctx) {
118
+ ctx.inject(["webServer"], ((wsCtx) => {
119
+ wsCtx.webServer.register(adaptersRouteDefinition);
120
+ }));
121
+ }
122
+ //#endregion
123
+ export { apply, name };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@max-null/dsh-quick-toolbar",
3
+ "version": "0.1.0",
4
+ "description": "插件按钮聚合器:聚合三方插件散乱按钮(插件中心/侧栏/底栏/会话管理…)到统一入口——SSiD 标题栏 / web 悬浮球(iOS 小白点式);适配器驱动,内置适配集;LLM 可生成适配(驻场工程师模式)",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./cordis.patch.yml": "./cordis.patch.yml",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "lib",
15
+ "cordis.patch.yml",
16
+ "README.md",
17
+ "LICENSE",
18
+ "docs/shots"
19
+ ],
20
+ "dsh": {
21
+ "bundle": {
22
+ "patch": "./cordis.patch.yml"
23
+ },
24
+ "client": {
25
+ "inject": [],
26
+ "platform": "web"
27
+ }
28
+ },
29
+ "scripts": {
30
+ "build": "tsdown",
31
+ "typecheck": "tsc --noEmit",
32
+ "watch": "tsdown --watch",
33
+ "test": "node --import tsx/esm --test tests/**/*.spec.ts"
34
+ },
35
+ "license": "MIT",
36
+ "peerDependencies": {
37
+ "cordis": "^4.0.0-rc.7",
38
+ "dsh-better-sidebar": ">=0.13.0"
39
+ },
40
+ "peerDependenciesMeta": {
41
+ "dsh-better-sidebar": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.0.0",
47
+ "tsdown": "^0.22.2",
48
+ "tsx": "^4.0.0",
49
+ "typescript": "^5.6.0"
50
+ },
51
+ "dependencies": {
52
+ "zod": "^4.5.4"
53
+ }
54
+ }