@max-null/dsh-quick-toolbar 0.1.0 → 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 +81 -13
- 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/docs/shots/web-panel.png +0 -0
- package/lib/client.js +666 -55
- package/lib/index.js +166 -18
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,16 +1,50 @@
|
|
|
1
|
-
# @max-null/dsh-quick-toolbar
|
|
1
|
+
# @max-null/dsh-quick-toolbar
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
本插件属于 **`@max-null/*` 插件系列**——这一系列共同构成 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 桌面体验。SSID 是整合它们的盒:`dsh-capture` · `dsh-chat-rail` · `dsh-chinese-thinking` · `dsh-draft-polish` · `dsh-guardian` · `dsh-habit` · `dsh-memory` · `dsh-node-appearance` · `dsh-plugin-center` · `dsh-quick-toolbar` · `dsh-skill-mcp-center` · `dsh-ssid-panels` · `dsh-ssid-zh-ui`。
|
|
4
|
+
|
|
5
|
+
This plugin belongs to the **`@max-null/*` family** — a set of plugins that together form the **[SSID (思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** desktop experience.
|
|
6
|
+
|
|
7
|
+
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**插件按钮聚合器**:把三方插件散乱的按钮(插件中心/侧栏/底栏/会话管理…)聚合到统一入口——SSiD 壳 → 标题栏按钮组;DSH web → **iOS 小白点式悬浮球**(自由定位拖拽、球↔面板 morph 展开、球永远锁定面板屏幕外侧角)。**核心价值 = 可扩展性**:本插件是「载体」(协议 + 引擎 + 注册表),**每个人都可以把**自己环境里任何插件的按钮**聚合进来**(LLM 一键注册 / 右键删除 = 注册制/注销制,全程不动插件源码、不写一行代码)——不要求第三方插件配合,不需要等作者适配。
|
|
8
|
+
|
|
9
|
+
A **plugin-button aggregator** for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): gathers scattered third-party plugin buttons (plugin center / sidebar / bottom bar / session manager …) into one entry — an SSiD title-bar button group in the SSID shell, and an iOS assistive-touch-style floating ball on plain DSH web (free positioning & dragging, ball↔panel morph expand, ball always locked to the panel's screen-outer corner). **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"把某个按钮加进来",它替你完成全部探查与注册;任何插件的按钮都能聚合,无论是官方的、社区的还是你自己的。
|
|
6
24
|
|
|
7
25
|
## 截图
|
|
8
26
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
|
27
|
+
### 动态聚合(GIF 实录)
|
|
28
|
+
|
|
29
|
+
| 聚合面板(内置 + LLM 注册按钮 + ➕) | 右键滑动删除(注销制) |
|
|
30
|
+
|---|---|
|
|
31
|
+
|  |  |
|
|
32
|
+
|
|
33
|
+
| 二次点击关闭(引擎探测语义) | ➕ LLM 注册会话(反问 → 探查 → 注册) |
|
|
34
|
+
|---|---|
|
|
35
|
+
|  |  |
|
|
36
|
+
|
|
37
|
+
### 一图两景:聚合面板 × LLM 反问(真实环境)
|
|
12
38
|
|
|
13
|
-
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
> Remote 为用户 LLM 现场注册的按钮(ds-harness-remote 入口)——演示「任何插件按钮都能聚合进统一入口」;右侧面板同时展示 LLM 按任务书先反问目标、再探查注册的完整流程。
|
|
42
|
+
|
|
43
|
+
| SSiD 标题栏(壳) | web 悬浮球 |
|
|
44
|
+
|---|---|
|
|
45
|
+
|  |  |
|
|
46
|
+
|
|
47
|
+
> 截图环境:SSiD 壳(标题栏/聚合面板)与 DSH web(悬浮球);Remote 为用户 LLM 现场注册的按钮(ds-harness-remote 入口),演示「统一入口聚合散落按钮」。
|
|
14
48
|
|
|
15
49
|
## 安装
|
|
16
50
|
|
|
@@ -21,18 +55,52 @@ dsh plugin --profile web add @max-null/dsh-quick-toolbar
|
|
|
21
55
|
|
|
22
56
|
## 使用
|
|
23
57
|
|
|
24
|
-
- **聚合按钮**:标题栏/悬浮球点开面板 →
|
|
25
|
-
-
|
|
58
|
+
- **聚合按钮**:标题栏/悬浮球点开面板 → 一键触发各插件功能(再点关闭——会话管理探测弹窗关闭按钮;设置面板语义锚点打开/再点关闭)。
|
|
59
|
+
- **内置入口**:悬浮球含「设置」(官方设置面板:引擎按语义锚点定位 footer 触发器,双 locale;再点关闭——DSH trigger 原生只开不关)。
|
|
60
|
+
- **壳环境(SSiD)**:悬浮球默认隐藏(标题栏接管入口);标题栏「悬浮球」开关按钮(圆圈+圆点图标)可开启。
|
|
61
|
+
- **用户适配器**:点面板 ➕ 或复制 `adapters.prompt.md` 到任意会话 → LLM 生成 `{ "adapters": [...] }` → 写入 `~/.dsh/quick-toolbar-adapters.json` → 刷新页面生效(host API 校验,非法条目丢弃并报明细)。**推荐直接走 ➕(见教程)**。
|
|
26
62
|
- 定位失败/插件未装/被禁用 → 静默跳过(绝不误伤、绝不误点)。
|
|
63
|
+
- **状态持久化**:位置/钉住/折叠/壳开关走 host(`/quick-toolbar/api/state` → `~/.dsh/quick-toolbar-state.json`,SSiD 开发手册 §7.10 规则)——内核动态端口不再丢状态。
|
|
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.
|
|
27
94
|
|
|
28
95
|
## 架构
|
|
29
96
|
|
|
30
97
|
- `src/adapters.ts`:适配器 schema + 内置适配器集(黄金示例)
|
|
31
|
-
- `src/behaviors.ts`:行为库(click / toggle-panel / dispatch-event / open-settings
|
|
98
|
+
- `src/behaviors.ts`:行为库(click / toggle-panel / dispatch-event / open-settings(语义锚点+再点关闭)/ command(composer 注入)——全部实现,v2 M1)
|
|
32
99
|
- `src/engine.ts`:执行器(防御执行)
|
|
33
100
|
- `src/schema.ts`:用户配置 zod 校验(LLM 产物防线)
|
|
34
|
-
- `src/
|
|
35
|
-
-
|
|
101
|
+
- `src/register-brief.ts`:➕ 按钮注入的注册任务书(先反问 → 探查 → 最小注册,与协议字段同步,锚点测试防漂移)
|
|
102
|
+
- `src/index.ts`(host 半):`GET /quick-toolbar/api/adapters`(用户适配器)+ `GET/POST /quick-toolbar/api/state`(状态 host 化,原子写)
|
|
103
|
+
- 设计文档:`doc/设计/2026-08-30-quick-toolbar-独立化设计方案.md`、`doc/设计/2026-08-30-quick-toolbar-v2设计.md`
|
|
36
104
|
|
|
37
105
|
## 开发
|
|
38
106
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/shots/web-panel.png
CHANGED
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -51,9 +51,280 @@ const BUILTIN_ADAPTERS = [
|
|
|
51
51
|
close: ".sm-modal .close"
|
|
52
52
|
},
|
|
53
53
|
hide: true
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "dsh-settings",
|
|
57
|
+
button: "button[aria-label=\"设置\"], button[aria-label=\"Settings\"]",
|
|
58
|
+
icon: {
|
|
59
|
+
source: "custom",
|
|
60
|
+
value: "settings"
|
|
61
|
+
},
|
|
62
|
+
label: "设置",
|
|
63
|
+
act: { kind: "open-settings" },
|
|
64
|
+
hide: false
|
|
54
65
|
}
|
|
55
66
|
];
|
|
56
67
|
//#endregion
|
|
68
|
+
//#region src/behaviors.ts
|
|
69
|
+
/**
|
|
70
|
+
* 直接点击目标(定位失败/禁用 → 静默跳过;称「防御执行」)。
|
|
71
|
+
*/
|
|
72
|
+
function actClick(target) {
|
|
73
|
+
if (target === null || target === void 0 || target.disabled === true) return false;
|
|
74
|
+
target.click();
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 面板开/关(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)
|
|
85
|
+
*/
|
|
86
|
+
function actTogglePanel(openTarget, closeTarget, probeVisible) {
|
|
87
|
+
if (closeTarget !== null && closeTarget !== void 0 && closeTarget.disabled !== true && probeVisible(closeTarget)) {
|
|
88
|
+
closeTarget.click();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (openTarget === null || openTarget === void 0 || openTarget.disabled === true) return false;
|
|
92
|
+
openTarget.click();
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/** 官方设置触发器锚点链(v2 调研点① 结论:DSH 面板 open 状态由 shell 私有,
|
|
96
|
+
* 公开语义入口 = sidebar.settings 槽的触发按钮——SettingsRoot.tsx onClick
|
|
97
|
+
* setOpen(true),footer 触发器类后缀 `_trigger`、无 aria。 */
|
|
98
|
+
const SETTINGS_ANCHORS = [
|
|
99
|
+
"[class$=\"_trigger\"]",
|
|
100
|
+
"button[aria-label=\"设置\"]",
|
|
101
|
+
"button[aria-label=\"Settings\"]",
|
|
102
|
+
"[role=\"button\"][aria-label=\"设置\"]",
|
|
103
|
+
"[role=\"button\"][aria-label=\"Settings\"]",
|
|
104
|
+
"button[title=\"设置\"]",
|
|
105
|
+
"button[title=\"Settings\"]"
|
|
106
|
+
];
|
|
107
|
+
/** 官方设置关闭目标锚点链(面板开着时二次点击应关闭——trigger onClick 只
|
|
108
|
+
* setOpen(true) 原生不 toggle,2026-08-30 用户实测「再点关闭失败」)。 */
|
|
109
|
+
const SETTINGS_CLOSE_ANCHORS = [
|
|
110
|
+
"button[class$=\"_close\"]",
|
|
111
|
+
"button[aria-label=\"关闭\"]",
|
|
112
|
+
"button[title=\"关闭\"]"
|
|
113
|
+
];
|
|
114
|
+
/** 设置面板打开判定(modal mask;SettingsRoot.tsx:mask div onClick=onClose)。 */
|
|
115
|
+
const SETTINGS_MASK_SELECTOR = "[class$=\"_mask\"]";
|
|
116
|
+
/** 打开/关闭官方设置面板:开着(mask 存在)→ 关闭(close 按钮 → mask 兜底);
|
|
117
|
+
* 关着 → 文本语义定位优先(footer trigger)→ 锚点链兜底。 */
|
|
118
|
+
function actOpenSettings(env) {
|
|
119
|
+
const mask = env.find(SETTINGS_MASK_SELECTOR);
|
|
120
|
+
if (mask !== null && mask !== void 0) {
|
|
121
|
+
for (let i = 0; i < SETTINGS_CLOSE_ANCHORS.length; i++) {
|
|
122
|
+
const closeBtn = env.find(SETTINGS_CLOSE_ANCHORS[i]);
|
|
123
|
+
if (closeBtn !== null && closeBtn !== void 0 && closeBtn.disabled !== true) {
|
|
124
|
+
closeBtn.click();
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
mask.click();
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (env.findByText !== void 0) {
|
|
132
|
+
const byText = env.findByText(["设置", "Settings"]);
|
|
133
|
+
if (byText !== null && byText !== void 0 && byText.disabled !== true) {
|
|
134
|
+
byText.click();
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (let i = 0; i < SETTINGS_ANCHORS.length; i++) {
|
|
139
|
+
const target = env.find(SETTINGS_ANCHORS[i]);
|
|
140
|
+
if (target !== null && target !== void 0 && target.disabled !== true) {
|
|
141
|
+
target.click();
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
/** 触发 dsh-commands 文本命令(空白名防御;执行语义由环境实现——v2 调研点②)。 */
|
|
148
|
+
function actCommand(env, name) {
|
|
149
|
+
if (name === void 0 || name === null || name.trim() === "") return false;
|
|
150
|
+
return env.execute(name.trim());
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/engine.ts
|
|
154
|
+
/**
|
|
155
|
+
* @max-null/dsh-quick-toolbar — 适配器执行器(DOM 绑定层)
|
|
156
|
+
*
|
|
157
|
+
* 引擎运行时:按适配器定位目标元素 → 调行为库 → 防御执行。
|
|
158
|
+
* 行为库保持窄接口(可测);本层唯一负责 DOM 绑定(browser 环境,
|
|
159
|
+
* 不在 node:test 的范围里——DOM 交互留给 L2 环境验证)。
|
|
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);
|
|
185
|
+
}
|
|
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(点击原按钮)。 */
|
|
204
|
+
function runAdapter(adapter, env) {
|
|
205
|
+
const act = adapter.act ?? { kind: "click" };
|
|
206
|
+
switch (act.kind) {
|
|
207
|
+
case "click": return actClick(env.find(adapter.button));
|
|
208
|
+
case "dispatch-event": return env.dispatch(act.event, act.detail);
|
|
209
|
+
case "toggle-panel": {
|
|
210
|
+
const closeEl = closeTargetOf(act, env);
|
|
211
|
+
return actTogglePanel(env.find(adapter.button), closeEl, env.isVisible ?? defaultIsVisible);
|
|
212
|
+
}
|
|
213
|
+
case "open-settings":
|
|
214
|
+
if (act.path !== void 0) console.warn(`quick-toolbar: open-settings path '${act.path}' 暂不支持(v2 深链待入)`);
|
|
215
|
+
return actOpenSettings({
|
|
216
|
+
find: env.find,
|
|
217
|
+
...env.findByText !== void 0 ? { findByText: env.findByText } : {}
|
|
218
|
+
});
|
|
219
|
+
case "command":
|
|
220
|
+
if (env.runCommand === void 0) {
|
|
221
|
+
console.warn("quick-toolbar: command 环境无 runCommand 通道(旧版 DSH/未注入)");
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return actCommand({ execute: env.runCommand }, act.name);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
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
|
|
57
328
|
//#region src/client.ts
|
|
58
329
|
/**
|
|
59
330
|
* @max-null/dsh-quick-toolbar — browser half.
|
|
@@ -162,6 +433,8 @@ window.__ModuleLoader__.load({
|
|
|
162
433
|
"tb.sidebar": ["侧栏", "Sidebar"],
|
|
163
434
|
"tb.bottom": ["底栏", "Bottom panel"],
|
|
164
435
|
"tb.sessions": ["会话管理", "Sessions"],
|
|
436
|
+
"tb.add": ["添加按钮", "Add button"],
|
|
437
|
+
"tb.addAria": ["添加/迁移按钮(让 LLM 来注册)", "Add / migrate a button (let the LLM register it)"],
|
|
165
438
|
"sm.open": ["会话管理", "Sessions"],
|
|
166
439
|
"sm.openTitle": ["打开会话管理面板", "Open session manager"]
|
|
167
440
|
};
|
|
@@ -258,9 +531,40 @@ window.__ModuleLoader__.load({
|
|
|
258
531
|
applyLocale();
|
|
259
532
|
}
|
|
260
533
|
var TOOLBAR_ID = "ssid-toolbar";
|
|
261
|
-
var
|
|
262
|
-
|
|
263
|
-
|
|
534
|
+
var qtState = {
|
|
535
|
+
pos: null,
|
|
536
|
+
collapsed: true,
|
|
537
|
+
pinned: false,
|
|
538
|
+
shellVisible: false
|
|
539
|
+
};
|
|
540
|
+
var loadState = function(done) {
|
|
541
|
+
fetch("/quick-toolbar/api/state").then(function(r) {
|
|
542
|
+
return r.json();
|
|
543
|
+
}).then(function(data) {
|
|
544
|
+
var s = data !== null && typeof data === "object" && data.ok === true ? data.state : void 0;
|
|
545
|
+
if (s !== void 0 && s !== null) {
|
|
546
|
+
if (s.pos !== null && s.pos !== void 0 && typeof s.pos.x === "number" && typeof s.pos.y === "number") qtState.pos = {
|
|
547
|
+
x: s.pos.x,
|
|
548
|
+
y: s.pos.y
|
|
549
|
+
};
|
|
550
|
+
if (typeof s.collapsed === "boolean") qtState.collapsed = s.collapsed;
|
|
551
|
+
if (typeof s.pinned === "boolean") qtState.pinned = s.pinned;
|
|
552
|
+
if (typeof s.shellVisible === "boolean") qtState.shellVisible = s.shellVisible;
|
|
553
|
+
}
|
|
554
|
+
done();
|
|
555
|
+
}).catch(function() {
|
|
556
|
+
done();
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
var saveState = function() {
|
|
560
|
+
try {
|
|
561
|
+
fetch("/quick-toolbar/api/state", {
|
|
562
|
+
method: "POST",
|
|
563
|
+
headers: { "Content-Type": "application/json" },
|
|
564
|
+
body: JSON.stringify(qtState)
|
|
565
|
+
}).catch(function() {});
|
|
566
|
+
} catch (_e) {}
|
|
567
|
+
};
|
|
264
568
|
var TOOLBAR_CSS = [
|
|
265
569
|
"#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
570
|
"#ssid-toolbar *{box-sizing:border-box}",
|
|
@@ -277,7 +581,15 @@ window.__ModuleLoader__.load({
|
|
|
277
581
|
"#ssid-toolbar .ssid-tb-pin svg{width:15px;height:15px}",
|
|
278
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}",
|
|
279
583
|
"#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)}"
|
|
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}"
|
|
281
593
|
].join("\n");
|
|
282
594
|
function toolbarIcon(name) {
|
|
283
595
|
var ICONS = {
|
|
@@ -288,10 +600,70 @@ window.__ModuleLoader__.load({
|
|
|
288
600
|
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
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>",
|
|
290
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>",
|
|
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>"
|
|
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>",
|
|
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>"
|
|
292
606
|
};
|
|
293
607
|
return ICONS[name] || ICONS.grid;
|
|
294
608
|
}
|
|
609
|
+
var toolbarEnv = function() {
|
|
610
|
+
return {
|
|
611
|
+
find: function(s) {
|
|
612
|
+
return document.querySelector(s);
|
|
613
|
+
},
|
|
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;
|
|
628
|
+
},
|
|
629
|
+
dispatch: function(event, detail) {
|
|
630
|
+
window.dispatchEvent(new CustomEvent(event, { detail }));
|
|
631
|
+
return true;
|
|
632
|
+
},
|
|
633
|
+
findByText: function(texts) {
|
|
634
|
+
var buttons = document.querySelectorAll("button");
|
|
635
|
+
for (var bi = 0; bi < buttons.length; bi++) {
|
|
636
|
+
var label = (buttons[bi].textContent || "").trim();
|
|
637
|
+
for (var ti = 0; ti < texts.length; ti++) if (label === texts[ti]) return buttons[bi];
|
|
638
|
+
}
|
|
639
|
+
return null;
|
|
640
|
+
},
|
|
641
|
+
runCommand: function(name) {
|
|
642
|
+
return typeCommandIntoComposer(name);
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
};
|
|
646
|
+
function injectComposerDraft(text) {
|
|
647
|
+
var seat = document.querySelector("[data-composer-seat]");
|
|
648
|
+
var el = seat !== null && seat !== void 0 ? seat.querySelector("textarea, [contenteditable=\"true\"]") : document.querySelector("textarea, [contenteditable=\"true\"]");
|
|
649
|
+
if (el === null || el === void 0) return false;
|
|
650
|
+
if (el instanceof HTMLTextAreaElement) {
|
|
651
|
+
var setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
|
|
652
|
+
if (setter !== void 0 && setter.set !== void 0) setter.set.call(el, text);
|
|
653
|
+
else el.value = text;
|
|
654
|
+
} else if (el.isContentEditable) el.textContent = text;
|
|
655
|
+
else return false;
|
|
656
|
+
el.dispatchEvent(new InputEvent("input", {
|
|
657
|
+
bubbles: true,
|
|
658
|
+
inputType: "insertText",
|
|
659
|
+
data: text
|
|
660
|
+
}));
|
|
661
|
+
el.focus();
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
function typeCommandIntoComposer(name) {
|
|
665
|
+
return injectComposerDraft("/" + name);
|
|
666
|
+
}
|
|
295
667
|
function toolbarAction(kind) {
|
|
296
668
|
if (kind === "plugin") {
|
|
297
669
|
closeSidePanelsBeforePluginCenter();
|
|
@@ -317,8 +689,9 @@ window.__ModuleLoader__.load({
|
|
|
317
689
|
}
|
|
318
690
|
}
|
|
319
691
|
function createToolbar() {
|
|
320
|
-
if (win.__SSID_SHELL__ === true) return;
|
|
692
|
+
if (win.__SSID_SHELL__ === true && !qtState.shellVisible) return;
|
|
321
693
|
if (document.getElementById(TOOLBAR_ID) !== null) return;
|
|
694
|
+
var addBtn = null;
|
|
322
695
|
var root = document.createElement("div");
|
|
323
696
|
root.id = TOOLBAR_ID;
|
|
324
697
|
var panel = document.createElement("div");
|
|
@@ -341,33 +714,184 @@ window.__ModuleLoader__.load({
|
|
|
341
714
|
"dsh-better-sidebar.bottom": "bottom",
|
|
342
715
|
"dsh-session-manager": "sessions"
|
|
343
716
|
};
|
|
344
|
-
var
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
adapter
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
717
|
+
var adapterIconHtml = function(adapter, kind) {
|
|
718
|
+
if (adapter.icon !== void 0 && adapter.icon.source === "custom") {
|
|
719
|
+
var value = adapter.icon.value;
|
|
720
|
+
if (value.indexOf("<") === 0) return value;
|
|
721
|
+
return toolbarIcon(value);
|
|
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) {}
|
|
734
|
+
return kind !== null ? toolbarIcon(kind) : toolbarIcon("grid");
|
|
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
|
+
};
|
|
744
|
+
var renderButton = function(adapter, kind) {
|
|
355
745
|
var b = document.createElement("button");
|
|
356
746
|
b.type = "button";
|
|
357
747
|
b.className = "ssid-tb-btn";
|
|
358
|
-
b.
|
|
359
|
-
b.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
b.
|
|
748
|
+
b.setAttribute("data-adapter-id", adapter.id);
|
|
749
|
+
b.innerHTML = adapterIconHtml(adapter, kind) + "<span></span>";
|
|
750
|
+
if (kind !== null) {
|
|
751
|
+
b.setAttribute("aria-label", "");
|
|
752
|
+
b.title = "";
|
|
753
|
+
trackLocale(b, "tb." + kind, "text-span");
|
|
754
|
+
trackLocale(b, "tb." + kind, "aria");
|
|
755
|
+
trackLocale(b, "tb." + kind, "title");
|
|
756
|
+
} else {
|
|
757
|
+
var label = adapterLabel(adapter);
|
|
758
|
+
var textSpan = b.querySelector("span");
|
|
759
|
+
if (textSpan !== null) textSpan.textContent = label;
|
|
760
|
+
b.setAttribute("aria-label", label);
|
|
761
|
+
b.title = label;
|
|
762
|
+
}
|
|
763
|
+
b.addEventListener("click", function() {
|
|
764
|
+
if (kind !== null) {
|
|
366
765
|
toolbarAction(kind);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
runAdapter(adapter, toolbarEnv());
|
|
769
|
+
});
|
|
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;
|
|
777
|
+
};
|
|
778
|
+
for (var ai = 0; ai < BUILTIN_ADAPTERS.length; ai++) {
|
|
779
|
+
var adapter = BUILTIN_ADAPTERS[ai];
|
|
780
|
+
var kind = TOOLBAR_KIND_BY_ADAPTER[adapter.id];
|
|
781
|
+
renderButton(adapter, kind !== void 0 ? kind : null);
|
|
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;
|
|
367
801
|
});
|
|
368
|
-
|
|
369
|
-
|
|
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() {});
|
|
370
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
|
+
}
|
|
855
|
+
var fetchUserAdapters = function() {
|
|
856
|
+
fetch("/quick-toolbar/api/adapters").then(function(r) {
|
|
857
|
+
return r.json();
|
|
858
|
+
}).then(function(data) {
|
|
859
|
+
var envelope = data !== null && typeof data === "object" ? data : void 0;
|
|
860
|
+
var rows = envelope !== void 0 && envelope.ok === true && envelope.value !== null && typeof envelope.value === "object" ? envelope.value.adapters : void 0;
|
|
861
|
+
if (!Array.isArray(rows)) return;
|
|
862
|
+
for (var ui = 0; ui < rows.length; ui++) {
|
|
863
|
+
var user = rows[ui];
|
|
864
|
+
if (user === null || typeof user !== "object") continue;
|
|
865
|
+
var userKind = TOOLBAR_KIND_BY_ADAPTER[user.id];
|
|
866
|
+
try {
|
|
867
|
+
var old = panel.querySelector("[data-adapter-id=\"" + user.id.replace(/"/g, "\\\"") + "\"]");
|
|
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);
|
|
871
|
+
} catch (_e) {}
|
|
872
|
+
wrapAdapterRow(renderButton(user, userKind !== void 0 ? userKind : null), user);
|
|
873
|
+
}
|
|
874
|
+
}).catch(function() {});
|
|
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);
|
|
371
895
|
var ball = document.createElement("button");
|
|
372
896
|
ball.type = "button";
|
|
373
897
|
ball.id = TOOLBAR_ID + "-ball";
|
|
@@ -384,13 +908,10 @@ window.__ModuleLoader__.load({
|
|
|
384
908
|
var BALL_R = 18;
|
|
385
909
|
var expanded = false;
|
|
386
910
|
var ballX = 0, ballY = 0;
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
ballY = savedPos.y;
|
|
392
|
-
}
|
|
393
|
-
} catch (_e) {}
|
|
911
|
+
if (qtState.pos !== null) {
|
|
912
|
+
ballX = qtState.pos.x;
|
|
913
|
+
ballY = qtState.pos.y;
|
|
914
|
+
}
|
|
394
915
|
if (ballX === 0 && ballY === 0) {
|
|
395
916
|
ballX = window.innerWidth - BALL_SIZE - 16;
|
|
396
917
|
ballY = window.innerHeight - BALL_SIZE - 16;
|
|
@@ -475,14 +996,22 @@ window.__ModuleLoader__.load({
|
|
|
475
996
|
}
|
|
476
997
|
var kids = panel.children;
|
|
477
998
|
for (var ki = 0; ki < kids.length; ki++) kids[ki].style.transitionDelay = collapsed ? "0ms" : 40 + ki * 24 + "ms";
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
} catch (_e) {}
|
|
999
|
+
qtState.collapsed = collapsed;
|
|
1000
|
+
saveState();
|
|
481
1001
|
};
|
|
482
|
-
var pinned = false;
|
|
483
1002
|
try {
|
|
484
|
-
|
|
1003
|
+
new ResizeObserver(function() {
|
|
1004
|
+
if (!expanded) return;
|
|
1005
|
+
var w2 = panel.offsetWidth + 2;
|
|
1006
|
+
var h2 = panel.offsetHeight + 2;
|
|
1007
|
+
if (root.offsetWidth !== w2 || root.offsetHeight !== h2) {
|
|
1008
|
+
root.style.width = w2 + "px";
|
|
1009
|
+
root.style.height = h2 + "px";
|
|
1010
|
+
}
|
|
1011
|
+
}).observe(panel);
|
|
485
1012
|
} catch (_e) {}
|
|
1013
|
+
var pinned = false;
|
|
1014
|
+
pinned = qtState.pinned;
|
|
486
1015
|
var applyPin = function() {
|
|
487
1016
|
pinBtn.style.color = pinned ? "var(--dsw-alias-interactive-accent, #4d9fff)" : "";
|
|
488
1017
|
pinBtn.style.opacity = pinned ? "1" : "";
|
|
@@ -500,11 +1029,11 @@ window.__ModuleLoader__.load({
|
|
|
500
1029
|
setCollapsed(pinned ? false : true);
|
|
501
1030
|
applyPin();
|
|
502
1031
|
applyLocale();
|
|
1032
|
+
fetchUserAdapters();
|
|
503
1033
|
pinBtn.addEventListener("click", function() {
|
|
504
1034
|
pinned = !pinned;
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
} catch (_e) {}
|
|
1035
|
+
qtState.pinned = pinned;
|
|
1036
|
+
saveState();
|
|
508
1037
|
applyPin();
|
|
509
1038
|
setCollapsed(pinned ? false : true);
|
|
510
1039
|
});
|
|
@@ -575,21 +1104,81 @@ window.__ModuleLoader__.load({
|
|
|
575
1104
|
ballY = Math.max(4, Math.min(Math.round(sp.by), vh - BALL_SIZE - 4));
|
|
576
1105
|
ball.style.left = ballX + "px";
|
|
577
1106
|
ball.style.top = ballY + "px";
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
} catch (_e) {}
|
|
1107
|
+
qtState.pos = {
|
|
1108
|
+
x: ballX,
|
|
1109
|
+
y: ballY
|
|
1110
|
+
};
|
|
1111
|
+
saveState();
|
|
584
1112
|
};
|
|
585
1113
|
document.addEventListener("mousemove", onMove);
|
|
586
1114
|
document.addEventListener("mouseup", onUp);
|
|
587
1115
|
});
|
|
588
1116
|
}
|
|
589
|
-
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
|
+
}
|
|
590
1175
|
exports.apply = function(ctx) {
|
|
591
1176
|
if (win.__dshQuickToolbarInstalled === true) return;
|
|
592
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;
|
|
593
1182
|
var style = document.createElement("style");
|
|
594
1183
|
style.setAttribute("data-dsh-quick-toolbar", "");
|
|
595
1184
|
style.textContent = BASE_CSS + (SHELL_CSS.length > 0 && win.__SSID_SHELL__ === true ? "\n" + SHELL_CSS.join("\n") : "");
|
|
@@ -598,16 +1187,25 @@ window.__ModuleLoader__.load({
|
|
|
598
1187
|
tbStyle.setAttribute("data-dsh-quick-toolbar-toolbar", "");
|
|
599
1188
|
tbStyle.textContent = TOOLBAR_CSS;
|
|
600
1189
|
document.head.appendChild(tbStyle);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
1190
|
+
loadState(function() {
|
|
1191
|
+
if (win.__SSID_SHELL__ === true && !qtState.shellVisible) return;
|
|
1192
|
+
createToolbar();
|
|
1193
|
+
});
|
|
1194
|
+
var hideIfShell = function() {
|
|
1195
|
+
if (win.__SSID_SHELL__ !== true) return false;
|
|
1196
|
+
if (qtState.shellVisible) return false;
|
|
604
1197
|
var tb = document.getElementById(TOOLBAR_ID);
|
|
605
1198
|
if (tb !== null) tb.remove();
|
|
606
1199
|
var tbBall = document.getElementById(TOOLBAR_ID + "-ball");
|
|
607
1200
|
if (tbBall !== null) tbBall.remove();
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1201
|
+
return true;
|
|
1202
|
+
};
|
|
1203
|
+
window.addEventListener("load", hideIfShell);
|
|
1204
|
+
var shellTries = 0;
|
|
1205
|
+
var shellTimer = setInterval(function() {
|
|
1206
|
+
shellTries++;
|
|
1207
|
+
if (hideIfShell() || shellTries >= 5) clearInterval(shellTimer);
|
|
1208
|
+
}, 1500);
|
|
611
1209
|
mountSmHeaderButton();
|
|
612
1210
|
new MutationObserver(function() {
|
|
613
1211
|
applyLocale();
|
|
@@ -621,6 +1219,19 @@ window.__ModuleLoader__.load({
|
|
|
621
1219
|
});
|
|
622
1220
|
window.addEventListener("ssid:titlebar", function(event) {
|
|
623
1221
|
var detail = event !== null && typeof event === "object" ? event.detail : void 0;
|
|
1222
|
+
if (detail === "quick-toolbar-toggle") {
|
|
1223
|
+
var nextOn = !qtState.shellVisible;
|
|
1224
|
+
qtState.shellVisible = nextOn;
|
|
1225
|
+
saveState();
|
|
1226
|
+
if (nextOn) createToolbar();
|
|
1227
|
+
else {
|
|
1228
|
+
var tbEl = document.getElementById(TOOLBAR_ID);
|
|
1229
|
+
if (tbEl !== null) tbEl.remove();
|
|
1230
|
+
var tbBallEl = document.getElementById(TOOLBAR_ID + "-ball");
|
|
1231
|
+
if (tbBallEl !== null) tbBallEl.remove();
|
|
1232
|
+
}
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
624
1235
|
if (detail === "session-manager") {
|
|
625
1236
|
clickSmOpenButton();
|
|
626
1237
|
return;
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
3
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
//#region src/schema.ts
|
|
6
6
|
/**
|
|
@@ -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,39 +93,179 @@ const adaptersRouteDefinition = {
|
|
|
85
93
|
kind: "exact",
|
|
86
94
|
path: ROUTE_PATH,
|
|
87
95
|
handler: async (req, res) => {
|
|
88
|
-
if (req.method
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (req.method === "POST") {
|
|
120
|
+
let raw = "";
|
|
121
|
+
req.on?.("data", (chunk) => {
|
|
122
|
+
raw += chunk;
|
|
92
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 {
|
|
145
|
+
sendJson(res, 200, {
|
|
146
|
+
ok: false,
|
|
147
|
+
error: "write-failed"
|
|
148
|
+
});
|
|
149
|
+
}
|
|
93
150
|
return;
|
|
94
151
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
152
|
+
sendJson(res, 405, {
|
|
153
|
+
ok: false,
|
|
154
|
+
error: "method-not-allowed"
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const STATE_PATH = join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "quick-toolbar-state.json");
|
|
159
|
+
const STATE_ROUTE = "/quick-toolbar/api/state";
|
|
160
|
+
/** 默认状态(文件缺失/字段缺失时取默认,宽松向下兼容)。 */
|
|
161
|
+
function defaultState() {
|
|
162
|
+
return {
|
|
163
|
+
pos: null,
|
|
164
|
+
collapsed: true,
|
|
165
|
+
pinned: false,
|
|
166
|
+
shellVisible: false
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** 归一化(防御非法文件:字段级回退默认,不因一条坏字段全丢)。 */
|
|
170
|
+
function normalizeState(raw) {
|
|
171
|
+
const d = defaultState();
|
|
172
|
+
const s = raw !== null && typeof raw === "object" ? raw : {};
|
|
173
|
+
const pos = s.pos;
|
|
174
|
+
if (pos !== null && pos !== void 0 && typeof pos === "object" && pos !== null) {
|
|
175
|
+
const p = pos;
|
|
176
|
+
if (typeof p.x === "number" && typeof p.y === "number") d.pos = {
|
|
177
|
+
x: p.x,
|
|
178
|
+
y: p.y
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (typeof s.collapsed === "boolean") d.collapsed = s.collapsed;
|
|
182
|
+
if (typeof s.pinned === "boolean") d.pinned = s.pinned;
|
|
183
|
+
if (typeof s.shellVisible === "boolean") d.shellVisible = s.shellVisible;
|
|
184
|
+
return d;
|
|
185
|
+
}
|
|
186
|
+
/** 读状态文件(缺失/非法 → 默认;不抛)。 */
|
|
187
|
+
function readStateFile() {
|
|
188
|
+
try {
|
|
189
|
+
return normalizeState(JSON.parse(readFileSync(STATE_PATH, "utf8")));
|
|
190
|
+
} catch {
|
|
191
|
+
return defaultState();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** 原子写状态文件(tmp + rename,防中断半写)。 */
|
|
195
|
+
function writeStateFile(state) {
|
|
196
|
+
const tmp = STATE_PATH + ".tmp";
|
|
197
|
+
writeFileSync(tmp, JSON.stringify(state));
|
|
198
|
+
renameSync(tmp, STATE_PATH);
|
|
199
|
+
}
|
|
200
|
+
const stateRouteDefinition = {
|
|
201
|
+
kind: "exact",
|
|
202
|
+
path: STATE_ROUTE,
|
|
203
|
+
handler: async (req, res) => {
|
|
204
|
+
if (req.method === "GET") {
|
|
205
|
+
sendJson(res, 200, {
|
|
206
|
+
ok: true,
|
|
207
|
+
state: readStateFile()
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (req.method === "POST") {
|
|
212
|
+
let raw = "";
|
|
213
|
+
req.on("data", (chunk) => {
|
|
214
|
+
raw += chunk;
|
|
215
|
+
});
|
|
216
|
+
await new Promise((resolve) => req.on("end", () => {
|
|
217
|
+
resolve();
|
|
218
|
+
}));
|
|
219
|
+
try {
|
|
220
|
+
writeStateFile(normalizeState(JSON.parse(raw)));
|
|
221
|
+
sendJson(res, 200, { ok: true });
|
|
222
|
+
} catch {
|
|
98
223
|
sendJson(res, 200, {
|
|
99
224
|
ok: false,
|
|
100
|
-
error: "invalid-
|
|
101
|
-
detail: parsed.issues
|
|
225
|
+
error: "invalid-state"
|
|
102
226
|
});
|
|
103
|
-
return;
|
|
104
227
|
}
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
sendJson(res, 405, {
|
|
231
|
+
ok: false,
|
|
232
|
+
error: "method-not-allowed"
|
|
233
|
+
});
|
|
234
|
+
}
|
|
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 {
|
|
105
250
|
sendJson(res, 200, {
|
|
106
251
|
ok: true,
|
|
107
|
-
|
|
252
|
+
url: w.connection.authenticatedUrl("http://" + w.webServer.host + ":" + String(w.webServer.port) + "/")
|
|
108
253
|
});
|
|
109
254
|
} catch {
|
|
110
255
|
sendJson(res, 200, {
|
|
111
|
-
ok:
|
|
112
|
-
|
|
256
|
+
ok: false,
|
|
257
|
+
error: "auth-url-unavailable"
|
|
113
258
|
});
|
|
114
259
|
}
|
|
115
260
|
}
|
|
116
261
|
};
|
|
117
262
|
function apply(ctx) {
|
|
118
|
-
ctx.inject(["webServer"], ((wsCtx) => {
|
|
263
|
+
ctx.inject(["webServer", "connection"], ((wsCtx) => {
|
|
264
|
+
wsSvc = wsCtx;
|
|
119
265
|
wsCtx.webServer.register(adaptersRouteDefinition);
|
|
266
|
+
wsCtx.webServer.register(stateRouteDefinition);
|
|
267
|
+
wsCtx.webServer.register(authUrlRouteDefinition);
|
|
120
268
|
}));
|
|
121
269
|
}
|
|
122
270
|
//#endregion
|
|
123
|
-
export { apply, name };
|
|
271
|
+
export { apply, defaultState, name, normalizeState, readStateFile, writeStateFile };
|
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": {
|