@max-null/dsh-quick-toolbar 0.3.1 → 0.8.3
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 +68 -7
- package/docs/shots/four-scenes.png +0 -0
- package/docs/shots/llm-register-session.gif +0 -0
- package/docs/shots/panel-aggregated.gif +0 -0
- package/docs/shots/panel-collapsed.png +0 -0
- package/docs/shots/panel-pinned.png +0 -0
- package/docs/shots/panel-slide-delete-static.png +0 -0
- package/docs/shots/panel-slide-delete.gif +0 -0
- package/docs/shots/panel-with-llm.png +0 -0
- package/docs/shots/second-click-open.gif +0 -0
- package/lib/client.js +395 -43
- package/lib/index.js +92 -23
- package/package.json +54 -54
package/README.md
CHANGED
|
@@ -4,17 +4,47 @@
|
|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
-
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**插件按钮聚合器**:把三方插件散乱的按钮(插件中心/侧栏/底栏/会话管理…)聚合到统一入口——SSiD 壳 → 标题栏按钮组;DSH web → **iOS 小白点式悬浮球**(自由定位拖拽、球↔面板 morph
|
|
7
|
+
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**插件按钮聚合器**:把三方插件散乱的按钮(插件中心/侧栏/底栏/会话管理…)聚合到统一入口——SSiD 壳 → 标题栏按钮组;DSH web → **iOS 小白点式悬浮球**(自由定位拖拽、球↔面板 morph 展开、球永远锁定面板屏幕外侧角)。**核心价值 = 可扩展性**:本插件是「载体」(协议 + 引擎 + 注册表),**每个人都可以把**自己环境里任何插件的按钮**聚合进来**(LLM 一键注册 / 右键删除 = 注册制/注销制,全程不动插件源码、不写一行代码)——不要求第三方插件配合,不需要等作者适配。
|
|
8
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).
|
|
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). **Extensibility is the point**: this plugin is a *carrier* (protocol + engine + registry) — **everyone can register the buttons of any plugin in their own environment** (LLM one-click registration / right-click deregistration; no plugin source touched, no code written, no cooperation required from third-party plugins).
|
|
10
|
+
|
|
11
|
+
## 理念:载体 + 驻场 LLM——动态聚合,零代码增删
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
你的环境 = 插件 A · 插件 B · 插件 C(各自散落的按钮)
|
|
15
|
+
└── 聚合到本工具栏(一个入口)
|
|
16
|
+
▲
|
|
17
|
+
注册(➕ 一键,LLM 探查+数据注册)/ 注销(右键删除=移除注册)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **插件 = 载体**:本插件只提供注册协议(字段/校验/引擎行为)与渲染执行器;不内置任何场景适配——场景无限,没有"完美脚本"能预适配所有环境。
|
|
21
|
+
- **适配 = 驻场 LLM 域**:点击 ➕ 发起「添加按钮」会话,环境里的 LLM 自主探查(浏览器/代码能力验证选择器)→ 生成纯数据注册(一条 JSON)。**添加 = 注册制**(不动插件原代码)。
|
|
22
|
+
- **删除 = 注销制**:右键按钮 → 滑出「删除」→ 移除注册、原按钮恢复显示,环境回到未注册原状(零代码残留、零回滚成本)。
|
|
23
|
+
- **人人可用**:你不需要会写插件——告诉 LLM"把某个按钮加进来",它替你完成全部探查与注册;任何插件的按钮都能聚合,无论是官方的、社区的还是你自己的。
|
|
10
24
|
|
|
11
25
|
## 截图
|
|
12
26
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
|
27
|
+
### 动态聚合(GIF 实录)
|
|
28
|
+
|
|
29
|
+
| 聚合面板(内置 + LLM 注册按钮 + ➕) | 右键滑动删除(注销制) |
|
|
30
|
+
|---|---|
|
|
31
|
+
|  |  |
|
|
32
|
+
|
|
33
|
+
| 二次点击关闭(引擎探测语义) | ➕ LLM 注册会话(反问 → 探查 → 注册) |
|
|
34
|
+
|---|---|
|
|
35
|
+
|  |  |
|
|
36
|
+
|
|
37
|
+
### 一图两景:聚合面板 × LLM 反问(真实环境)
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
> Remote 为用户 LLM 现场注册的按钮(ds-harness-remote 入口)——演示「任何插件按钮都能聚合进统一入口」;右侧面板同时展示 LLM 按任务书先反问目标、再探查注册的完整流程。
|
|
16
42
|
|
|
17
|
-
|
|
43
|
+
| SSiD 标题栏(壳) | web 悬浮球 |
|
|
44
|
+
|---|---|
|
|
45
|
+
|  |  |
|
|
46
|
+
|
|
47
|
+
> 截图环境:SSiD 壳(标题栏/聚合面板)与 DSH web(悬浮球);Remote 为用户 LLM 现场注册的按钮(ds-harness-remote 入口),演示「统一入口聚合散落按钮」。
|
|
18
48
|
|
|
19
49
|
## 安装
|
|
20
50
|
|
|
@@ -28,16 +58,47 @@ dsh plugin --profile web add @max-null/dsh-quick-toolbar
|
|
|
28
58
|
- **聚合按钮**:标题栏/悬浮球点开面板 → 一键触发各插件功能(再点关闭——会话管理探测弹窗关闭按钮;设置面板语义锚点打开/再点关闭)。
|
|
29
59
|
- **内置入口**:悬浮球含「设置」(官方设置面板:引擎按语义锚点定位 footer 触发器,双 locale;再点关闭——DSH trigger 原生只开不关)。
|
|
30
60
|
- **壳环境(SSiD)**:悬浮球默认隐藏(标题栏接管入口);标题栏「悬浮球」开关按钮(圆圈+圆点图标)可开启。
|
|
31
|
-
-
|
|
61
|
+
- **用户适配器**:点面板 ➕ 或复制 `adapters.prompt.md` 到任意会话 → LLM 生成 `{ "adapters": [...] }` → 写入 `~/.dsh/quick-toolbar-adapters.json` → 刷新页面生效(host API 校验,非法条目丢弃并报明细)。**推荐直接走 ➕(见教程)**。
|
|
32
62
|
- 定位失败/插件未装/被禁用 → 静默跳过(绝不误伤、绝不误点)。
|
|
33
63
|
- **状态持久化**:位置/钉住/折叠/壳开关走 host(`/quick-toolbar/api/state` → `~/.dsh/quick-toolbar-state.json`,SSiD 开发手册 §7.10 规则)——内核动态端口不再丢状态。
|
|
34
64
|
|
|
65
|
+
## 教程:迁移 / 新增一个按钮(开一个会话,让 LLM 来做)
|
|
66
|
+
|
|
67
|
+
> **核心观念:迁移按钮不是自己写配置,而是开一个会话,把任务交给环境里的 LLM。**
|
|
68
|
+
> 本插件是「载体」,不内置扫描器、不预设所有场景——**适配由你环境里的 LLM 现场完成**
|
|
69
|
+
> (驻场工程师模式);你只需要告诉它要哪个按钮。此设计不是社区主流,但它承诺:
|
|
70
|
+
> 环境里任何按钮都能被聚合,无需第三方插件配合、无需等待本插件适配。
|
|
71
|
+
|
|
72
|
+
**推荐路径(三步,约 1 分钟):**
|
|
73
|
+
|
|
74
|
+
1. **点击工具栏面板里的 ➕「添加按钮」**——插件创建「添加按钮」会话并把注册任务书注入其中(自动执行,无需你打字;服务不可用时退化为输入框草稿,按 Enter 发送)。若环境支持复制,也可直接复制 `adapters.prompt.md` 到任意会话。
|
|
75
|
+
2. **回答 LLM 的反问**——它会先问清楚目标:*迁移*(哪个按钮?在哪个面板/插件里?)
|
|
76
|
+
还是*新增*(图标/文字/点击后发生什么?)。回答后 LLM 才会动手——它不会猜。
|
|
77
|
+
3. **LLM 自主探查并注册**——它用自己的浏览器/代码能力找到目标、验证选择器,
|
|
78
|
+
把最小注册 `{ "adapters": [{ "id": "...", "button": "..." }] }` 写入
|
|
79
|
+
`~/.dsh/quick-toolbar-adapters.json`。你**刷新页面**,按钮即出现在工具栏。
|
|
80
|
+
若未出现,告诉 LLM,它会修正重试。
|
|
81
|
+
|
|
82
|
+
**手动方式(高级用户)**:跳过会话,直接编辑 `~/.dsh/quick-toolbar-adapters.json`
|
|
83
|
+
(同 id 覆盖既有注册;非法条目被 host 校验丢弃并报明细——详见 `adapters.prompt.md` 字段表)。
|
|
84
|
+
|
|
85
|
+
**说明**:注册结果是**纯数据**(图标/文字/点击由载体派生或按声明执行),不是代码——
|
|
86
|
+
LLM 注册完就无需在场;那份 JSON 可导出、可备份、可随环境迁移。
|
|
87
|
+
|
|
88
|
+
**删除按钮**:在工具栏面板里**右键点击该按钮 → 「删除此按钮」**(仅用户注册的按钮;
|
|
89
|
+
内置按钮不可删)。删除后原按钮恢复显示、配置写回 `~/.dsh/quick-toolbar-adapters.json`
|
|
90
|
+
(也可直接手动编辑该 JSON)。**SSiD 壳环境路径**:标题栏「悬浮球」开关(圆圈+圆点)→
|
|
91
|
+
球显示 → 点球展开面板 → 右键按钮 → 滑出「删除」。
|
|
92
|
+
|
|
93
|
+
> **English quick start** — *Moving a button in is NOT a settings task: open a session and let the on-site LLM do it.* Click the **➕ "Add button"** in the toolbar panel; the plugin creates an "Add button" session and injects the registration brief into it (auto-runs; falls back to a composer draft if the session service is unavailable). The LLM will first ask you which button to migrate / what new button you want, then it explores the environment, verifies the selector, writes `{ "adapters": [...] }` to `~/.dsh/quick-toolbar-adapters.json`, and you refresh the page. Registration results are plain data — the carrier derives icon/text/click from the original button, and the LLM is no longer needed afterwards.
|
|
94
|
+
|
|
35
95
|
## 架构
|
|
36
96
|
|
|
37
97
|
- `src/adapters.ts`:适配器 schema + 内置适配器集(黄金示例)
|
|
38
98
|
- `src/behaviors.ts`:行为库(click / toggle-panel / dispatch-event / open-settings(语义锚点+再点关闭)/ command(composer 注入)——全部实现,v2 M1)
|
|
39
99
|
- `src/engine.ts`:执行器(防御执行)
|
|
40
100
|
- `src/schema.ts`:用户配置 zod 校验(LLM 产物防线)
|
|
101
|
+
- `src/register-brief.ts`:➕ 按钮注入的注册任务书(先反问 → 探查 → 最小注册,与协议字段同步,锚点测试防漂移)
|
|
41
102
|
- `src/index.ts`(host 半):`GET /quick-toolbar/api/adapters`(用户适配器)+ `GET/POST /quick-toolbar/api/state`(状态 host 化,原子写)
|
|
42
103
|
- 设计文档:`doc/设计/2026-08-30-quick-toolbar-独立化设计方案.md`、`doc/设计/2026-08-30-quick-toolbar-v2设计.md`
|
|
43
104
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -20,7 +20,10 @@ const BUILTIN_ADAPTERS = [
|
|
|
20
20
|
{
|
|
21
21
|
id: "dsh-better-sidebar.sidebar",
|
|
22
22
|
button: "[class*=\"toggleCluster\"]",
|
|
23
|
-
icon: {
|
|
23
|
+
icon: {
|
|
24
|
+
source: "custom",
|
|
25
|
+
value: "sidebar"
|
|
26
|
+
},
|
|
24
27
|
label: "侧栏",
|
|
25
28
|
act: {
|
|
26
29
|
kind: "dispatch-event",
|
|
@@ -32,7 +35,10 @@ const BUILTIN_ADAPTERS = [
|
|
|
32
35
|
{
|
|
33
36
|
id: "dsh-better-sidebar.bottom",
|
|
34
37
|
button: "[class*=\"toggleCluster\"]",
|
|
35
|
-
icon: {
|
|
38
|
+
icon: {
|
|
39
|
+
source: "custom",
|
|
40
|
+
value: "bottom"
|
|
41
|
+
},
|
|
36
42
|
label: "底栏",
|
|
37
43
|
act: {
|
|
38
44
|
kind: "dispatch-event",
|
|
@@ -75,18 +81,21 @@ function actClick(target) {
|
|
|
75
81
|
return true;
|
|
76
82
|
}
|
|
77
83
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
84
|
+
* 面板开/关(v0.8.0 探测语义——替代已废弃的面板容器协议 findPanel/#id-panel,
|
|
85
|
+
* 该协议从未有环境实现:client 半 findPanel 恒 null,内置项走 toolbarAction 桥接,
|
|
86
|
+
* 用户适配器因此死路——「添加按钮2」会话 LLM 源码实测发现):
|
|
87
|
+
* close 目标**可见** = 弹窗开着 → 点它关闭;**不可见/不存在** = 弹窗关着 → 点原按钮打开。
|
|
88
|
+
* @param openTarget 原按钮(弹窗关着时点击它打开)
|
|
89
|
+
* @param closeTarget 弹窗内关闭按钮(LLM 探测、随弹窗显隐)
|
|
90
|
+
* @param probeVisible 可见性探测(DOM 环境 = rect/computedStyle;测试可 stub)
|
|
80
91
|
*/
|
|
81
|
-
function actTogglePanel(
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
return false;
|
|
92
|
+
function actTogglePanel(openTarget, closeTarget, probeVisible) {
|
|
93
|
+
if (closeTarget !== null && closeTarget !== void 0 && closeTarget.disabled !== true && probeVisible(closeTarget)) {
|
|
94
|
+
closeTarget.click();
|
|
95
|
+
return true;
|
|
88
96
|
}
|
|
89
|
-
|
|
97
|
+
if (openTarget === null || openTarget === void 0 || openTarget.disabled === true) return false;
|
|
98
|
+
openTarget.click();
|
|
90
99
|
return true;
|
|
91
100
|
}
|
|
92
101
|
/** 官方设置触发器锚点链(v2 调研点① 结论:DSH 面板 open 状态由 shell 私有,
|
|
@@ -155,22 +164,60 @@ function actCommand(env, name) {
|
|
|
155
164
|
* 行为库保持窄接口(可测);本层唯一负责 DOM 绑定(browser 环境,
|
|
156
165
|
* 不在 node:test 的范围里——DOM 交互留给 L2 环境验证)。
|
|
157
166
|
*/
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
/** 遮罩关闭候选链(第二次点击的 mask 通道——DSH 弹窗多为遮罩交互) */
|
|
168
|
+
const MASK_CHAINS = [
|
|
169
|
+
"[class$=\"_backdrop\"]",
|
|
170
|
+
"[class*=\"backdrop\"]",
|
|
171
|
+
"[class$=\"_mask\"]",
|
|
172
|
+
"[class*=\"modal-mask\"]"
|
|
173
|
+
];
|
|
174
|
+
/**
|
|
175
|
+
* 归一 secondClick 通道(v0.8.0「二次点击事件」):secondClick 优先,close 旧字段
|
|
176
|
+
* 兼容(字符串简写 = 点击关闭按钮)。
|
|
177
|
+
*/
|
|
178
|
+
function closeTargetOf(act, env) {
|
|
179
|
+
const spec = act.secondClick ?? act.close ?? null;
|
|
180
|
+
if (spec === null) return null;
|
|
181
|
+
if (typeof spec === "string") return env.find(spec);
|
|
182
|
+
if (spec.kind === "mask") {
|
|
183
|
+
const probe = env.isVisible ?? defaultIsVisible;
|
|
184
|
+
for (let i = 0; i < MASK_CHAINS.length; i++) {
|
|
185
|
+
const el = env.find(MASK_CHAINS[i]);
|
|
186
|
+
if (el !== null && probe(el)) return el;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return env.find(spec.selector);
|
|
161
191
|
}
|
|
162
|
-
/**
|
|
192
|
+
/** 缺省可见性探测(DOM:offset 尺寸优先,computedStyle 兜底) */
|
|
193
|
+
function defaultIsVisible(el) {
|
|
194
|
+
if (el === null || typeof el !== "object") return false;
|
|
195
|
+
const node = el;
|
|
196
|
+
if (typeof node.offsetWidth === "number") {
|
|
197
|
+
if (node.offsetWidth > 0 || node.offsetHeight > 0) return true;
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (typeof getComputedStyle === "function") try {
|
|
201
|
+
const style = getComputedStyle(node);
|
|
202
|
+
return style.display !== "none" && style.visibility !== "hidden";
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
/** 按内置定义执行一条适配器(防御执行:定位失败/禁用 → false,绝不误伤)
|
|
209
|
+
* 「换位置」缺省:act 未填 → click(点击原按钮)。 */
|
|
163
210
|
function runAdapter(adapter, env) {
|
|
164
|
-
|
|
211
|
+
const act = adapter.act ?? { kind: "click" };
|
|
212
|
+
switch (act.kind) {
|
|
165
213
|
case "click": return actClick(env.find(adapter.button));
|
|
166
|
-
case "dispatch-event": return env.dispatch(
|
|
214
|
+
case "dispatch-event": return env.dispatch(act.event, act.detail);
|
|
167
215
|
case "toggle-panel": {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
return false;
|
|
216
|
+
const closeEl = closeTargetOf(act, env);
|
|
217
|
+
return actTogglePanel(env.find(adapter.button), closeEl, env.isVisible ?? defaultIsVisible);
|
|
171
218
|
}
|
|
172
219
|
case "open-settings":
|
|
173
|
-
if (
|
|
220
|
+
if (act.path !== void 0) console.warn(`quick-toolbar: open-settings path '${act.path}' 暂不支持(v2 深链待入)`);
|
|
174
221
|
return actOpenSettings({
|
|
175
222
|
find: env.find,
|
|
176
223
|
...env.findByText !== void 0 ? { findByText: env.findByText } : {}
|
|
@@ -180,10 +227,116 @@ function runAdapter(adapter, env) {
|
|
|
180
227
|
console.warn("quick-toolbar: command 环境无 runCommand 通道(旧版 DSH/未注入)");
|
|
181
228
|
return false;
|
|
182
229
|
}
|
|
183
|
-
return actCommand({ execute: env.runCommand },
|
|
230
|
+
return actCommand({ execute: env.runCommand }, act.name);
|
|
184
231
|
}
|
|
185
232
|
}
|
|
186
233
|
//#endregion
|
|
234
|
+
//#region src/register-brief.ts
|
|
235
|
+
/**
|
|
236
|
+
* 注册任务书(v0.5.1)——「➕ 按钮」注入的 prompt 文本。
|
|
237
|
+
*
|
|
238
|
+
* 设计(V2-9 载体哲学 + 安装教程闭环):
|
|
239
|
+
* 用户点击工具栏「➕」→ 首选经 sessions/workspaces/uiWorkspace 服务创建
|
|
240
|
+
* 「添加按钮」会话并把本任务书以 queue 模式注入(自动执行)——同款机制 =
|
|
241
|
+
* 插件中心 LLM 更新(ensureLlmUpdateSession,2026-08-30 实证修正:早期
|
|
242
|
+
* inject=[] 令 apply 只见 {fiber},「无会话通道」判断是错结论);
|
|
243
|
+
* 服务缺失/失败 → 降级为当前 composer 草稿注入(用户 Enter 发送)。
|
|
244
|
+
* 任务书必须自包含(环境 LLM 不一定能访问本仓库文件)——协议要点全部内嵌。
|
|
245
|
+
*
|
|
246
|
+
* 同步纪律:字段与 src/adapters.ts / src/schema.ts 保持一致;
|
|
247
|
+
* 漂移即教程失效(tests/register-brief.spec.ts 有锚点断言防退化)。
|
|
248
|
+
*/
|
|
249
|
+
const REGISTER_BRIEF = `# 任务:为「工具栏插件」迁移 / 新增一个按钮
|
|
250
|
+
|
|
251
|
+
你是 DSH 环境的驻场工程师。用户想把某个按钮**迁移**到工具栏
|
|
252
|
+
(@max-null/dsh-quick-toolbar:SSiD 标题栏按钮组 / web 悬浮球·聚合面板),
|
|
253
|
+
或者**新增**一个自定义按钮。工具栏是「载体」——你注册的是**数据**
|
|
254
|
+
(图标/文字/点击由载体从原按钮派生或按声明执行),不是代码。
|
|
255
|
+
|
|
256
|
+
## 第一步(现在就要做):先反问用户,不要猜
|
|
257
|
+
|
|
258
|
+
用一句话向用户确认目标,等回答后再动手:
|
|
259
|
+
|
|
260
|
+
- **迁移**:要迁移的是哪个按钮?它在哪个面板/页面/插件里,长什么样?
|
|
261
|
+
- **新增**:要新增什么样的按钮?图标 / 文字 / 点击后应发生什么?
|
|
262
|
+
|
|
263
|
+
## 第二步(确认目标后):自主探查并验证
|
|
264
|
+
|
|
265
|
+
- **探查通道**:经 ➕ 注入的本任务书开头附有「探查通道」段(当前 DSH 页面完整
|
|
266
|
+
URL,含认证 token)——用浏览器直接打开它即可访问用户环境;**不要裸访问
|
|
267
|
+
内核端口(会 401)**。手工粘贴本文件时若无附带 URL,向用户要页面地址。
|
|
268
|
+
- 用你的浏览器 / 代码能力找到目标按钮元素(DOM 查询、截图、读源码都行),
|
|
269
|
+
**自己验证 CSS 选择器确实可用**——不要凭猜测写选择器,不要依赖用户给 DOM 快照。
|
|
270
|
+
- 提示:场景无限,没有"完美脚本"能预适配所有环境——探查正是你在场的原因,
|
|
271
|
+
载体不内置扫描器,这个活由你做。
|
|
272
|
+
|
|
273
|
+
## 第三步:注册(数据,不是代码)
|
|
274
|
+
|
|
275
|
+
**最小注册(迁移——推荐起点)**:被整合的按钮自带图标/文字/点击事件,聚合就是
|
|
276
|
+
"换个位置",两条就够了:
|
|
277
|
+
|
|
278
|
+
\`\`\`json
|
|
279
|
+
{ "id": "dsh-some-plugin", "button": ".some-btn" }
|
|
280
|
+
\`\`\`
|
|
281
|
+
|
|
282
|
+
\`\`\`json
|
|
283
|
+
{ "adapters": [ { "id": "dsh-some-plugin", "button": ".some-btn" } ] }
|
|
284
|
+
\`\`\`
|
|
285
|
+
|
|
286
|
+
- 图标 = 扣取原按钮视觉(svg/img/背景);文字 = 扣取原按钮文案;
|
|
287
|
+
点击 = 点击原按钮(act 缺省 \`click\`)。
|
|
288
|
+
- **原按钮自动隐藏**(换位置 = 双入口无意义;缺省隐藏,显式 \`"hide": false\` 才保留——
|
|
289
|
+
若原按钮被隐藏后其弹窗定位错位,改用 \`hide: false\`)。
|
|
290
|
+
- **自定义按钮**(无原按钮可扣)才需要显式字段:
|
|
291
|
+
\`\`\`json
|
|
292
|
+
{ "id": "my-btn", "button": "#app-shell", "icon": { "source": "custom", "value": "emoji 或 svg" },
|
|
293
|
+
"label": "文字", "act": { "kind": "click" }, "hide": false }
|
|
294
|
+
\`\`\`
|
|
295
|
+
|
|
296
|
+
**行为枚举(只能选这些,禁止写代码)**:
|
|
297
|
+
|
|
298
|
+
| kind | 语义 |
|
|
299
|
+
|---|---|
|
|
300
|
+
| \`click\` | 直接点击 |
|
|
301
|
+
| \`toggle-panel\` | **二次点击事件**:弹窗开 → 按 \`secondClick\` 通道关闭;关 → 点原按钮打开。\`secondClick\` 推荐 \`{ "kind": "mask" }\`(点遮罩关闭——DSH 弹窗通用交互);或点关闭按钮 \`{ "kind": "click", "selector": "验证过的关闭按钮" }\`(也可直接写选择器字符串) |
|
|
302
|
+
| \`dispatch-event\` | 派发 CustomEvent(\`event\`、\`detail?\`) |
|
|
303
|
+
| \`open-settings\` | 打开设置面板(引擎语义锚点定位,再点关闭) |
|
|
304
|
+
| \`command\` | 向输入框注入斜杠命令草稿(\`name\`,不含 \`/\`) |
|
|
305
|
+
|
|
306
|
+
## 探测闭环(注册前必须完成——别让用户点出"开得关不掉")
|
|
307
|
+
|
|
308
|
+
用你的浏览器能力实测原按钮行为,按结果选 act:
|
|
309
|
+
|
|
310
|
+
1. 点击一次:开的是**弹窗/面板**还是普通页面?
|
|
311
|
+
2. **用 JS \`.click()\` 验证 toggle 性**(DOM 事件触发、穿透遮罩)——Playwright 物理点击
|
|
312
|
+
可能被弹窗遮罩(backdrop)**拦截而误判**"不能再点关闭"(remote 案例实测:物理点不到
|
|
313
|
+
≠ 不能 toggle,JS 点击下原按钮可能是天然开关):
|
|
314
|
+
- \`document.querySelector(原按钮选择器).click()\` 连点两次:开→关 → **天然 toggle** → \`click\` 就够;
|
|
315
|
+
- 仍开→关不掉 → act 用 \`toggle-panel\` 并探测关闭通道(下一步)。
|
|
316
|
+
3. \`toggle-panel\` 关闭通道:**优先 \`secondClick: { "kind": "mask" }\`(点遮罩)——
|
|
317
|
+
但必须探测验证遮罩点击真的会关闭**(remote 弹窗遮罩实测不响应);不行换
|
|
318
|
+
\`{ "kind": "click", "selector": "弹窗内唯一×按钮选择器" }\`。
|
|
319
|
+
4. **\`close\` 旧字段兼容**:\`"close": "关闭按钮选择器"\`(字符串简写,v0.8.0 前写法)
|
|
320
|
+
与 secondClick 等效,schema 仍接受——旧任务书/注册可读,不必改写。
|
|
321
|
+
5. 记录原按钮是否会在意「隐藏」(见第三步 hide 说明)。
|
|
322
|
+
|
|
323
|
+
## 第四步:写入配置,让用户刷新
|
|
324
|
+
|
|
325
|
+
把完整 JSON(含 \`adapters\` 数组)写入:
|
|
326
|
+
\`~/.dsh/quick-toolbar-adapters.json\`(DSH 环境即 \`$DSH_HOME/quick-toolbar-adapters.json\`;
|
|
327
|
+
重复 \`id\` = 覆盖既有注册)。写入后告知用户**刷新页面**,按钮即出现在工具栏;
|
|
328
|
+
若未出现,修正选择器后重试。协议全文见仓库 \`adapters.prompt.md\`(如可访问请先读一遍)。
|
|
329
|
+
|
|
330
|
+
## 注册后验证清单(刷新后逐项确认)
|
|
331
|
+
|
|
332
|
+
① 按钮已出现在工具栏(图标/文字来自原按钮) ② 点击可开启 ③ 再点能关闭
|
|
333
|
+
(toggle-panel 场景) ④ 原按钮已隐藏(或按 hide:false 保留)。任一项不满足 → 修正后重试。
|
|
334
|
+
|
|
335
|
+
## 纪律
|
|
336
|
+
|
|
337
|
+
- 只写数据,不提供函数/代码(schema 拒绝一切非枚举行为)。
|
|
338
|
+
- 一次只注册 1–2 个按钮,验证后再扩展;定位失败无害(引擎静默跳过,绝不误点)。`;
|
|
339
|
+
//#endregion
|
|
187
340
|
//#region src/client.ts
|
|
188
341
|
/**
|
|
189
342
|
* @max-null/dsh-quick-toolbar — browser half.
|
|
@@ -292,6 +445,8 @@ window.__ModuleLoader__.load({
|
|
|
292
445
|
"tb.sidebar": ["侧栏", "Sidebar"],
|
|
293
446
|
"tb.bottom": ["底栏", "Bottom panel"],
|
|
294
447
|
"tb.sessions": ["会话管理", "Sessions"],
|
|
448
|
+
"tb.add": ["添加按钮", "Add button"],
|
|
449
|
+
"tb.addAria": ["添加/迁移按钮(让 LLM 来注册)", "Add / migrate a button (let the LLM register it)"],
|
|
295
450
|
"sm.open": ["会话管理", "Sessions"],
|
|
296
451
|
"sm.openTitle": ["打开会话管理面板", "Open session manager"]
|
|
297
452
|
};
|
|
@@ -438,19 +593,28 @@ window.__ModuleLoader__.load({
|
|
|
438
593
|
"#ssid-toolbar .ssid-tb-pin svg{width:15px;height:15px}",
|
|
439
594
|
"#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}",
|
|
440
595
|
"#ssid-toolbar .ssid-tb-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,148,168,.14))}",
|
|
441
|
-
"#ssid-toolbar .ssid-tb-btn svg{flex:none;width:15px;height:15px;color:var(--dsw-alias-label-secondary,#98a2b3)}"
|
|
596
|
+
"#ssid-toolbar .ssid-tb-btn svg{flex:none;width:15px;height:15px;color:var(--dsw-alias-label-secondary,#98a2b3)}",
|
|
597
|
+
"#ssid-toolbar .ssid-tb-add{border:1px dashed var(--dsw-alias-border-strong,rgba(128,148,168,.45));background:transparent;color:var(--dsw-alias-label-tertiary,#7b8494);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;margin-top:2px;transition:color .15s,border-color .15s,background .15s}",
|
|
598
|
+
"#ssid-toolbar .ssid-tb-add:hover{color:var(--dsw-alias-label-primary,#d8e0ea);border-color:var(--dsw-alias-label-secondary,#98a2b3);background:var(--dsw-alias-interactive-bg-hover,rgba(128,148,168,.14))}",
|
|
599
|
+
"#ssid-toolbar .ssid-tb-add svg{flex:none;width:15px;height:15px;color:var(--dsw-alias-label-secondary,#98a2b3)}",
|
|
600
|
+
".ssid-tb-row{position:relative;overflow:hidden;border-radius:8px}",
|
|
601
|
+
".ssid-tb-slide{display:flex;position:relative;width:100%;transition:transform .2s ease}",
|
|
602
|
+
".ssid-tb-row .ssid-tb-btn{flex:1 1 auto;min-width:0;position:relative;z-index:1;box-sizing:border-box;background:transparent}",
|
|
603
|
+
".ssid-tb-row.ssid-tb-row-open .ssid-tb-slide{transform:translateX(-56px)}",
|
|
604
|
+
".ssid-tb-del{position:absolute;right:-56px;top:0;bottom:0;width:56px;border:0;background:var(--dsw-alias-state-error-primary,#e5484d);color:#fff;font-size:12px;cursor:pointer;font-weight:500;border-radius:8px 0 0 8px}"
|
|
442
605
|
].join("\n");
|
|
443
606
|
function toolbarIcon(name) {
|
|
444
607
|
var ICONS = {
|
|
445
608
|
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>",
|
|
446
609
|
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>",
|
|
447
|
-
sidebar: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"
|
|
448
|
-
bottom: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"
|
|
610
|
+
sidebar: "<svg viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"1.5\" y=\"2\" width=\"13\" height=\"12\" rx=\"2.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/><rect x=\"10.5\" y=\"3.25\" width=\"2.75\" height=\"9.5\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/></svg>",
|
|
611
|
+
bottom: "<svg viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><rect x=\"1.5\" y=\"2\" width=\"13\" height=\"12\" rx=\"2.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/><rect x=\"3.25\" y=\"10\" width=\"9.5\" height=\"2.75\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/></svg>",
|
|
449
612
|
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>",
|
|
450
613
|
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>",
|
|
451
614
|
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>",
|
|
452
615
|
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>"
|
|
616
|
+
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>",
|
|
617
|
+
add: "<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M8 3.5v9M3.5 8h9\"/></svg>"
|
|
454
618
|
};
|
|
455
619
|
return ICONS[name] || ICONS.grid;
|
|
456
620
|
}
|
|
@@ -459,8 +623,20 @@ window.__ModuleLoader__.load({
|
|
|
459
623
|
find: function(s) {
|
|
460
624
|
return document.querySelector(s);
|
|
461
625
|
},
|
|
462
|
-
|
|
463
|
-
return
|
|
626
|
+
isVisible: function(el) {
|
|
627
|
+
if (el === null || typeof el !== "object") return false;
|
|
628
|
+
var node = el;
|
|
629
|
+
if (typeof node.getBoundingClientRect === "function") {
|
|
630
|
+
var r = node.getBoundingClientRect();
|
|
631
|
+
if (r.width > 0 || r.height > 0) return true;
|
|
632
|
+
}
|
|
633
|
+
if (typeof getComputedStyle === "function") try {
|
|
634
|
+
var s = getComputedStyle(node);
|
|
635
|
+
return s.display !== "none" && s.visibility !== "hidden";
|
|
636
|
+
} catch (_e) {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
return false;
|
|
464
640
|
},
|
|
465
641
|
dispatch: function(event, detail) {
|
|
466
642
|
window.dispatchEvent(new CustomEvent(event, { detail }));
|
|
@@ -479,11 +655,10 @@ window.__ModuleLoader__.load({
|
|
|
479
655
|
}
|
|
480
656
|
};
|
|
481
657
|
};
|
|
482
|
-
function
|
|
658
|
+
function injectComposerDraft(text) {
|
|
483
659
|
var seat = document.querySelector("[data-composer-seat]");
|
|
484
660
|
var el = seat !== null && seat !== void 0 ? seat.querySelector("textarea, [contenteditable=\"true\"]") : document.querySelector("textarea, [contenteditable=\"true\"]");
|
|
485
661
|
if (el === null || el === void 0) return false;
|
|
486
|
-
var text = "/" + name;
|
|
487
662
|
if (el instanceof HTMLTextAreaElement) {
|
|
488
663
|
var setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
|
|
489
664
|
if (setter !== void 0 && setter.set !== void 0) setter.set.call(el, text);
|
|
@@ -496,14 +671,11 @@ window.__ModuleLoader__.load({
|
|
|
496
671
|
data: text
|
|
497
672
|
}));
|
|
498
673
|
el.focus();
|
|
499
|
-
el.dispatchEvent(new KeyboardEvent("keydown", {
|
|
500
|
-
key: "Enter",
|
|
501
|
-
code: "Enter",
|
|
502
|
-
bubbles: true,
|
|
503
|
-
cancelable: true
|
|
504
|
-
}));
|
|
505
674
|
return true;
|
|
506
675
|
}
|
|
676
|
+
function typeCommandIntoComposer(name) {
|
|
677
|
+
return injectComposerDraft("/" + name);
|
|
678
|
+
}
|
|
507
679
|
function toolbarAction(kind) {
|
|
508
680
|
if (kind === "plugin") {
|
|
509
681
|
closeSidePanelsBeforePluginCenter();
|
|
@@ -531,6 +703,7 @@ window.__ModuleLoader__.load({
|
|
|
531
703
|
function createToolbar() {
|
|
532
704
|
if (win.__SSID_SHELL__ === true && !qtState.shellVisible) return;
|
|
533
705
|
if (document.getElementById(TOOLBAR_ID) !== null) return;
|
|
706
|
+
var addBtn = null;
|
|
534
707
|
var root = document.createElement("div");
|
|
535
708
|
root.id = TOOLBAR_ID;
|
|
536
709
|
var panel = document.createElement("div");
|
|
@@ -554,13 +727,32 @@ window.__ModuleLoader__.load({
|
|
|
554
727
|
"dsh-session-manager": "sessions"
|
|
555
728
|
};
|
|
556
729
|
var adapterIconHtml = function(adapter, kind) {
|
|
557
|
-
if (adapter.icon.source === "custom") {
|
|
730
|
+
if (adapter.icon !== void 0 && adapter.icon.source === "custom") {
|
|
558
731
|
var value = adapter.icon.value;
|
|
559
732
|
if (value.indexOf("<") === 0) return value;
|
|
560
733
|
return toolbarIcon(value);
|
|
561
734
|
}
|
|
735
|
+
try {
|
|
736
|
+
var el = document.querySelector(adapter.button);
|
|
737
|
+
if (el !== null) {
|
|
738
|
+
var svg = el.querySelector("svg");
|
|
739
|
+
if (svg !== null) return svg.outerHTML;
|
|
740
|
+
var img = el.querySelector("img");
|
|
741
|
+
if (img !== null) return img.outerHTML;
|
|
742
|
+
var bg = getComputedStyle(el).backgroundImage;
|
|
743
|
+
if (bg !== null && bg !== "none") return "<span style=\"display:inline-block;width:15px;height:15px;background:" + bg + ";background-size:contain;background-repeat:no-repeat\"></span>";
|
|
744
|
+
}
|
|
745
|
+
} catch (_e) {}
|
|
562
746
|
return kind !== null ? toolbarIcon(kind) : toolbarIcon("grid");
|
|
563
747
|
};
|
|
748
|
+
var adapterLabel = function(adapter) {
|
|
749
|
+
if (adapter.label !== void 0 && adapter.label !== "") return adapter.label;
|
|
750
|
+
try {
|
|
751
|
+
var el = document.querySelector(adapter.button);
|
|
752
|
+
if (el !== null && (el.textContent || "").trim() !== "") return (el.textContent || "").trim().slice(0, 12);
|
|
753
|
+
} catch (_e) {}
|
|
754
|
+
return adapter.id;
|
|
755
|
+
};
|
|
564
756
|
var renderButton = function(adapter, kind) {
|
|
565
757
|
var b = document.createElement("button");
|
|
566
758
|
b.type = "button";
|
|
@@ -574,7 +766,7 @@ window.__ModuleLoader__.load({
|
|
|
574
766
|
trackLocale(b, "tb." + kind, "aria");
|
|
575
767
|
trackLocale(b, "tb." + kind, "title");
|
|
576
768
|
} else {
|
|
577
|
-
var label = adapter
|
|
769
|
+
var label = adapterLabel(adapter);
|
|
578
770
|
var textSpan = b.querySelector("span");
|
|
579
771
|
if (textSpan !== null) textSpan.textContent = label;
|
|
580
772
|
b.setAttribute("aria-label", label);
|
|
@@ -587,13 +779,91 @@ window.__ModuleLoader__.load({
|
|
|
587
779
|
}
|
|
588
780
|
runAdapter(adapter, toolbarEnv());
|
|
589
781
|
});
|
|
590
|
-
panel.
|
|
782
|
+
if (addBtn !== null && addBtn.parentNode === panel) panel.insertBefore(b, addBtn);
|
|
783
|
+
else panel.appendChild(b);
|
|
784
|
+
if (adapter.hide !== false) try {
|
|
785
|
+
var origBtn = document.querySelector(adapter.button);
|
|
786
|
+
if (origBtn !== null) origBtn.style.display = "none";
|
|
787
|
+
} catch (_e) {}
|
|
788
|
+
return b;
|
|
591
789
|
};
|
|
592
790
|
for (var ai = 0; ai < BUILTIN_ADAPTERS.length; ai++) {
|
|
593
791
|
var adapter = BUILTIN_ADAPTERS[ai];
|
|
594
792
|
var kind = TOOLBAR_KIND_BY_ADAPTER[adapter.id];
|
|
595
793
|
renderButton(adapter, kind !== void 0 ? kind : null);
|
|
596
794
|
}
|
|
795
|
+
var slideCloseAll = function() {
|
|
796
|
+
var rows = panel.querySelectorAll(".ssid-tb-row.ssid-tb-row-open");
|
|
797
|
+
for (var ri = 0; ri < rows.length; ri++) rows[ri].classList.remove("ssid-tb-row-open");
|
|
798
|
+
};
|
|
799
|
+
document.addEventListener("mousedown", function(e) {
|
|
800
|
+
var t = e.target;
|
|
801
|
+
if (t === null) return;
|
|
802
|
+
if (!(t instanceof Element ? t.closest(".ssid-tb-row") !== null : false)) slideCloseAll();
|
|
803
|
+
});
|
|
804
|
+
function removeUserAdapter(ad) {
|
|
805
|
+
fetch("/quick-toolbar/api/adapters").then(function(r) {
|
|
806
|
+
return r.json();
|
|
807
|
+
}).then(function(data) {
|
|
808
|
+
var envelope = data !== null && typeof data === "object" ? data : void 0;
|
|
809
|
+
var rows = envelope !== void 0 && envelope.ok === true && envelope.value !== null && typeof envelope.value === "object" ? envelope.value.adapters : void 0;
|
|
810
|
+
if (!Array.isArray(rows)) return;
|
|
811
|
+
var next = rows.filter(function(a) {
|
|
812
|
+
return a.id !== ad.id;
|
|
813
|
+
});
|
|
814
|
+
return fetch("/quick-toolbar/api/adapters", {
|
|
815
|
+
method: "POST",
|
|
816
|
+
headers: { "Content-Type": "application/json" },
|
|
817
|
+
body: JSON.stringify({ adapters: next })
|
|
818
|
+
}).then(function(r) {
|
|
819
|
+
return r.json();
|
|
820
|
+
}).then(function(res) {
|
|
821
|
+
if (res === null || typeof res !== "object" || res.ok !== true) return;
|
|
822
|
+
try {
|
|
823
|
+
var btn = panel.querySelector("[data-adapter-id=\"" + ad.id.replace(/"/g, "\\\"") + "\"]");
|
|
824
|
+
var row = btn !== null && typeof btn.closest === "function" ? btn.closest(".ssid-tb-row") : null;
|
|
825
|
+
if (row !== null && row.parentNode === panel) panel.removeChild(row);
|
|
826
|
+
} catch (_e) {}
|
|
827
|
+
try {
|
|
828
|
+
var orig = document.querySelector(ad.button);
|
|
829
|
+
if (orig !== null) orig.style.display = "";
|
|
830
|
+
} catch (_e2) {}
|
|
831
|
+
});
|
|
832
|
+
}).catch(function() {});
|
|
833
|
+
}
|
|
834
|
+
function wrapAdapterRow(btn, ad) {
|
|
835
|
+
var row = document.createElement("div");
|
|
836
|
+
row.className = "ssid-tb-row";
|
|
837
|
+
var slide = document.createElement("div");
|
|
838
|
+
slide.className = "ssid-tb-slide";
|
|
839
|
+
if (btn.parentNode !== null) btn.parentNode.removeChild(btn);
|
|
840
|
+
slide.appendChild(btn);
|
|
841
|
+
var delBtn = document.createElement("button");
|
|
842
|
+
delBtn.type = "button";
|
|
843
|
+
delBtn.className = "ssid-tb-del";
|
|
844
|
+
delBtn.textContent = "删除";
|
|
845
|
+
delBtn.addEventListener("click", function() {
|
|
846
|
+
row.classList.remove("ssid-tb-row-open");
|
|
847
|
+
setTimeout(function() {
|
|
848
|
+
removeUserAdapter(ad);
|
|
849
|
+
}, 220);
|
|
850
|
+
});
|
|
851
|
+
slide.appendChild(delBtn);
|
|
852
|
+
row.appendChild(slide);
|
|
853
|
+
panel.insertBefore(row, addBtn);
|
|
854
|
+
btn.setAttribute("data-user-adapter", "1");
|
|
855
|
+
row.addEventListener("contextmenu", function(e) {
|
|
856
|
+
e.preventDefault();
|
|
857
|
+
e.stopPropagation();
|
|
858
|
+
if (row.classList.contains("ssid-tb-row-open")) {
|
|
859
|
+
row.classList.remove("ssid-tb-row-open");
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
slideCloseAll();
|
|
863
|
+
row.classList.add("ssid-tb-row-open");
|
|
864
|
+
});
|
|
865
|
+
return row;
|
|
866
|
+
}
|
|
597
867
|
var fetchUserAdapters = function() {
|
|
598
868
|
fetch("/quick-toolbar/api/adapters").then(function(r) {
|
|
599
869
|
return r.json();
|
|
@@ -607,12 +877,33 @@ window.__ModuleLoader__.load({
|
|
|
607
877
|
var userKind = TOOLBAR_KIND_BY_ADAPTER[user.id];
|
|
608
878
|
try {
|
|
609
879
|
var old = panel.querySelector("[data-adapter-id=\"" + user.id.replace(/"/g, "\\\"") + "\"]");
|
|
610
|
-
|
|
880
|
+
var oldRow = old !== null && typeof old.closest === "function" ? old.closest(".ssid-tb-row") : null;
|
|
881
|
+
if (oldRow !== null && oldRow.parentNode === panel) panel.removeChild(oldRow);
|
|
882
|
+
else if (old !== null && old.parentNode === panel) panel.removeChild(old);
|
|
611
883
|
} catch (_e) {}
|
|
612
|
-
renderButton(user, userKind !== void 0 ? userKind : null);
|
|
884
|
+
wrapAdapterRow(renderButton(user, userKind !== void 0 ? userKind : null), user);
|
|
613
885
|
}
|
|
614
886
|
}).catch(function() {});
|
|
615
887
|
};
|
|
888
|
+
addBtn = document.createElement("button");
|
|
889
|
+
addBtn.type = "button";
|
|
890
|
+
addBtn.className = "ssid-tb-add";
|
|
891
|
+
addBtn.setAttribute("aria-label", "添加/迁移按钮");
|
|
892
|
+
addBtn.title = "添加按钮";
|
|
893
|
+
addBtn.innerHTML = toolbarIcon("add") + "<span></span>";
|
|
894
|
+
trackLocale(addBtn, "tb.addAria", "aria");
|
|
895
|
+
trackLocale(addBtn, "tb.add", "title");
|
|
896
|
+
var addSpan = addBtn.querySelector("span");
|
|
897
|
+
if (addSpan !== null) addSpan.textContent = "添加按钮";
|
|
898
|
+
trackLocale(addBtn, "tb.add", "text-span");
|
|
899
|
+
addBtn.addEventListener("click", function() {
|
|
900
|
+
ensureRegisterSession().then(function(ok) {
|
|
901
|
+
if (!ok) registerBriefWithSessionUrl().then(function(brief) {
|
|
902
|
+
injectComposerDraft(brief);
|
|
903
|
+
});
|
|
904
|
+
});
|
|
905
|
+
});
|
|
906
|
+
panel.appendChild(addBtn);
|
|
616
907
|
var ball = document.createElement("button");
|
|
617
908
|
ball.type = "button";
|
|
618
909
|
ball.id = TOOLBAR_ID + "-ball";
|
|
@@ -835,10 +1126,71 @@ window.__ModuleLoader__.load({
|
|
|
835
1126
|
document.addEventListener("mouseup", onUp);
|
|
836
1127
|
});
|
|
837
1128
|
}
|
|
838
|
-
exports.inject = [
|
|
1129
|
+
exports.inject = [
|
|
1130
|
+
"sessions",
|
|
1131
|
+
"workspaces",
|
|
1132
|
+
"uiWorkspace"
|
|
1133
|
+
];
|
|
1134
|
+
var sessionsSvc = null;
|
|
1135
|
+
var workspacesSvc = null;
|
|
1136
|
+
var uiWorkspaceSvc = null;
|
|
1137
|
+
function buildRegisterBrief(url) {
|
|
1138
|
+
return "## 探查通道(已为你的浏览器认证好):\n- 用浏览器打开下面这个 URL 即可直达当前 DSH 页面(首次访问会换 cookie 并自动重定向到干净根路径;token 仅本会话使用,勿写入文件/回复):\n" + url + "\n\n" + REGISTER_BRIEF;
|
|
1139
|
+
}
|
|
1140
|
+
function registerBriefWithSessionUrl() {
|
|
1141
|
+
return fetch("/quick-toolbar/api/auth-url").then(function(r) {
|
|
1142
|
+
return r.json();
|
|
1143
|
+
}).then(function(d) {
|
|
1144
|
+
return buildRegisterBrief(d !== null && typeof d === "object" && d.ok === true && typeof d.url === "string" ? d.url : "");
|
|
1145
|
+
}).catch(function() {
|
|
1146
|
+
return buildRegisterBrief("");
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
function ensureRegisterSession() {
|
|
1150
|
+
var svc = sessionsSvc;
|
|
1151
|
+
var wsvc = workspacesSvc;
|
|
1152
|
+
var uws = uiWorkspaceSvc;
|
|
1153
|
+
if (svc === null || wsvc === null || uws === null) return Promise.resolve(false);
|
|
1154
|
+
if (typeof uws.connectWorkspace !== "function") return Promise.resolve(false);
|
|
1155
|
+
var snapshot = wsvc.list !== void 0 && typeof wsvc.list.getSnapshot === "function" ? wsvc.list.getSnapshot() : void 0;
|
|
1156
|
+
var items = snapshot !== void 0 && snapshot !== null && snapshot.items !== void 0 && snapshot.items !== null ? snapshot.items : [];
|
|
1157
|
+
var wsId = items.length > 0 ? items[0].workspaceId : void 0;
|
|
1158
|
+
if (wsId === void 0 || wsId === "") return Promise.resolve(false);
|
|
1159
|
+
return uws.connectWorkspace(wsId).then(function(sid) {
|
|
1160
|
+
if (typeof sid !== "string" || sid === "") return false;
|
|
1161
|
+
var svc3 = svc;
|
|
1162
|
+
if (svc3 === null || svc3 === void 0) return false;
|
|
1163
|
+
var face = typeof svc3.binding === "function" ? svc3.binding(sid) : void 0;
|
|
1164
|
+
var s = face !== void 0 && face !== null && face.session !== void 0 && face.session !== null ? face.session : void 0;
|
|
1165
|
+
if (s === void 0 || s === null || typeof s.prompt !== "function") return false;
|
|
1166
|
+
return registerBriefWithSessionUrl().then(function(brief) {
|
|
1167
|
+
var sess2 = s;
|
|
1168
|
+
if (sess2 === void 0 || sess2 === null || typeof sess2.prompt !== "function") return Promise.resolve({ ok: false });
|
|
1169
|
+
return sess2.prompt([{
|
|
1170
|
+
type: "text",
|
|
1171
|
+
text: brief
|
|
1172
|
+
}], "queue");
|
|
1173
|
+
}).then(function(res) {
|
|
1174
|
+
if (res === void 0 || res === null || res.ok !== true) return false;
|
|
1175
|
+
var sess = s;
|
|
1176
|
+
var svc2 = svc;
|
|
1177
|
+
if (sess === void 0 || sess === null) return false;
|
|
1178
|
+
if (svc2 === null || svc2 === void 0) return false;
|
|
1179
|
+
if (typeof sess.rename === "function") sess.rename("添加按钮").catch(function() {});
|
|
1180
|
+
if (typeof svc2.open === "function") svc2.open(sid);
|
|
1181
|
+
return true;
|
|
1182
|
+
});
|
|
1183
|
+
}).catch(function() {
|
|
1184
|
+
return false;
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
839
1187
|
exports.apply = function(ctx) {
|
|
840
1188
|
if (win.__dshQuickToolbarInstalled === true) return;
|
|
841
1189
|
win.__dshQuickToolbarInstalled = true;
|
|
1190
|
+
var svcCtx = ctx !== null && typeof ctx === "object" ? ctx : {};
|
|
1191
|
+
sessionsSvc = svcCtx.sessions ?? null;
|
|
1192
|
+
workspacesSvc = svcCtx.workspaces ?? null;
|
|
1193
|
+
uiWorkspaceSvc = svcCtx.uiWorkspace ?? null;
|
|
842
1194
|
var style = document.createElement("style");
|
|
843
1195
|
style.setAttribute("data-dsh-quick-toolbar", "");
|
|
844
1196
|
style.textContent = BASE_CSS + (SHELL_CSS.length > 0 && win.__SSID_SHELL__ === true ? "\n" + SHELL_CSS.join("\n") : "");
|
package/lib/index.js
CHANGED
|
@@ -16,12 +16,20 @@ const adapterSchema = z.object({
|
|
|
16
16
|
icon: z.discriminatedUnion("source", [z.object({ source: z.literal("from-button") }), z.object({
|
|
17
17
|
source: z.literal("custom"),
|
|
18
18
|
value: z.string().min(1)
|
|
19
|
-
})]),
|
|
19
|
+
})]).optional(),
|
|
20
20
|
label: z.string().optional(),
|
|
21
21
|
act: z.discriminatedUnion("kind", [
|
|
22
22
|
z.object({ kind: z.literal("click") }),
|
|
23
23
|
z.object({
|
|
24
24
|
kind: z.literal("toggle-panel"),
|
|
25
|
+
secondClick: z.union([
|
|
26
|
+
z.string().min(1),
|
|
27
|
+
z.object({ kind: z.literal("mask") }),
|
|
28
|
+
z.object({
|
|
29
|
+
kind: z.literal("click"),
|
|
30
|
+
selector: z.string().min(1)
|
|
31
|
+
})
|
|
32
|
+
]).optional(),
|
|
25
33
|
close: z.string().optional()
|
|
26
34
|
}),
|
|
27
35
|
z.object({
|
|
@@ -37,7 +45,7 @@ const adapterSchema = z.object({
|
|
|
37
45
|
kind: z.literal("command"),
|
|
38
46
|
name: z.string()
|
|
39
47
|
})
|
|
40
|
-
]),
|
|
48
|
+
]).optional(),
|
|
41
49
|
hide: z.boolean().optional(),
|
|
42
50
|
enabled: z.boolean().optional()
|
|
43
51
|
});
|
|
@@ -85,33 +93,66 @@ const adaptersRouteDefinition = {
|
|
|
85
93
|
kind: "exact",
|
|
86
94
|
path: ROUTE_PATH,
|
|
87
95
|
handler: async (req, res) => {
|
|
88
|
-
if (req.method
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
if (req.method === "GET") {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = parseUserAdapters(readFileSync(ADAPTERS_PATH, "utf8"));
|
|
99
|
+
if (!parsed.ok) {
|
|
100
|
+
sendJson(res, 200, {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: "invalid-schema",
|
|
103
|
+
detail: parsed.issues
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
sendJson(res, 200, {
|
|
108
|
+
ok: true,
|
|
109
|
+
value: parsed.value
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
sendJson(res, 200, {
|
|
113
|
+
ok: true,
|
|
114
|
+
value: { adapters: [] }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
93
117
|
return;
|
|
94
118
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
119
|
+
if (req.method === "POST") {
|
|
120
|
+
let raw = "";
|
|
121
|
+
req.on?.("data", (chunk) => {
|
|
122
|
+
raw += chunk;
|
|
123
|
+
});
|
|
124
|
+
await new Promise((resolve) => req.on?.("end", () => {
|
|
125
|
+
resolve();
|
|
126
|
+
}));
|
|
127
|
+
try {
|
|
128
|
+
const parsed = parseUserAdapters(raw);
|
|
129
|
+
if (!parsed.ok) {
|
|
130
|
+
sendJson(res, 200, {
|
|
131
|
+
ok: false,
|
|
132
|
+
error: "invalid-schema",
|
|
133
|
+
detail: parsed.issues
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const tmp = ADAPTERS_PATH + ".tmp";
|
|
138
|
+
writeFileSync(tmp, raw, "utf8");
|
|
139
|
+
renameSync(tmp, ADAPTERS_PATH);
|
|
140
|
+
sendJson(res, 200, {
|
|
141
|
+
ok: true,
|
|
142
|
+
value: parsed.value
|
|
143
|
+
});
|
|
144
|
+
} catch {
|
|
98
145
|
sendJson(res, 200, {
|
|
99
146
|
ok: false,
|
|
100
|
-
error: "
|
|
101
|
-
detail: parsed.issues
|
|
147
|
+
error: "write-failed"
|
|
102
148
|
});
|
|
103
|
-
return;
|
|
104
149
|
}
|
|
105
|
-
|
|
106
|
-
ok: true,
|
|
107
|
-
value: parsed.value
|
|
108
|
-
});
|
|
109
|
-
} catch {
|
|
110
|
-
sendJson(res, 200, {
|
|
111
|
-
ok: true,
|
|
112
|
-
value: { adapters: [] }
|
|
113
|
-
});
|
|
150
|
+
return;
|
|
114
151
|
}
|
|
152
|
+
sendJson(res, 405, {
|
|
153
|
+
ok: false,
|
|
154
|
+
error: "method-not-allowed"
|
|
155
|
+
});
|
|
115
156
|
}
|
|
116
157
|
};
|
|
117
158
|
const STATE_PATH = join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "quick-toolbar-state.json");
|
|
@@ -192,10 +233,38 @@ const stateRouteDefinition = {
|
|
|
192
233
|
});
|
|
193
234
|
}
|
|
194
235
|
};
|
|
236
|
+
let wsSvc = null;
|
|
237
|
+
const authUrlRouteDefinition = {
|
|
238
|
+
kind: "exact",
|
|
239
|
+
path: "/quick-toolbar/api/auth-url",
|
|
240
|
+
handler: async (_req, res) => {
|
|
241
|
+
const w = wsSvc;
|
|
242
|
+
if (w === null || w === void 0) {
|
|
243
|
+
sendJson(res, 200, {
|
|
244
|
+
ok: false,
|
|
245
|
+
error: "auth-url-unavailable"
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
sendJson(res, 200, {
|
|
251
|
+
ok: true,
|
|
252
|
+
url: w.connection.authenticatedUrl("http://" + w.webServer.host + ":" + String(w.webServer.port) + "/")
|
|
253
|
+
});
|
|
254
|
+
} catch {
|
|
255
|
+
sendJson(res, 200, {
|
|
256
|
+
ok: false,
|
|
257
|
+
error: "auth-url-unavailable"
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
195
262
|
function apply(ctx) {
|
|
196
|
-
ctx.inject(["webServer"], ((wsCtx) => {
|
|
263
|
+
ctx.inject(["webServer", "connection"], ((wsCtx) => {
|
|
264
|
+
wsSvc = wsCtx;
|
|
197
265
|
wsCtx.webServer.register(adaptersRouteDefinition);
|
|
198
266
|
wsCtx.webServer.register(stateRouteDefinition);
|
|
267
|
+
wsCtx.webServer.register(authUrlRouteDefinition);
|
|
199
268
|
}));
|
|
200
269
|
}
|
|
201
270
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@max-null/dsh-quick-toolbar",
|
|
3
|
-
"version": "0.3
|
|
4
|
-
"description": "
|
|
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
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@max-null/dsh-quick-toolbar",
|
|
3
|
+
"version": "0.8.3",
|
|
4
|
+
"description": "插件按钮聚合器(高度可扩展):把三方插件散乱按钮聚合到统一入口——SSiD 标题栏 / web 悬浮球;载体+驻场 LLM 范式——任何人都能把自己环境里任何插件的按钮动态聚合(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
|
+
}
|