@max-null/dsh-quick-toolbar 0.3.1 → 0.8.2
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/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 +379 -39
- package/lib/index.js +92 -23
- package/package.json +2 -2
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
|
package/lib/client.js
CHANGED
|
@@ -75,18 +75,21 @@ function actClick(target) {
|
|
|
75
75
|
return true;
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
78
|
+
* 面板开/关(v0.8.0 探测语义——替代已废弃的面板容器协议 findPanel/#id-panel,
|
|
79
|
+
* 该协议从未有环境实现:client 半 findPanel 恒 null,内置项走 toolbarAction 桥接,
|
|
80
|
+
* 用户适配器因此死路——「添加按钮2」会话 LLM 源码实测发现):
|
|
81
|
+
* close 目标**可见** = 弹窗开着 → 点它关闭;**不可见/不存在** = 弹窗关着 → 点原按钮打开。
|
|
82
|
+
* @param openTarget 原按钮(弹窗关着时点击它打开)
|
|
83
|
+
* @param closeTarget 弹窗内关闭按钮(LLM 探测、随弹窗显隐)
|
|
84
|
+
* @param probeVisible 可见性探测(DOM 环境 = rect/computedStyle;测试可 stub)
|
|
80
85
|
*/
|
|
81
|
-
function actTogglePanel(
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
return false;
|
|
86
|
+
function actTogglePanel(openTarget, closeTarget, probeVisible) {
|
|
87
|
+
if (closeTarget !== null && closeTarget !== void 0 && closeTarget.disabled !== true && probeVisible(closeTarget)) {
|
|
88
|
+
closeTarget.click();
|
|
89
|
+
return true;
|
|
88
90
|
}
|
|
89
|
-
|
|
91
|
+
if (openTarget === null || openTarget === void 0 || openTarget.disabled === true) return false;
|
|
92
|
+
openTarget.click();
|
|
90
93
|
return true;
|
|
91
94
|
}
|
|
92
95
|
/** 官方设置触发器锚点链(v2 调研点① 结论:DSH 面板 open 状态由 shell 私有,
|
|
@@ -155,22 +158,60 @@ function actCommand(env, name) {
|
|
|
155
158
|
* 行为库保持窄接口(可测);本层唯一负责 DOM 绑定(browser 环境,
|
|
156
159
|
* 不在 node:test 的范围里——DOM 交互留给 L2 环境验证)。
|
|
157
160
|
*/
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
161
|
+
/** 遮罩关闭候选链(第二次点击的 mask 通道——DSH 弹窗多为遮罩交互) */
|
|
162
|
+
const MASK_CHAINS = [
|
|
163
|
+
"[class$=\"_backdrop\"]",
|
|
164
|
+
"[class*=\"backdrop\"]",
|
|
165
|
+
"[class$=\"_mask\"]",
|
|
166
|
+
"[class*=\"modal-mask\"]"
|
|
167
|
+
];
|
|
168
|
+
/**
|
|
169
|
+
* 归一 secondClick 通道(v0.8.0「二次点击事件」):secondClick 优先,close 旧字段
|
|
170
|
+
* 兼容(字符串简写 = 点击关闭按钮)。
|
|
171
|
+
*/
|
|
172
|
+
function closeTargetOf(act, env) {
|
|
173
|
+
const spec = act.secondClick ?? act.close ?? null;
|
|
174
|
+
if (spec === null) return null;
|
|
175
|
+
if (typeof spec === "string") return env.find(spec);
|
|
176
|
+
if (spec.kind === "mask") {
|
|
177
|
+
const probe = env.isVisible ?? defaultIsVisible;
|
|
178
|
+
for (let i = 0; i < MASK_CHAINS.length; i++) {
|
|
179
|
+
const el = env.find(MASK_CHAINS[i]);
|
|
180
|
+
if (el !== null && probe(el)) return el;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
return env.find(spec.selector);
|
|
161
185
|
}
|
|
162
|
-
/**
|
|
186
|
+
/** 缺省可见性探测(DOM:offset 尺寸优先,computedStyle 兜底) */
|
|
187
|
+
function defaultIsVisible(el) {
|
|
188
|
+
if (el === null || typeof el !== "object") return false;
|
|
189
|
+
const node = el;
|
|
190
|
+
if (typeof node.offsetWidth === "number") {
|
|
191
|
+
if (node.offsetWidth > 0 || node.offsetHeight > 0) return true;
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (typeof getComputedStyle === "function") try {
|
|
195
|
+
const style = getComputedStyle(node);
|
|
196
|
+
return style.display !== "none" && style.visibility !== "hidden";
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
/** 按内置定义执行一条适配器(防御执行:定位失败/禁用 → false,绝不误伤)
|
|
203
|
+
* 「换位置」缺省:act 未填 → click(点击原按钮)。 */
|
|
163
204
|
function runAdapter(adapter, env) {
|
|
164
|
-
|
|
205
|
+
const act = adapter.act ?? { kind: "click" };
|
|
206
|
+
switch (act.kind) {
|
|
165
207
|
case "click": return actClick(env.find(adapter.button));
|
|
166
|
-
case "dispatch-event": return env.dispatch(
|
|
208
|
+
case "dispatch-event": return env.dispatch(act.event, act.detail);
|
|
167
209
|
case "toggle-panel": {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
return false;
|
|
210
|
+
const closeEl = closeTargetOf(act, env);
|
|
211
|
+
return actTogglePanel(env.find(adapter.button), closeEl, env.isVisible ?? defaultIsVisible);
|
|
171
212
|
}
|
|
172
213
|
case "open-settings":
|
|
173
|
-
if (
|
|
214
|
+
if (act.path !== void 0) console.warn(`quick-toolbar: open-settings path '${act.path}' 暂不支持(v2 深链待入)`);
|
|
174
215
|
return actOpenSettings({
|
|
175
216
|
find: env.find,
|
|
176
217
|
...env.findByText !== void 0 ? { findByText: env.findByText } : {}
|
|
@@ -180,10 +221,110 @@ function runAdapter(adapter, env) {
|
|
|
180
221
|
console.warn("quick-toolbar: command 环境无 runCommand 通道(旧版 DSH/未注入)");
|
|
181
222
|
return false;
|
|
182
223
|
}
|
|
183
|
-
return actCommand({ execute: env.runCommand },
|
|
224
|
+
return actCommand({ execute: env.runCommand }, act.name);
|
|
184
225
|
}
|
|
185
226
|
}
|
|
186
227
|
//#endregion
|
|
228
|
+
//#region src/register-brief.ts
|
|
229
|
+
/**
|
|
230
|
+
* 注册任务书(v0.5.1)——「➕ 按钮」注入的 prompt 文本。
|
|
231
|
+
*
|
|
232
|
+
* 设计(V2-9 载体哲学 + 安装教程闭环):
|
|
233
|
+
* 用户点击工具栏「➕」→ 首选经 sessions/workspaces/uiWorkspace 服务创建
|
|
234
|
+
* 「添加按钮」会话并把本任务书以 queue 模式注入(自动执行)——同款机制 =
|
|
235
|
+
* 插件中心 LLM 更新(ensureLlmUpdateSession,2026-08-30 实证修正:早期
|
|
236
|
+
* inject=[] 令 apply 只见 {fiber},「无会话通道」判断是错结论);
|
|
237
|
+
* 服务缺失/失败 → 降级为当前 composer 草稿注入(用户 Enter 发送)。
|
|
238
|
+
* 任务书必须自包含(环境 LLM 不一定能访问本仓库文件)——协议要点全部内嵌。
|
|
239
|
+
*
|
|
240
|
+
* 同步纪律:字段与 src/adapters.ts / src/schema.ts 保持一致;
|
|
241
|
+
* 漂移即教程失效(tests/register-brief.spec.ts 有锚点断言防退化)。
|
|
242
|
+
*/
|
|
243
|
+
const REGISTER_BRIEF = `# 任务:为「工具栏插件」迁移 / 新增一个按钮
|
|
244
|
+
|
|
245
|
+
你是 DSH 环境的驻场工程师。用户想把某个按钮**迁移**到工具栏
|
|
246
|
+
(@max-null/dsh-quick-toolbar:SSiD 标题栏按钮组 / web 悬浮球·聚合面板),
|
|
247
|
+
或者**新增**一个自定义按钮。工具栏是「载体」——你注册的是**数据**
|
|
248
|
+
(图标/文字/点击由载体从原按钮派生或按声明执行),不是代码。
|
|
249
|
+
|
|
250
|
+
## 第一步(现在就要做):先反问用户,不要猜
|
|
251
|
+
|
|
252
|
+
用一句话向用户确认目标,等回答后再动手:
|
|
253
|
+
|
|
254
|
+
- **迁移**:要迁移的是哪个按钮?它在哪个面板/页面/插件里,长什么样?
|
|
255
|
+
- **新增**:要新增什么样的按钮?图标 / 文字 / 点击后应发生什么?
|
|
256
|
+
|
|
257
|
+
## 第二步(确认目标后):自主探查并验证
|
|
258
|
+
|
|
259
|
+
- **探查通道**:经 ➕ 注入的本任务书开头附有「探查通道」段(当前 DSH 页面完整
|
|
260
|
+
URL,含认证 token)——用浏览器直接打开它即可访问用户环境;**不要裸访问
|
|
261
|
+
内核端口(会 401)**。手工粘贴本文件时若无附带 URL,向用户要页面地址。
|
|
262
|
+
- 用你的浏览器 / 代码能力找到目标按钮元素(DOM 查询、截图、读源码都行),
|
|
263
|
+
**自己验证 CSS 选择器确实可用**——不要凭猜测写选择器,不要依赖用户给 DOM 快照。
|
|
264
|
+
- 提示:场景无限,没有"完美脚本"能预适配所有环境——探查正是你在场的原因,
|
|
265
|
+
载体不内置扫描器,这个活由你做。
|
|
266
|
+
|
|
267
|
+
## 第三步:注册(数据,不是代码)
|
|
268
|
+
|
|
269
|
+
**最小注册(迁移——推荐起点)**:被整合的按钮自带图标/文字/点击事件,聚合就是
|
|
270
|
+
"换个位置",两条就够了:
|
|
271
|
+
|
|
272
|
+
\`\`\`json
|
|
273
|
+
{ "id": "dsh-some-plugin", "button": ".some-btn" }
|
|
274
|
+
\`\`\`
|
|
275
|
+
|
|
276
|
+
\`\`\`json
|
|
277
|
+
{ "adapters": [ { "id": "dsh-some-plugin", "button": ".some-btn" } ] }
|
|
278
|
+
\`\`\`
|
|
279
|
+
|
|
280
|
+
- 图标 = 扣取原按钮视觉(svg/img/背景);文字 = 扣取原按钮文案;
|
|
281
|
+
点击 = 点击原按钮(act 缺省 \`click\`)。
|
|
282
|
+
- **原按钮自动隐藏**(换位置 = 双入口无意义;缺省隐藏,显式 \`"hide": false\` 才保留——
|
|
283
|
+
若原按钮被隐藏后其弹窗定位错位,改用 \`hide: false\`)。
|
|
284
|
+
- **自定义按钮**(无原按钮可扣)才需要显式字段:
|
|
285
|
+
\`\`\`json
|
|
286
|
+
{ "id": "my-btn", "button": "#app-shell", "icon": { "source": "custom", "value": "emoji 或 svg" },
|
|
287
|
+
"label": "文字", "act": { "kind": "click" }, "hide": false }
|
|
288
|
+
\`\`\`
|
|
289
|
+
|
|
290
|
+
**行为枚举(只能选这些,禁止写代码)**:
|
|
291
|
+
|
|
292
|
+
| kind | 语义 |
|
|
293
|
+
|---|---|
|
|
294
|
+
| \`click\` | 直接点击 |
|
|
295
|
+
| \`toggle-panel\` | **二次点击事件**:弹窗开 → 按 \`secondClick\` 通道关闭;关 → 点原按钮打开。\`secondClick\` 推荐 \`{ "kind": "mask" }\`(点遮罩关闭——DSH 弹窗通用交互);或点关闭按钮 \`{ "kind": "click", "selector": "验证过的关闭按钮" }\`(也可直接写选择器字符串) |
|
|
296
|
+
| \`dispatch-event\` | 派发 CustomEvent(\`event\`、\`detail?\`) |
|
|
297
|
+
| \`open-settings\` | 打开设置面板(引擎语义锚点定位,再点关闭) |
|
|
298
|
+
| \`command\` | 向输入框注入斜杠命令草稿(\`name\`,不含 \`/\`) |
|
|
299
|
+
|
|
300
|
+
## 探测闭环(注册前必须完成——别让用户点出"开得关不掉")
|
|
301
|
+
|
|
302
|
+
用你的浏览器能力实测原按钮行为,按结果选 act:
|
|
303
|
+
|
|
304
|
+
1. 点击一次:开的是**弹窗/面板**还是普通页面?
|
|
305
|
+
2. 弹窗/面板能**再点原按钮关闭**吗?**不能** → act 必须用 \`toggle-panel\` 并
|
|
306
|
+
探测弹窗内的「关闭」按钮(浏览弹窗 DOM,选准且唯一的选择器写 \`close\`):
|
|
307
|
+
\`{\"act\": { \"kind\": \"toggle-panel\", \"close\": \".verified-close\" }}\`——
|
|
308
|
+
「再点关闭」是载体的基本承诺,\`click\` 只适用于天然可再点切换的按钮。
|
|
309
|
+
3. 记录原按钮是否会在意「隐藏」(见第三步 hide 说明)。
|
|
310
|
+
|
|
311
|
+
## 第四步:写入配置,让用户刷新
|
|
312
|
+
|
|
313
|
+
把完整 JSON(含 \`adapters\` 数组)写入:
|
|
314
|
+
\`~/.dsh/quick-toolbar-adapters.json\`(DSH 环境即 \`$DSH_HOME/quick-toolbar-adapters.json\`;
|
|
315
|
+
重复 \`id\` = 覆盖既有注册)。写入后告知用户**刷新页面**,按钮即出现在工具栏;
|
|
316
|
+
若未出现,修正选择器后重试。协议全文见仓库 \`adapters.prompt.md\`(如可访问请先读一遍)。
|
|
317
|
+
|
|
318
|
+
## 注册后验证清单(刷新后逐项确认)
|
|
319
|
+
|
|
320
|
+
① 按钮已出现在工具栏(图标/文字来自原按钮) ② 点击可开启 ③ 再点能关闭
|
|
321
|
+
(toggle-panel 场景) ④ 原按钮已隐藏(或按 hide:false 保留)。任一项不满足 → 修正后重试。
|
|
322
|
+
|
|
323
|
+
## 纪律
|
|
324
|
+
|
|
325
|
+
- 只写数据,不提供函数/代码(schema 拒绝一切非枚举行为)。
|
|
326
|
+
- 一次只注册 1–2 个按钮,验证后再扩展;定位失败无害(引擎静默跳过,绝不误点)。`;
|
|
327
|
+
//#endregion
|
|
187
328
|
//#region src/client.ts
|
|
188
329
|
/**
|
|
189
330
|
* @max-null/dsh-quick-toolbar — browser half.
|
|
@@ -292,6 +433,8 @@ window.__ModuleLoader__.load({
|
|
|
292
433
|
"tb.sidebar": ["侧栏", "Sidebar"],
|
|
293
434
|
"tb.bottom": ["底栏", "Bottom panel"],
|
|
294
435
|
"tb.sessions": ["会话管理", "Sessions"],
|
|
436
|
+
"tb.add": ["添加按钮", "Add button"],
|
|
437
|
+
"tb.addAria": ["添加/迁移按钮(让 LLM 来注册)", "Add / migrate a button (let the LLM register it)"],
|
|
295
438
|
"sm.open": ["会话管理", "Sessions"],
|
|
296
439
|
"sm.openTitle": ["打开会话管理面板", "Open session manager"]
|
|
297
440
|
};
|
|
@@ -438,7 +581,15 @@ window.__ModuleLoader__.load({
|
|
|
438
581
|
"#ssid-toolbar .ssid-tb-pin svg{width:15px;height:15px}",
|
|
439
582
|
"#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
583
|
"#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)}"
|
|
584
|
+
"#ssid-toolbar .ssid-tb-btn svg{flex:none;width:15px;height:15px;color:var(--dsw-alias-label-secondary,#98a2b3)}",
|
|
585
|
+
"#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}",
|
|
586
|
+
"#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))}",
|
|
587
|
+
"#ssid-toolbar .ssid-tb-add svg{flex:none;width:15px;height:15px;color:var(--dsw-alias-label-secondary,#98a2b3)}",
|
|
588
|
+
".ssid-tb-row{position:relative;overflow:hidden;border-radius:8px}",
|
|
589
|
+
".ssid-tb-slide{display:flex;position:relative;width:100%;transition:transform .2s ease}",
|
|
590
|
+
".ssid-tb-row .ssid-tb-btn{flex:1 1 auto;min-width:0;position:relative;z-index:1;box-sizing:border-box;background:transparent}",
|
|
591
|
+
".ssid-tb-row.ssid-tb-row-open .ssid-tb-slide{transform:translateX(-56px)}",
|
|
592
|
+
".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
593
|
].join("\n");
|
|
443
594
|
function toolbarIcon(name) {
|
|
444
595
|
var ICONS = {
|
|
@@ -450,7 +601,8 @@ window.__ModuleLoader__.load({
|
|
|
450
601
|
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
602
|
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
603
|
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>"
|
|
604
|
+
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>",
|
|
605
|
+
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
606
|
};
|
|
455
607
|
return ICONS[name] || ICONS.grid;
|
|
456
608
|
}
|
|
@@ -459,8 +611,20 @@ window.__ModuleLoader__.load({
|
|
|
459
611
|
find: function(s) {
|
|
460
612
|
return document.querySelector(s);
|
|
461
613
|
},
|
|
462
|
-
|
|
463
|
-
return
|
|
614
|
+
isVisible: function(el) {
|
|
615
|
+
if (el === null || typeof el !== "object") return false;
|
|
616
|
+
var node = el;
|
|
617
|
+
if (typeof node.getBoundingClientRect === "function") {
|
|
618
|
+
var r = node.getBoundingClientRect();
|
|
619
|
+
if (r.width > 0 || r.height > 0) return true;
|
|
620
|
+
}
|
|
621
|
+
if (typeof getComputedStyle === "function") try {
|
|
622
|
+
var s = getComputedStyle(node);
|
|
623
|
+
return s.display !== "none" && s.visibility !== "hidden";
|
|
624
|
+
} catch (_e) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
return false;
|
|
464
628
|
},
|
|
465
629
|
dispatch: function(event, detail) {
|
|
466
630
|
window.dispatchEvent(new CustomEvent(event, { detail }));
|
|
@@ -479,11 +643,10 @@ window.__ModuleLoader__.load({
|
|
|
479
643
|
}
|
|
480
644
|
};
|
|
481
645
|
};
|
|
482
|
-
function
|
|
646
|
+
function injectComposerDraft(text) {
|
|
483
647
|
var seat = document.querySelector("[data-composer-seat]");
|
|
484
648
|
var el = seat !== null && seat !== void 0 ? seat.querySelector("textarea, [contenteditable=\"true\"]") : document.querySelector("textarea, [contenteditable=\"true\"]");
|
|
485
649
|
if (el === null || el === void 0) return false;
|
|
486
|
-
var text = "/" + name;
|
|
487
650
|
if (el instanceof HTMLTextAreaElement) {
|
|
488
651
|
var setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
|
|
489
652
|
if (setter !== void 0 && setter.set !== void 0) setter.set.call(el, text);
|
|
@@ -496,14 +659,11 @@ window.__ModuleLoader__.load({
|
|
|
496
659
|
data: text
|
|
497
660
|
}));
|
|
498
661
|
el.focus();
|
|
499
|
-
el.dispatchEvent(new KeyboardEvent("keydown", {
|
|
500
|
-
key: "Enter",
|
|
501
|
-
code: "Enter",
|
|
502
|
-
bubbles: true,
|
|
503
|
-
cancelable: true
|
|
504
|
-
}));
|
|
505
662
|
return true;
|
|
506
663
|
}
|
|
664
|
+
function typeCommandIntoComposer(name) {
|
|
665
|
+
return injectComposerDraft("/" + name);
|
|
666
|
+
}
|
|
507
667
|
function toolbarAction(kind) {
|
|
508
668
|
if (kind === "plugin") {
|
|
509
669
|
closeSidePanelsBeforePluginCenter();
|
|
@@ -531,6 +691,7 @@ window.__ModuleLoader__.load({
|
|
|
531
691
|
function createToolbar() {
|
|
532
692
|
if (win.__SSID_SHELL__ === true && !qtState.shellVisible) return;
|
|
533
693
|
if (document.getElementById(TOOLBAR_ID) !== null) return;
|
|
694
|
+
var addBtn = null;
|
|
534
695
|
var root = document.createElement("div");
|
|
535
696
|
root.id = TOOLBAR_ID;
|
|
536
697
|
var panel = document.createElement("div");
|
|
@@ -554,13 +715,32 @@ window.__ModuleLoader__.load({
|
|
|
554
715
|
"dsh-session-manager": "sessions"
|
|
555
716
|
};
|
|
556
717
|
var adapterIconHtml = function(adapter, kind) {
|
|
557
|
-
if (adapter.icon.source === "custom") {
|
|
718
|
+
if (adapter.icon !== void 0 && adapter.icon.source === "custom") {
|
|
558
719
|
var value = adapter.icon.value;
|
|
559
720
|
if (value.indexOf("<") === 0) return value;
|
|
560
721
|
return toolbarIcon(value);
|
|
561
722
|
}
|
|
723
|
+
try {
|
|
724
|
+
var el = document.querySelector(adapter.button);
|
|
725
|
+
if (el !== null) {
|
|
726
|
+
var svg = el.querySelector("svg");
|
|
727
|
+
if (svg !== null) return svg.outerHTML;
|
|
728
|
+
var img = el.querySelector("img");
|
|
729
|
+
if (img !== null) return img.outerHTML;
|
|
730
|
+
var bg = getComputedStyle(el).backgroundImage;
|
|
731
|
+
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>";
|
|
732
|
+
}
|
|
733
|
+
} catch (_e) {}
|
|
562
734
|
return kind !== null ? toolbarIcon(kind) : toolbarIcon("grid");
|
|
563
735
|
};
|
|
736
|
+
var adapterLabel = function(adapter) {
|
|
737
|
+
if (adapter.label !== void 0 && adapter.label !== "") return adapter.label;
|
|
738
|
+
try {
|
|
739
|
+
var el = document.querySelector(adapter.button);
|
|
740
|
+
if (el !== null && (el.textContent || "").trim() !== "") return (el.textContent || "").trim().slice(0, 12);
|
|
741
|
+
} catch (_e) {}
|
|
742
|
+
return adapter.id;
|
|
743
|
+
};
|
|
564
744
|
var renderButton = function(adapter, kind) {
|
|
565
745
|
var b = document.createElement("button");
|
|
566
746
|
b.type = "button";
|
|
@@ -574,7 +754,7 @@ window.__ModuleLoader__.load({
|
|
|
574
754
|
trackLocale(b, "tb." + kind, "aria");
|
|
575
755
|
trackLocale(b, "tb." + kind, "title");
|
|
576
756
|
} else {
|
|
577
|
-
var label = adapter
|
|
757
|
+
var label = adapterLabel(adapter);
|
|
578
758
|
var textSpan = b.querySelector("span");
|
|
579
759
|
if (textSpan !== null) textSpan.textContent = label;
|
|
580
760
|
b.setAttribute("aria-label", label);
|
|
@@ -587,13 +767,91 @@ window.__ModuleLoader__.load({
|
|
|
587
767
|
}
|
|
588
768
|
runAdapter(adapter, toolbarEnv());
|
|
589
769
|
});
|
|
590
|
-
panel.
|
|
770
|
+
if (addBtn !== null && addBtn.parentNode === panel) panel.insertBefore(b, addBtn);
|
|
771
|
+
else panel.appendChild(b);
|
|
772
|
+
if (adapter.hide !== false) try {
|
|
773
|
+
var origBtn = document.querySelector(adapter.button);
|
|
774
|
+
if (origBtn !== null) origBtn.style.display = "none";
|
|
775
|
+
} catch (_e) {}
|
|
776
|
+
return b;
|
|
591
777
|
};
|
|
592
778
|
for (var ai = 0; ai < BUILTIN_ADAPTERS.length; ai++) {
|
|
593
779
|
var adapter = BUILTIN_ADAPTERS[ai];
|
|
594
780
|
var kind = TOOLBAR_KIND_BY_ADAPTER[adapter.id];
|
|
595
781
|
renderButton(adapter, kind !== void 0 ? kind : null);
|
|
596
782
|
}
|
|
783
|
+
var slideCloseAll = function() {
|
|
784
|
+
var rows = panel.querySelectorAll(".ssid-tb-row.ssid-tb-row-open");
|
|
785
|
+
for (var ri = 0; ri < rows.length; ri++) rows[ri].classList.remove("ssid-tb-row-open");
|
|
786
|
+
};
|
|
787
|
+
document.addEventListener("mousedown", function(e) {
|
|
788
|
+
var t = e.target;
|
|
789
|
+
if (t === null) return;
|
|
790
|
+
if (!(t instanceof Element ? t.closest(".ssid-tb-row") !== null : false)) slideCloseAll();
|
|
791
|
+
});
|
|
792
|
+
function removeUserAdapter(ad) {
|
|
793
|
+
fetch("/quick-toolbar/api/adapters").then(function(r) {
|
|
794
|
+
return r.json();
|
|
795
|
+
}).then(function(data) {
|
|
796
|
+
var envelope = data !== null && typeof data === "object" ? data : void 0;
|
|
797
|
+
var rows = envelope !== void 0 && envelope.ok === true && envelope.value !== null && typeof envelope.value === "object" ? envelope.value.adapters : void 0;
|
|
798
|
+
if (!Array.isArray(rows)) return;
|
|
799
|
+
var next = rows.filter(function(a) {
|
|
800
|
+
return a.id !== ad.id;
|
|
801
|
+
});
|
|
802
|
+
return fetch("/quick-toolbar/api/adapters", {
|
|
803
|
+
method: "POST",
|
|
804
|
+
headers: { "Content-Type": "application/json" },
|
|
805
|
+
body: JSON.stringify({ adapters: next })
|
|
806
|
+
}).then(function(r) {
|
|
807
|
+
return r.json();
|
|
808
|
+
}).then(function(res) {
|
|
809
|
+
if (res === null || typeof res !== "object" || res.ok !== true) return;
|
|
810
|
+
try {
|
|
811
|
+
var btn = panel.querySelector("[data-adapter-id=\"" + ad.id.replace(/"/g, "\\\"") + "\"]");
|
|
812
|
+
var row = btn !== null && typeof btn.closest === "function" ? btn.closest(".ssid-tb-row") : null;
|
|
813
|
+
if (row !== null && row.parentNode === panel) panel.removeChild(row);
|
|
814
|
+
} catch (_e) {}
|
|
815
|
+
try {
|
|
816
|
+
var orig = document.querySelector(ad.button);
|
|
817
|
+
if (orig !== null) orig.style.display = "";
|
|
818
|
+
} catch (_e2) {}
|
|
819
|
+
});
|
|
820
|
+
}).catch(function() {});
|
|
821
|
+
}
|
|
822
|
+
function wrapAdapterRow(btn, ad) {
|
|
823
|
+
var row = document.createElement("div");
|
|
824
|
+
row.className = "ssid-tb-row";
|
|
825
|
+
var slide = document.createElement("div");
|
|
826
|
+
slide.className = "ssid-tb-slide";
|
|
827
|
+
if (btn.parentNode !== null) btn.parentNode.removeChild(btn);
|
|
828
|
+
slide.appendChild(btn);
|
|
829
|
+
var delBtn = document.createElement("button");
|
|
830
|
+
delBtn.type = "button";
|
|
831
|
+
delBtn.className = "ssid-tb-del";
|
|
832
|
+
delBtn.textContent = "删除";
|
|
833
|
+
delBtn.addEventListener("click", function() {
|
|
834
|
+
row.classList.remove("ssid-tb-row-open");
|
|
835
|
+
setTimeout(function() {
|
|
836
|
+
removeUserAdapter(ad);
|
|
837
|
+
}, 220);
|
|
838
|
+
});
|
|
839
|
+
slide.appendChild(delBtn);
|
|
840
|
+
row.appendChild(slide);
|
|
841
|
+
panel.insertBefore(row, addBtn);
|
|
842
|
+
btn.setAttribute("data-user-adapter", "1");
|
|
843
|
+
row.addEventListener("contextmenu", function(e) {
|
|
844
|
+
e.preventDefault();
|
|
845
|
+
e.stopPropagation();
|
|
846
|
+
if (row.classList.contains("ssid-tb-row-open")) {
|
|
847
|
+
row.classList.remove("ssid-tb-row-open");
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
slideCloseAll();
|
|
851
|
+
row.classList.add("ssid-tb-row-open");
|
|
852
|
+
});
|
|
853
|
+
return row;
|
|
854
|
+
}
|
|
597
855
|
var fetchUserAdapters = function() {
|
|
598
856
|
fetch("/quick-toolbar/api/adapters").then(function(r) {
|
|
599
857
|
return r.json();
|
|
@@ -607,12 +865,33 @@ window.__ModuleLoader__.load({
|
|
|
607
865
|
var userKind = TOOLBAR_KIND_BY_ADAPTER[user.id];
|
|
608
866
|
try {
|
|
609
867
|
var old = panel.querySelector("[data-adapter-id=\"" + user.id.replace(/"/g, "\\\"") + "\"]");
|
|
610
|
-
|
|
868
|
+
var oldRow = old !== null && typeof old.closest === "function" ? old.closest(".ssid-tb-row") : null;
|
|
869
|
+
if (oldRow !== null && oldRow.parentNode === panel) panel.removeChild(oldRow);
|
|
870
|
+
else if (old !== null && old.parentNode === panel) panel.removeChild(old);
|
|
611
871
|
} catch (_e) {}
|
|
612
|
-
renderButton(user, userKind !== void 0 ? userKind : null);
|
|
872
|
+
wrapAdapterRow(renderButton(user, userKind !== void 0 ? userKind : null), user);
|
|
613
873
|
}
|
|
614
874
|
}).catch(function() {});
|
|
615
875
|
};
|
|
876
|
+
addBtn = document.createElement("button");
|
|
877
|
+
addBtn.type = "button";
|
|
878
|
+
addBtn.className = "ssid-tb-add";
|
|
879
|
+
addBtn.setAttribute("aria-label", "添加/迁移按钮");
|
|
880
|
+
addBtn.title = "添加按钮";
|
|
881
|
+
addBtn.innerHTML = toolbarIcon("add") + "<span></span>";
|
|
882
|
+
trackLocale(addBtn, "tb.addAria", "aria");
|
|
883
|
+
trackLocale(addBtn, "tb.add", "title");
|
|
884
|
+
var addSpan = addBtn.querySelector("span");
|
|
885
|
+
if (addSpan !== null) addSpan.textContent = "添加按钮";
|
|
886
|
+
trackLocale(addBtn, "tb.add", "text-span");
|
|
887
|
+
addBtn.addEventListener("click", function() {
|
|
888
|
+
ensureRegisterSession().then(function(ok) {
|
|
889
|
+
if (!ok) registerBriefWithSessionUrl().then(function(brief) {
|
|
890
|
+
injectComposerDraft(brief);
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
panel.appendChild(addBtn);
|
|
616
895
|
var ball = document.createElement("button");
|
|
617
896
|
ball.type = "button";
|
|
618
897
|
ball.id = TOOLBAR_ID + "-ball";
|
|
@@ -835,10 +1114,71 @@ window.__ModuleLoader__.load({
|
|
|
835
1114
|
document.addEventListener("mouseup", onUp);
|
|
836
1115
|
});
|
|
837
1116
|
}
|
|
838
|
-
exports.inject = [
|
|
1117
|
+
exports.inject = [
|
|
1118
|
+
"sessions",
|
|
1119
|
+
"workspaces",
|
|
1120
|
+
"uiWorkspace"
|
|
1121
|
+
];
|
|
1122
|
+
var sessionsSvc = null;
|
|
1123
|
+
var workspacesSvc = null;
|
|
1124
|
+
var uiWorkspaceSvc = null;
|
|
1125
|
+
function buildRegisterBrief(url) {
|
|
1126
|
+
return "## 探查通道(已为你的浏览器认证好):\n- 用浏览器打开下面这个 URL 即可直达当前 DSH 页面(首次访问会换 cookie 并自动重定向到干净根路径;token 仅本会话使用,勿写入文件/回复):\n" + url + "\n\n" + REGISTER_BRIEF;
|
|
1127
|
+
}
|
|
1128
|
+
function registerBriefWithSessionUrl() {
|
|
1129
|
+
return fetch("/quick-toolbar/api/auth-url").then(function(r) {
|
|
1130
|
+
return r.json();
|
|
1131
|
+
}).then(function(d) {
|
|
1132
|
+
return buildRegisterBrief(d !== null && typeof d === "object" && d.ok === true && typeof d.url === "string" ? d.url : "");
|
|
1133
|
+
}).catch(function() {
|
|
1134
|
+
return buildRegisterBrief("");
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
function ensureRegisterSession() {
|
|
1138
|
+
var svc = sessionsSvc;
|
|
1139
|
+
var wsvc = workspacesSvc;
|
|
1140
|
+
var uws = uiWorkspaceSvc;
|
|
1141
|
+
if (svc === null || wsvc === null || uws === null) return Promise.resolve(false);
|
|
1142
|
+
if (typeof uws.connectWorkspace !== "function") return Promise.resolve(false);
|
|
1143
|
+
var snapshot = wsvc.list !== void 0 && typeof wsvc.list.getSnapshot === "function" ? wsvc.list.getSnapshot() : void 0;
|
|
1144
|
+
var items = snapshot !== void 0 && snapshot !== null && snapshot.items !== void 0 && snapshot.items !== null ? snapshot.items : [];
|
|
1145
|
+
var wsId = items.length > 0 ? items[0].workspaceId : void 0;
|
|
1146
|
+
if (wsId === void 0 || wsId === "") return Promise.resolve(false);
|
|
1147
|
+
return uws.connectWorkspace(wsId).then(function(sid) {
|
|
1148
|
+
if (typeof sid !== "string" || sid === "") return false;
|
|
1149
|
+
var svc3 = svc;
|
|
1150
|
+
if (svc3 === null || svc3 === void 0) return false;
|
|
1151
|
+
var face = typeof svc3.binding === "function" ? svc3.binding(sid) : void 0;
|
|
1152
|
+
var s = face !== void 0 && face !== null && face.session !== void 0 && face.session !== null ? face.session : void 0;
|
|
1153
|
+
if (s === void 0 || s === null || typeof s.prompt !== "function") return false;
|
|
1154
|
+
return registerBriefWithSessionUrl().then(function(brief) {
|
|
1155
|
+
var sess2 = s;
|
|
1156
|
+
if (sess2 === void 0 || sess2 === null || typeof sess2.prompt !== "function") return Promise.resolve({ ok: false });
|
|
1157
|
+
return sess2.prompt([{
|
|
1158
|
+
type: "text",
|
|
1159
|
+
text: brief
|
|
1160
|
+
}], "queue");
|
|
1161
|
+
}).then(function(res) {
|
|
1162
|
+
if (res === void 0 || res === null || res.ok !== true) return false;
|
|
1163
|
+
var sess = s;
|
|
1164
|
+
var svc2 = svc;
|
|
1165
|
+
if (sess === void 0 || sess === null) return false;
|
|
1166
|
+
if (svc2 === null || svc2 === void 0) return false;
|
|
1167
|
+
if (typeof sess.rename === "function") sess.rename("添加按钮").catch(function() {});
|
|
1168
|
+
if (typeof svc2.open === "function") svc2.open(sid);
|
|
1169
|
+
return true;
|
|
1170
|
+
});
|
|
1171
|
+
}).catch(function() {
|
|
1172
|
+
return false;
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
839
1175
|
exports.apply = function(ctx) {
|
|
840
1176
|
if (win.__dshQuickToolbarInstalled === true) return;
|
|
841
1177
|
win.__dshQuickToolbarInstalled = true;
|
|
1178
|
+
var svcCtx = ctx !== null && typeof ctx === "object" ? ctx : {};
|
|
1179
|
+
sessionsSvc = svcCtx.sessions ?? null;
|
|
1180
|
+
workspacesSvc = svcCtx.workspaces ?? null;
|
|
1181
|
+
uiWorkspaceSvc = svcCtx.uiWorkspace ?? null;
|
|
842
1182
|
var style = document.createElement("style");
|
|
843
1183
|
style.setAttribute("data-dsh-quick-toolbar", "");
|
|
844
1184
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-quick-toolbar",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.2",
|
|
4
|
+
"description": "插件按钮聚合器(高度可扩展):把三方插件散乱按钮聚合到统一入口——SSiD 标题栏 / web 悬浮球;载体+驻场 LLM 范式——任何人都能把自己环境里任何插件的按钮动态聚合(LLM 一键注册、右键删除=注销,纯数据零代码,不动插件源码)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|