dsh-dock 0.9.6 → 0.9.8

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.
Files changed (35) hide show
  1. package/LICENSE +18 -18
  2. package/README.md +325 -325
  3. package/client.js +42 -8
  4. package/cordis.patch.yml +2 -2
  5. package/features/animation/host.js +660 -660
  6. package/features/animation/view.jsx +2247 -2247
  7. package/features/balance/host.js +494 -494
  8. package/features/balance/view.js +351 -351
  9. package/features/games/games/breakout.jsx +227 -227
  10. package/features/games/games/gomoku.jsx +244 -244
  11. package/features/games/games/racer.jsx +169 -169
  12. package/features/games/games/shared.jsx +118 -118
  13. package/features/games/games/snake.jsx +144 -144
  14. package/features/games/games/sokoban.jsx +180 -180
  15. package/features/games/games/tank.jsx +128 -128
  16. package/features/games/games/tetris.jsx +308 -308
  17. package/features/games/games/xiangqi.jsx +277 -277
  18. package/features/games/tank-core.js +152 -152
  19. package/features/games/view.jsx +639 -639
  20. package/features/games/xiangqi-core.js +299 -299
  21. package/features/heartbeat/view.js +35 -35
  22. package/features/mobile-relay/gateway.js +472 -472
  23. package/features/mobile-relay/host.js +570 -569
  24. package/features/mobile-relay/view.jsx +300 -300
  25. package/features/modelconfig/host.js +403 -403
  26. package/features/modelconfig/view.js +490 -490
  27. package/features/theme/view.js +61 -61
  28. package/features/tokenlog/host.js +690 -678
  29. package/features/tokenlog/view.jsx +561 -561
  30. package/features/visionproxy/host.js +315 -315
  31. package/index.js +175 -118
  32. package/package.json +59 -59
  33. package/src/client.jsx +668 -668
  34. package/src/host-core.js +98 -95
  35. package/src/shared.js +167 -130
package/src/client.jsx CHANGED
@@ -1,668 +1,668 @@
1
- // dsh-dock · Client 半部(浏览器外壳,源码入口;构建为根目录 client.js)
2
- //
3
- // v0.4.0 模块化架构:外壳只负责面板本身(入口按钮/居中弹窗/设置页/首页总揽),
4
- // 每个功能是一个 features/<id>/view.js(x) 模块(自带描述符 + 视图 + 样式),在此 import 组装。
5
- // 菜单次序由各模块 order 字段决定(首页固定第一,tokenlog=10 → 第二个菜单)。
6
- //
7
- // 外部功能桥(dockBridge):独立发布的功能包(scripts/extract-feature.mjs 生成的骨架)
8
- // 在浏览器端 require("dsh-dock").dockBridge.register(def) 把自己的视图注册进本面板;
9
- // dock 未安装时该包走自己的独立面板——「单独成包发布」与「完美集成功能坞」两全。
10
- //
11
- // 面板挂载在 settings.section:设置 → 功能坞;侧栏底部另有「功能坞」入口按钮
12
- // (sidebar.footer.action),点击弹出居中功能面板(shell.overlay)。
13
- // 弹层窗口:默认大窗,支持最大化/最小化、标题栏拖动、右下角缩放(几何页内记忆)。
14
- import react from "react";
15
- import {
16
- FeatureBoundary,
17
- initFeatureState, stateOf, toggleFeature, subscribeFeatureState,
18
- chipShown, setChipShown,
19
- panelNav, openPanel, setPanelOpen, navigatePanel, subscribePanel,
20
- } from "./shared.js";
21
- import { feature as fTokenlog } from "../features/tokenlog/view.jsx";
22
- import { feature as fModelconfig } from "../features/modelconfig/view.js";
23
- import { feature as fHeartbeat } from "../features/heartbeat/view.js";
24
- import { feature as fTheme } from "../features/theme/view.js";
25
- import { feature as fBalance } from "../features/balance/view.js";
26
- import { feature as fAnimation } from "../features/animation/view.jsx";
27
- import { feature as fGames } from "../features/games/view.jsx";
28
- import { feature as fMobileRelay } from "../features/mobile-relay/view.jsx";
29
-
30
- const name = "dsh-dock";
31
- const DOCK_VERSION = "0.9.5";
32
-
33
- // ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
34
- const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation, fGames, fMobileRelay];
35
- // 规划占位(路线图):接入后移除并建 features/<id>/ 模块
36
- const PLANNED_FEATURES = [];
37
- const PLANNED_NOTES = {};
38
-
39
- // ---- 外部功能注册表(dockBridge 回装通道) ----
40
- const externalDefs = [];
41
- const externalListeners = new Set();
42
- function notifyExternal() {
43
- for (const fn of externalListeners) fn();
44
- }
45
- /**
46
- * 外部功能桥:独立功能包经 require("dsh-dock").dockBridge 注册视图。
47
- * def: { id, name, order?, accent?, description?, css?, View, HomeStat?, Overlay?, package? }
48
- * 返回注销函数。重复 id 视为更新(HMR/重注册场景)。
49
- */
50
- const dockBridge = {
51
- version: DOCK_VERSION,
52
- register(def) {
53
- if (!def || typeof def !== "object" || typeof def.id !== "string" || !def.id || typeof def.View !== "function") {
54
- console.error("[dsh-dock] dockBridge.register 参数不符(需 { id, name, View }):", def);
55
- return () => {};
56
- }
57
- const entry = {
58
- id: def.id,
59
- name: String(def.name || def.id),
60
- order: typeof def.order === "number" ? def.order : 500,
61
- accent: typeof def.accent === "string" ? def.accent : "#94a3b8",
62
- description: String(def.description || ""),
63
- css: typeof def.css === "string" ? def.css : "",
64
- View: def.View,
65
- HomeStat: typeof def.HomeStat === "function" ? def.HomeStat : null,
66
- Overlay: typeof def.Overlay === "function" ? def.Overlay : null,
67
- external: true,
68
- package: typeof def.package === "string" ? def.package : "",
69
- };
70
- const i = externalDefs.findIndex((x) => x.id === entry.id);
71
- if (i >= 0) externalDefs[i] = entry;
72
- else externalDefs.push(entry);
73
- if (entry.css) ensureCss(); // 外部样式后到:重算并更新已注入的 <style>
74
- notifyExternal();
75
- return () => {
76
- const j = externalDefs.indexOf(entry);
77
- if (j >= 0) { externalDefs.splice(j, 1); notifyExternal(); }
78
- };
79
- },
80
- features() {
81
- return externalDefs.map((x) => Object.assign({}, x));
82
- },
83
- };
84
-
85
- // ---- 菜单模块表(内置 + 外部,按 order 升序;同序稳定保持注册顺序) ----
86
- function allModules() {
87
- return BUILTIN_FEATURES.concat(PLANNED_FEATURES, externalDefs)
88
- .slice()
89
- .sort((a, b) => ((a.order || 500) - (b.order || 500)));
90
- }
91
-
92
- // ---- 开关状态与面板导航总线在 src/shared.js(会话区 chips 与面板共用,避免外壳↔模块循环依赖) ----
93
-
94
- // ---- 样式:外壳样式 + 各功能模块自带样式(css 字段)合并注入一个 <style> ----
95
- // ⚠️ 变量名不得用 CSS:浏览器存在全局 window.CSS 命名空间,bundle 任何作用域解析歧义
96
- // 都会把标识符解析成该全局对象(无 .join),曾导致插件应用失败、整页启动崩溃。
97
- const SHELL_CSS = [
98
- // ---- 主题桥接层(亮色适配的根基)----
99
- // dsh 宿主没有 --dsw-alias-accent,也没有 --dsw-alias-state-warning-primary(正确名是 state-warn-primary),
100
- // 旧写法全部落到暗色系回退值(浅蓝/琥珀),亮色白底下看不清。这里在 body 上按宿主主题
101
- // (body / body[data-ds-dark-theme])声明两套 dock 语义变量,各模块统一引用:
102
- // --dk-accent 主强调色(亮色用深蓝保证对比度,暗色用亮蓝)
103
- // --dk-warn 警告色(亮色用深琥珀,暗色用亮琥珀)
104
- // --dk-ok/--dk-err 成功/错误(亮色取宿主深色值,暗色保持宿主亮色值)
105
- // --dk-tdim 表格分隔线/弱边框(亮色用中性灰蓝、暗色用白透明)
106
- "body{--dk-accent:#2f6fed;--dk-accent-soft:color-mix(in srgb,#2f6fed 12%,transparent);--dk-warn:#b45309;--dk-ok:var(--dsw-alias-state-success-primary,#16a34a);--dk-err:var(--dsw-alias-state-error-primary,#dc2626);--dk-tdim:rgba(100,116,139,.22);--dk-hover-tint:rgba(47,111,237,.08);--dk-games-accent:#7c3aed;}",
107
- "body[data-ds-dark-theme]{--dk-accent:#4d9fff;--dk-accent-soft:color-mix(in srgb,#4d9fff 12%,transparent);--dk-warn:#fbbf24;--dk-ok:var(--dsw-alias-state-success-primary,#34d399);--dk-err:var(--dsw-alias-state-error-primary,#f87171);--dk-tdim:rgba(122,132,152,.14);--dk-hover-tint:rgba(80,110,180,.12);--dk-games-accent:#a78bfa;}",
108
- // 宿主语义变量兜底别名(部分模块引用了宿主不存在的名字,统一在此补齐,取值随主题自动切换)
109
- "body{--dsw-alias-accent:var(--dk-accent);--dsw-alias-accent-soft:var(--dk-accent-soft);--dsw-alias-state-warning-primary:var(--dk-warn);}",
110
- ".dock-root{display:flex;flex-direction:column;gap:12px;padding:4px 0;color:var(--dsw-alias-label-primary);font-size:13px;}",
111
- ".dock-intro{color:var(--dsw-alias-label-secondary);line-height:1.6;}",
112
- ".dock-card{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;}",
113
- ".dock-card-head{display:flex;align-items:center;gap:8px;}",
114
- ".dock-dot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-label-secondary);flex:none;}",
115
- ".dock-dot.on{background:var(--dsw-alias-state-success-primary);}",
116
- ".dock-dot.err{background:var(--dsw-alias-state-error-primary);}",
117
- ".dock-name{font-weight:600;flex:none;}",
118
- ".dock-desc{color:var(--dsw-alias-label-secondary);flex:1;}",
119
- ".dock-switch{flex:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:3px 10px;cursor:pointer;font-size:12px;}",
120
- ".dock-switch.on{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);}",
121
- ".dock-badge{flex:none;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:3px 10px;font-size:12px;}",
122
- ".dock-body{border-top:1px solid var(--dsw-alias-border-l1);padding-top:8px;color:var(--dsw-alias-label-secondary);display:flex;flex-direction:column;gap:4px;}",
123
- // 侧栏入口按钮(docke2- 前缀)+ 功能坞弹出面板(dockm- 前缀,仿 dsh 设置的居中模态:遮罩 + 对话框,左导航 + 右内容)
124
- // 侧栏入口按钮(docke2- 前缀):与 dsh 设置按钮(.VOzbGW_trigger)几何逐条对齐——
125
- // 宽栏同为整宽左对齐行按钮(高 42、margin 4/-2、padding 0 10px 0 8px、圆角 12),窄栏同为 36 圆钮居中,
126
- // 使两按钮上下成列、左右边缘完全对齐(此前右对齐/叠放都会错位)。
127
- ".docke2-btn{box-sizing:border-box;cursor:pointer;display:flex;align-items:center;gap:8px;border:none;background:transparent;color:var(--dsw-alias-label-primary);border-radius:12px;width:calc(100% + 4px);height:42px;margin:4px -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
128
- ".docke2-btn:hover{background:var(--dsw-alias-interactive-bg-hover);}",
129
- ".docke2-btn.docke2-on{color:var(--dk-accent);background:var(--dk-accent-soft);}",
130
- ".docke2-btn.docke2-rail{width:36px;height:36px;margin:8px 0 10px;padding:0;border-radius:50%;justify-content:center;gap:0;}",
131
- ".docke2-label{white-space:nowrap;overflow:hidden;}",
132
- // 遮罩固定用深色半透明:亮色主题下宿主 bg-layer-1 是纯白,白底遮罩会让弹窗与页面完全无层次
133
- ".dockm-backdrop{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:rgba(15,17,21,.45);backdrop-filter:blur(4px);pointer-events:auto;animation:dockm-fade .15s var(--ds-ease-in-out);}",
134
- "@keyframes dockm-fade{from{opacity:0}to{opacity:1}}",
135
- ".dockm-dialog{box-sizing:border-box;position:relative;width:min(1080px,calc(100vw - 32px));height:min(700px,calc(100vh - 32px));display:flex;flex-direction:column;border-radius:16px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 20px 64px rgb(0 0 0 / .32);overflow:hidden;animation:dockm-pop .18s var(--ds-ease-in-out);}",
136
- "@keyframes dockm-pop{from{opacity:0;transform:translateY(10px) scale(.985)}to{opacity:1;transform:none}}",
137
- ".dockm-head{flex:none;display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l1);cursor:move;user-select:none;touch-action:none;}",
138
- ".dockm-title{font-weight:600;font-size:14px;}",
139
- ".dockm-sub{color:var(--dsw-alias-label-tertiary);font-size:12px;}",
140
- ".dockm-ctrls{margin-left:auto;flex:none;display:flex;align-items:center;gap:2px;}",
141
- ".dockm-close{cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;font-size:14px;}",
142
- ".dockm-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
143
- ".dockm-win{cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;font-size:13px;}",
144
- ".dockm-win:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
145
- ".dockm-dialog.dockm-max{border-radius:12px;}",
146
- ".dockm-dialog.dockm-min{height:auto;min-height:0;}",
147
- ".dockm-dialog.dockm-min .dockm-body{display:none;}",
148
- ".dockm-resize{position:absolute;right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;z-index:3;touch-action:none;}",
149
- ".dockm-body{flex:1;min-height:0;display:flex;}",
150
- ".dockm-nav{flex:none;width:176px;display:flex;flex-direction:column;gap:2px;padding:10px 8px;overflow-y:auto;border-right:1px solid var(--dsw-alias-border-l1);}",
151
- ".dockm-nav-item{display:flex;align-items:center;gap:8px;padding:8px 10px;border:none;background:transparent;border-radius:10px;cursor:pointer;color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:13px;text-align:left;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
152
- ".dockm-nav-item:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
153
- ".dockm-nav-item.on{background:var(--dk-accent-soft);color:var(--dsw-alias-label-primary);font-weight:600;}",
154
- ".dockm-nav-item .dockm-badge{margin-left:auto;}",
155
- ".dockm-dot{width:8px;height:8px;border-radius:50%;flex:none;}",
156
- ".dockm-badge{flex:none;font-size:11px;border-radius:999px;padding:0 8px;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);}",
157
- ".dockm-content{flex:1;min-width:0;display:flex;flex-direction:column;gap:12px;padding:16px 18px;overflow-y:auto;}",
158
- ".dockm-content-head{display:flex;flex-direction:column;gap:4px;}",
159
- ".dockm-name{display:flex;align-items:center;gap:8px;font-weight:600;font-size:15px;}",
160
- ".dockm-desc{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;}",
161
- ".dockm-view{display:flex;flex-direction:column;gap:8px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;}",
162
- ".dockm-note{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;}",
163
- ".dockm-err{color:var(--dsw-alias-state-error-primary);}",
164
- ".dockm-foot{margin-top:auto;display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-tertiary);font-size:11px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;flex-wrap:wrap;}",
165
- ".dockm-switch{cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:4px 12px;font-family:inherit;font-size:12px;margin-left:auto;}",
166
- ".dockm-switch:hover{color:var(--dsw-alias-label-primary);}",
167
- ".dockm-switch.on{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
168
- // 首页总揽(dockh- 前缀):导航首项图标 + 模块卡片网格
169
- ".dockm-navhome{display:inline-flex;align-items:center;color:var(--dk-accent);flex:none;}",
170
- ".dockh-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px;}",
171
- ".dockh-card{display:flex;flex-direction:column;gap:6px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:10px 12px;cursor:pointer;transition:border-color .15s var(--ds-ease-in-out);}",
172
- ".dockh-card:hover,.dockh-card:focus-visible{border-color:var(--dk-accent);outline:none;}",
173
- ".dockh-head{display:flex;align-items:center;gap:8px;}",
174
- ".dockh-name{font-weight:600;flex:none;}",
175
- ".dockh-badge{flex:none;font-size:11px;border-radius:999px;padding:0 8px;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);}",
176
- // 状态标识(非交互:圆点+文字,与可点的开关一眼区分)
177
- ".dockh-status{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;}",
178
- ".dockh-sdot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--dsw-alias-state-success-primary,#34d399);box-shadow:0 0 4px color-mix(in srgb,var(--dsw-alias-state-success-primary,#34d399) 55%,transparent);}",
179
- ".dockh-status.off .dockh-sdot{background:transparent;border:1.5px solid var(--dsw-alias-label-tertiary);box-shadow:none;opacity:.75;}",
180
- ".dockh-status.plan .dockh-sdot{background:transparent;border:1.5px dashed var(--dsw-alias-label-tertiary);box-shadow:none;}",
181
- // iOS 风滑动开关(首页卡片 / 弹层页脚 / 设置页共用的启停控件)
182
- ".dock-sw{flex:none;position:relative;width:34px;height:19px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;padding:0;transition:background .18s var(--ds-ease-in-out),border-color .18s var(--ds-ease-in-out);}",
183
- ".dock-sw::after{content:\"\";position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgb(0 0 0 / .35);transition:transform .18s var(--ds-ease-in-out);}",
184
- ".dock-sw:hover{border-color:var(--dk-accent);}",
185
- ".dock-sw.on{background:var(--dsw-alias-state-success-primary,#34d399);border-color:transparent;}",
186
- ".dock-sw.on::after{transform:translateX(15px);}",
187
- // 弹层页脚:文字标签 + 开关 成组
188
- ".dockm-foot-sw{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:8px;}",
189
- ".dockm-foot-swlabel{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
190
- ".dockm-foot-swlabel.on{color:var(--dsw-alias-state-success-primary);}",
191
- // 窄屏:功能坞是一级工作台而不是被压扁的桌面弹窗。全屏承接安全区,导航改横向,
192
- // 禁用桌面拖拽/缩放,所有导航和关闭动作保持至少 44px 的可点击面积。
193
- "@media (max-width:680px){.dockm-backdrop{align-items:stretch;justify-content:stretch;background:var(--dsw-alias-bg-layer-2);backdrop-filter:none}.dockm-dialog,.dockm-dialog.dockm-max{position:fixed!important;left:0!important;top:0!important;right:auto!important;bottom:auto!important;width:100dvw!important;height:100dvh!important;min-height:100dvh;border:0;border-radius:0;box-shadow:none}.dockm-dialog.dockm-min .dockm-body{display:flex}.dockm-head{min-height:60px;box-sizing:border-box;padding:calc(env(safe-area-inset-top) + 8px) 12px 8px;cursor:default;touch-action:manipulation}.dockm-sub,.dockm-win,.dockm-resize{display:none}.dockm-title{font-size:16px}.dockm-close{width:44px;height:44px;font-size:17px}.dockm-body{flex-direction:column;min-height:0}.dockm-nav{box-sizing:border-box;width:auto;max-width:100%;min-height:56px;flex-direction:row;gap:6px;padding:6px 10px;overflow-x:auto;overflow-y:hidden;border-right:0;border-bottom:1px solid var(--dsw-alias-border-l1);scrollbar-width:none;overscroll-behavior-x:contain}.dockm-nav::-webkit-scrollbar{display:none}.dockm-nav-item{flex:none;min-height:44px;padding:0 12px;font-size:13px;touch-action:manipulation}.dockm-nav-item .dockm-badge{display:none}.dockm-content{padding:16px max(16px,env(safe-area-inset-right)) calc(20px + env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));gap:14px;overscroll-behavior:contain}.dockm-content-head{gap:5px}.dockm-name{font-size:16px}.dockm-desc{font-size:13px;line-height:1.55}.dockm-foot{padding-top:12px;font-size:11px}.dockm-foot-sw{width:100%;margin-left:0;justify-content:space-between}.dock-sw{width:44px;height:26px}.dock-sw::after{top:3px;left:3px;width:18px;height:18px}.dock-sw.on::after{transform:translateX(18px)}.dockh-grid{grid-template-columns:1fr;gap:10px}.dockh-card{padding:12px}.dockh-desc{font-size:13px}.dockh-go{font-size:12px}}",
194
- "@media (prefers-reduced-motion:reduce){.dockm-backdrop,.dockm-dialog{animation:none}.dockm-nav-item,.dockm-close,.dockm-win,.dock-sw{transition:none}}",
195
- ".dockh-desc{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;}",
196
- ".dockh-stat{color:var(--dsw-alias-label-tertiary);font-size:12px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
197
- ".dockh-foot{display:flex;align-items:center;gap:8px;}",
198
- ".dockh-go{color:var(--dsw-alias-label-tertiary);font-size:11px;}",
199
- // 会话输入区工具行 chips(dockchip- 前缀):余额/用量随身小控件,挂在模型选择器左侧
200
- // 溢出兼容(两层防护,缺一不可):
201
- // 1) 宽度上限:宿主 .row 是 flex-wrap:wrap 且换行优先于收缩,chips 一长 tools 整行变宽,
202
- // 右侧模型选择器/发送键被挤到第二行(左右错位)。宿主 .row 带 container-type:inline-size,
203
- // 用容器单位 cqw 让上限随输入卡宽度自适应(窄卡少占、宽卡多占);不支持 cqw 的老内核退回固定值。
204
- // 注意:旧上限 min(280px,36cqw) 仍偏宽——标准 780px 输入卡下(.row 内容约 762px),
205
- // add 钮(28)+工作区选择器(~214)+chips(280) 已与右侧模型选择器+发送键(~345px)合计超宽,
206
- // 即便 chips 未截断 .row 也会换行把模型选择器/发送键挤到第二行。实测把上限收到 189px
207
- // (≈25cqw)并使 chips 内距收紧(padding 2px 6px、gap 2px)后,实测 4~6M 用量 + 余额
208
- // 两 chip 可完整显示且不再换行;超限时仍被省略号截断,完整值在 title 悬浮提示里。
209
- // 2) 截断:超限部分用省略号截断(完整数值在 title 悬浮提示里),防 chips 凸出输入卡圆角(悬空)。
210
- ".dockchip-row{display:inline-flex;align-items:center;gap:2px;min-width:0;flex:0 1 auto;overflow:hidden;max-width:189px;}",
211
- "@supports (width:1cqw){.dockchip-row{max-width:min(189px,25cqw);}}",
212
- ".dockchip{display:inline-flex;align-items:center;gap:5px;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;padding:2px 6px;font-family:inherit;font-size:11px;line-height:18px;white-space:nowrap;min-width:0;overflow:hidden;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
213
- ".dockchip > span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}",
214
- ".dockchip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
215
- ".dockchip .dockchip-dot{width:6px;height:6px;border-radius:50%;flex:none;}",
216
- ".dockchip.err{color:var(--dsw-alias-state-error-primary);}"
217
- ].join("\n");
218
-
219
- let dockCssTag = null;
220
- let initialCssSchedule = null;
221
- function cancelInitialCssSchedule() {
222
- if (!initialCssSchedule || typeof window === "undefined") return;
223
- if (initialCssSchedule.type === "idle" && typeof window.cancelIdleCallback === "function") window.cancelIdleCallback(initialCssSchedule.id);
224
- if (initialCssSchedule.type === "timer") window.clearTimeout(initialCssSchedule.id);
225
- initialCssSchedule = null;
226
- }
227
- function fullDockCss() {
228
- const parts = [SHELL_CSS];
229
- for (const f of BUILTIN_FEATURES) if (f.css) parts.push(f.css);
230
- for (const f of externalDefs) if (f.css) parts.push(f.css);
231
- return parts.join("\n");
232
- }
233
- // 样式全局注入一次(入口按钮与右下角浮层挂在设置页之外,不能依赖页内 <style>)。
234
- // 这里用 SHELL_CSS 唯一命名,并做防御:注入永不抛错、失败只降级不打断启动;
235
- // 外部功能注册携带样式时可重算更新(外部样式后到)。
236
- function ensureCss() {
237
- if (typeof document === "undefined") return;
238
- cancelInitialCssSchedule();
239
- try {
240
- if (!dockCssTag || !dockCssTag.isConnected) {
241
- if (document.querySelector('style[data-plugin-css="dsh-dock"]')) {
242
- dockCssTag = document.querySelector('style[data-plugin-css="dsh-dock"]');
243
- } else {
244
- dockCssTag = document.createElement("style");
245
- dockCssTag.dataset.pluginCss = "dsh-dock";
246
- document.head.appendChild(dockCssTag);
247
- }
248
- }
249
- dockCssTag.textContent = fullDockCss();
250
- } catch (e) {
251
- console.error("[dsh-dock] ensureCss failed:", e && e.message ? e.message : String(e));
252
- }
253
- }
254
- // 刷新页时宿主内容优先绘制:功能坞样式很大,首帧同步解析会让整页短暂停顿。
255
- // 首次注入延后到浏览器空闲期;之后的外部模块注册仍走 ensureCss() 立即更新。
256
- function scheduleInitialCss() {
257
- if (typeof document === "undefined" || initialCssSchedule || (dockCssTag && dockCssTag.isConnected)) return;
258
- const run = () => {
259
- initialCssSchedule = null;
260
- ensureCss();
261
- };
262
- if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") {
263
- initialCssSchedule = { type: "idle", id: window.requestIdleCallback(run, { timeout: 350 }) };
264
- } else {
265
- initialCssSchedule = { type: "timer", id: setTimeout(run, 48) };
266
- }
267
- }
268
-
269
- // ---- 外部功能变化订阅 hook(弹层/设置页重渲染用) ----
270
- function useExternalVersion() {
271
- const [, bump] = react.useReducer((n) => n + 1, 0);
272
- react.useEffect(() => {
273
- externalListeners.add(bump);
274
- return () => { externalListeners.delete(bump); };
275
- }, []);
276
- }
277
-
278
- // ---- 弹层窗口几何记忆(页面生命周期内):拖动/缩放后记住位置与尺寸,重开还原 ----
279
- let lastGeom = { x: null, y: null, w: null, h: null };
280
-
281
- function DockIcon(props) {
282
- const size = (props && props.size) || 16;
283
- return react.createElement("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, "aria-hidden": true },
284
- react.createElement("rect", { x: 2.5, y: 2.5, width: 4.4, height: 4.4, rx: 1.2 }),
285
- react.createElement("rect", { x: 9, y: 2.5, width: 4.4, height: 4.4, rx: 1.2 }),
286
- react.createElement("rect", { x: 2.5, y: 9, width: 4.4, height: 4.4, rx: 1.2 }),
287
- react.createElement("rect", { x: 9, y: 9, width: 4.4, height: 4.4, rx: 1.2 }));
288
- }
289
-
290
- function DockEntry(props) {
291
- const wide = !!props.wide;
292
- const [open, setOpen] = react.useState(panelNav.open);
293
- react.useEffect(() => subscribePanel(() => setOpen(panelNav.open)), []);
294
- return react.createElement("button", {
295
- type: "button",
296
- className: "docke2-btn" + (open ? " docke2-on" : "") + (wide ? "" : " docke2-rail"),
297
- title: "功能坞",
298
- "aria-label": "功能坞",
299
- "aria-expanded": open,
300
- onClick: () => setPanelOpen(!open)
301
- }, react.createElement(DockIcon, { size: wide ? 16 : 18 }), wide ? react.createElement("span", { className: "docke2-label" }, "功能坞") : null);
302
- }
303
-
304
- // ---- 功能坞弹出面板:shell.overlay(仿 dsh 设置:居中模态 = 遮罩 + 对话框) ----
305
- // 左侧导航:首项「首页」总揽(默认选中),其后是各功能模块(order 升序);planned 模块只占位展示
306
- function DockModal() {
307
- // 默认关闭;SSR/无浏览器环境下默认展开内容(便于冒烟测试渲染整棵弹层树)
308
- // 导航总线(open/active/params)驱动:chips 点击 openPanel(...) 即可打开并定位
309
- const [nav, setNav] = react.useState({ open: panelNav.open, active: panelNav.active, params: panelNav.params });
310
- react.useEffect(() => subscribePanel(() => setNav({ open: panelNav.open, active: panelNav.active, params: panelNav.params })), []);
311
- const open = nav.open || typeof document === "undefined";
312
- const active = nav.active;
313
- const setActive = navigatePanel;
314
- const navParams = nav.params;
315
- const [, force] = react.useReducer((n) => n + 1, 0);
316
- useExternalVersion();
317
- // ---- 窗口几何:普通(默认居中大窗)/最大化/最小化;拖动标题栏移动、右下角缩放 ----
318
- // lastGeom 记住本页生命周期内最后一次几何,重开弹层时还原
319
- const [win, setWin] = react.useState(() => ({ mode: "normal", x: null, y: null, w: null, h: null }));
320
- const dlgRef = react.useRef(null);
321
- const contentRef = react.useRef(null);
322
- react.useEffect(() => {
323
- if (lastGeom.w) setWin({ mode: "normal", x: lastGeom.x, y: lastGeom.y, w: lastGeom.w, h: lastGeom.h });
324
- }, []);
325
- // 切换左侧模块时总是从新页面顶部开始,避免把上一页的滚动位置带进小游戏等短页面。
326
- react.useEffect(() => {
327
- if (open && contentRef.current) contentRef.current.scrollTop = 0;
328
- }, [active, open]);
329
- function beginDrag(e, type) {
330
- if (win.mode !== "normal" || e.button !== 0) return;
331
- if (type === "move" && e.target && e.target.closest && e.target.closest("button,select,input")) return;
332
- const node = dlgRef.current;
333
- if (!node || typeof window === "undefined") return;
334
- const rect = node.getBoundingClientRect();
335
- const startX = e.clientX, startY = e.clientY;
336
- const origin = { left: rect.left, top: rect.top, w: rect.width, h: rect.height };
337
- const clampX = (v) => Math.min(Math.max(8, v), window.innerWidth - origin.w - 8);
338
- const clampY = (v) => Math.min(Math.max(8, v), window.innerHeight - origin.h - 8);
339
- const onMove = (ev) => {
340
- const dx = ev.clientX - startX, dy = ev.clientY - startY;
341
- if (type === "move") setWin((s) => Object.assign({}, s, { x: clampX(origin.left + dx), y: clampY(origin.top + dy) }));
342
- else setWin((s) => Object.assign({}, s, {
343
- w: Math.max(640, Math.min(origin.w + dx, window.innerWidth - 16)),
344
- h: Math.max(420, Math.min(origin.h + dy, window.innerHeight - 16))
345
- }));
346
- };
347
- const onUp = () => {
348
- window.removeEventListener("pointermove", onMove);
349
- window.removeEventListener("pointerup", onUp);
350
- setWin((s) => { lastGeom = { x: s.x, y: s.y, w: s.w, h: s.h }; return s; });
351
- };
352
- window.addEventListener("pointermove", onMove);
353
- window.addEventListener("pointerup", onUp);
354
- }
355
- if (!open) return null;
356
- const MODULES = allModules();
357
- const isHome = active === "home";
358
- const mod = isHome ? null : (MODULES.find((m) => m.id === active) || MODULES[0]);
359
- const st = mod ? stateOf(mod.id) : null;
360
- // 外部包视图包错误边界;视图统一收到 { ctx, feature, params } props
361
- //(params 来自导航总线:如 { provider } 高亮余额行、{ sessionId } 按会话筛选用量)
362
- const View = mod ? mod.View : null;
363
- const viewNode = mod && View
364
- ? react.createElement("div", { className: "dockm-view" },
365
- react.createElement(mod.external ? FeatureBoundary : react.Fragment, null,
366
- react.createElement(View, { ctx: ctxRef.current, feature: mod, params: navParams })))
367
- : null;
368
- const enabledCount = MODULES.filter((m) => { const s = stateOf(m.id); return !!(s && s.enabled); }).length;
369
- // 最大化:铺满视口(留 10px 边);被拖过/缩放过:固定坐标;否则 CSS 默认居中
370
- const dialogStyle = win.mode === "max"
371
- ? { position: "fixed", left: 10, top: 10, right: 10, bottom: 10, width: "auto", height: "auto" }
372
- : (win.x != null || win.y != null || win.w != null || win.h != null)
373
- ? {
374
- position: "fixed",
375
- left: win.x != null ? win.x : undefined,
376
- top: win.y != null ? win.y : undefined,
377
- width: win.w != null ? win.w : undefined,
378
- height: win.mode === "min" ? "auto" : (win.h != null ? win.h : undefined)
379
- }
380
- : null;
381
- return react.createElement("div", { className: "dockm-backdrop", onClick: () => setPanelOpen(false) },
382
- react.createElement("div", {
383
- className: "dockm-dialog" + (win.mode === "max" ? " dockm-max" : "") + (win.mode === "min" ? " dockm-min" : ""),
384
- style: dialogStyle,
385
- ref: dlgRef,
386
- onClick: (e) => e.stopPropagation()
387
- },
388
- react.createElement("div", {
389
- className: "dockm-head",
390
- onPointerDown: (e) => beginDrag(e, "move"),
391
- onDoubleClick: () => setWin((s) => Object.assign({}, s, { mode: s.mode === "max" ? "normal" : "max" }))
392
- },
393
- react.createElement(DockIcon, null),
394
- react.createElement("span", { className: "dockm-title" }, "功能坞"),
395
- react.createElement("span", { className: "dockm-sub" }, "dsh-dock · 也可在 设置 → 功能坞 打开管理页"),
396
- react.createElement("span", { className: "dockm-ctrls" },
397
- react.createElement("button", {
398
- type: "button",
399
- className: "dockm-win",
400
- title: win.mode === "min" ? "还原" : "最小化",
401
- onClick: () => setWin((s) => Object.assign({}, s, { mode: s.mode === "min" ? "normal" : "min" }))
402
- }, "▁"),
403
- react.createElement("button", {
404
- type: "button",
405
- className: "dockm-win",
406
- title: win.mode === "max" ? "还原" : "最大化",
407
- onClick: () => setWin((s) => Object.assign({}, s, { mode: s.mode === "max" ? "normal" : "max" }))
408
- }, win.mode === "max" ? "❐" : "▢"),
409
- react.createElement("button", { type: "button", className: "dockm-close", "aria-label": "关闭", title: "关闭", onClick: () => setPanelOpen(false) }, "✕"))),
410
- react.createElement("div", { className: "dockm-body" },
411
- react.createElement("nav", { className: "dockm-nav", "aria-label": "功能模块" },
412
- react.createElement("button", {
413
- type: "button",
414
- key: "home",
415
- className: "dockm-nav-item" + (isHome ? " on" : ""),
416
- onClick: () => setActive("home")
417
- },
418
- react.createElement("span", { className: "dockm-navhome" }, react.createElement(DockIcon, null)),
419
- react.createElement("span", null, "首页")),
420
- MODULES.map((m) =>
421
- react.createElement("button", {
422
- type: "button",
423
- key: m.id,
424
- className: "dockm-nav-item" + (m.id === active ? " on" : ""),
425
- onClick: () => setActive(m.id)
426
- },
427
- react.createElement("span", { className: "dockm-dot", style: { background: m.accent } }),
428
- react.createElement("span", null, m.name),
429
- m.planned ? react.createElement("span", { className: "dockm-badge" }, "规划中") : null,
430
- m.external ? react.createElement("span", { className: "dockm-badge" }, "外部") : null))),
431
- react.createElement("div", { className: "dockm-content", ref: contentRef },
432
- react.createElement("div", { className: "dockm-content-head" },
433
- react.createElement("div", { className: "dockm-name" },
434
- isHome
435
- ? react.createElement("span", { className: "dockm-navhome" }, react.createElement(DockIcon, null))
436
- : react.createElement("span", { className: "dockm-dot", style: { background: mod.accent } }),
437
- isHome ? "首页" : mod.name,
438
- !isHome && mod.planned ? react.createElement("span", { className: "dockm-badge" }, "规划中") : null,
439
- !isHome && mod.external ? react.createElement("span", { className: "dockm-badge" }, "外部包" + (mod.package ? " · " + mod.package : "")) : null),
440
- react.createElement("div", { className: "dockm-desc" },
441
- isHome
442
- ? "所有子功能总揽:运行状态、概要与快捷开关,点击卡片进入对应功能页。"
443
- : mod.description)),
444
- isHome
445
- ? react.createElement("div", { className: "dockm-view" }, react.createElement(HomeView, { ctx: ctxRef.current, onOpen: setActive, onToggle: force }))
446
- : mod.planned
447
- ? react.createElement("div", { className: "dockm-note" }, PLANNED_NOTES[mod.id] || "待接入:见 README 路线图")
448
- : st && st.enabled && viewNode
449
- ? viewNode
450
- : st && st.error
451
- ? react.createElement("div", { className: "dockm-note dockm-err" }, "功能出错:" + st.error)
452
- : react.createElement("div", { className: "dockm-note" }, "该功能当前为停用状态(开关已持久化,重启后保持)"),
453
- react.createElement("div", { className: "dockm-foot" },
454
- react.createElement("span", null, isHome
455
- ? "功能坞 v" + DOCK_VERSION + " · 共 " + MODULES.length + " 个功能模块," + enabledCount + " 个已启用"
456
- : "功能坞 v" + DOCK_VERSION + " · 新功能按路线图追加"),
457
- !isHome && mod && !mod.planned && st
458
- ? react.createElement("span", { className: "dockm-foot-sw" },
459
- react.createElement("span", { className: "dockm-foot-swlabel" + (st.enabled ? " on" : "") }, st.enabled ? "已启用" : "已停用"),
460
- react.createElement("button", {
461
- type: "button",
462
- className: "dock-sw" + (st.enabled ? " on" : ""),
463
- role: "switch",
464
- "aria-checked": !!st.enabled,
465
- "aria-label": (st.enabled ? "停用" : "启用") + mod.name,
466
- title: st.enabled ? "停用「" + mod.name + "」" : "启用「" + mod.name + "」",
467
- onClick: () => { toggleFeature(mod.id); force(); }
468
- }))
469
- : null,
470
- !isHome && mod && typeof mod.Chip === "function"
471
- ? react.createElement("span", { className: "dockm-foot-sw" },
472
- react.createElement("span", { className: "dockm-foot-swlabel" + (chipShown(mod.id) ? " on" : "") }, "会话页小控件"),
473
- react.createElement("button", {
474
- type: "button",
475
- className: "dock-sw" + (chipShown(mod.id) ? " on" : ""),
476
- role: "switch",
477
- "aria-checked": chipShown(mod.id),
478
- "aria-label": (chipShown(mod.id) ? "隐藏" : "显示") + mod.name + "的会话页小控件",
479
- title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
480
- onClick: () => { setChipShown(mod.id, !chipShown(mod.id)); force(); }
481
- }))
482
- : null))),
483
- win.mode === "normal"
484
- ? react.createElement("div", { className: "dockm-resize", onPointerDown: (e) => beginDrag(e, "size") })
485
- : null));
486
- }
487
-
488
- // ---- 首页总揽:每个功能模块一张卡片(状态标识 + 运行概要 + 启停开关),点卡片进对应功能 ----
489
- // 交互约定:状态是「圆点+文字」纯标识(不可点);启停是 iOS 滑动开关(明显可点);点卡片其余区域跳详情页。
490
- function HomeView(props) {
491
- const ctx = props && props.ctx;
492
- const [, force] = react.useReducer((n) => n + 1, 0);
493
- useExternalVersion();
494
- const open = (id) => { if (props && typeof props.onOpen === "function") props.onOpen(id); };
495
- return react.createElement("div", { className: "dockh-grid" },
496
- allModules().map((m) => {
497
- const st = stateOf(m.id);
498
- const enabled = !!(st && st.enabled);
499
- const Stat = m.HomeStat;
500
- const statNode = m.planned
501
- ? react.createElement("span", null, PLANNED_NOTES[m.id] || "待接入:见 README 路线图")
502
- : enabled && Stat
503
- ? react.createElement(m.external ? FeatureBoundary : react.Fragment, null,
504
- react.createElement(Stat, { ctx: ctx }))
505
- : react.createElement("span", null, "已停用,启用后在此展示运行概要");
506
- return react.createElement("div", {
507
- key: m.id,
508
- className: "dockh-card",
509
- role: "button",
510
- tabIndex: 0,
511
- onClick: () => open(m.id),
512
- onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(m.id); } }
513
- },
514
- react.createElement("div", { className: "dockh-head" },
515
- react.createElement("span", { className: "dockm-dot", style: { background: m.accent } }),
516
- react.createElement("span", { className: "dockh-name" }, m.name),
517
- m.external ? react.createElement("span", { className: "dockh-badge", title: m.package || undefined }, "外部") : null,
518
- // 状态标识:圆点 + 文字(纯展示,与开关视觉区分)
519
- react.createElement("span", { className: "dockh-status" + (m.planned ? " plan" : enabled ? "" : " off") },
520
- react.createElement("span", { className: "dockh-sdot" }),
521
- react.createElement("span", null, m.planned ? "规划中" : enabled ? "运行中" : "已停用")),
522
- // 启停开关(规划中的功能不显示)
523
- m.planned ? null : react.createElement("button", {
524
- type: "button",
525
- className: "dock-sw" + (enabled ? " on" : ""),
526
- role: "switch",
527
- "aria-checked": enabled,
528
- "aria-label": (enabled ? "停用" : "启用") + m.name,
529
- title: enabled ? "停用「" + m.name + "」" : "启用「" + m.name + "」",
530
- onClick: (e) => {
531
- e.stopPropagation();
532
- toggleFeature(m.id);
533
- // onToggle = DockModal 的 force:连带刷新弹层脚注的「N 个已启用」统计
534
- if (props && typeof props.onToggle === "function") props.onToggle();
535
- force();
536
- }
537
- })),
538
- react.createElement("div", { className: "dockh-desc" }, m.description),
539
- react.createElement("div", { className: "dockh-stat" }, statNode),
540
- react.createElement("div", { className: "dockh-foot" },
541
- react.createElement("span", { className: "dockh-go" }, "查看详情 →")));
542
- }));
543
- }
544
-
545
- // ---- 设置页管理面板(settings.section):所有功能模块卡片一览 ----
546
- function DockPanel() {
547
- const ctx = ctxRef.current;
548
- const [, force] = react.useReducer((n) => n + 1, 0);
549
- useExternalVersion();
550
- const toggle = (id) => {
551
- toggleFeature(id);
552
- force();
553
- };
554
- return react.createElement("div", { className: "dock-root" },
555
- react.createElement("div", { className: "dock-intro" },
556
- "功能坞(dsh-dock)· 所有小功能集中在这一个面板里管理。v0.4.0 起每个功能是独立模块(features/<id>/),",
557
- "可单独提取打包发布(scripts/extract-feature.mjs);独立发布的功能包装回后经 dockBridge 注册进本面板。"),
558
- allModules().map((f) => {
559
- const st = stateOf(f.id);
560
- const View = f.View;
561
- const viewNode = (!f.planned && st.enabled && View)
562
- ? react.createElement("div", { className: "dock-body" },
563
- react.createElement(f.external ? FeatureBoundary : react.Fragment, null,
564
- react.createElement(View, { ctx: ctx, feature: f })))
565
- : null;
566
- return react.createElement("div", { className: "dock-card", key: f.id },
567
- react.createElement("div", { className: "dock-card-head" },
568
- react.createElement("span", { className: "dock-dot" + (st.error ? " err" : st.enabled ? " on" : "") }),
569
- react.createElement("span", { className: "dock-name" }, f.name),
570
- react.createElement("span", { className: "dock-desc" }, f.description + (f.external ? "(来自外部包" + (f.package ? " " + f.package : "") + ")" : "")),
571
- f.planned
572
- ? react.createElement("span", { className: "dock-badge" }, "规划中")
573
- : react.createElement("button", {
574
- type: "button",
575
- className: "dock-sw" + (st.enabled ? " on" : ""),
576
- role: "switch",
577
- "aria-checked": st.enabled,
578
- "aria-label": (st.enabled ? "停用" : "启用") + f.name,
579
- title: st.enabled ? "停用「" + f.name + "」" : "启用「" + f.name + "」",
580
- onClick: () => toggle(f.id)
581
- }),
582
- !f.planned && typeof f.Chip === "function"
583
- ? react.createElement("span", { className: "dockm-foot-sw" },
584
- react.createElement("span", { className: "dockm-foot-swlabel" + (chipShown(f.id) ? " on" : "") }, "会话页小控件"),
585
- react.createElement("button", {
586
- type: "button",
587
- className: "dock-sw" + (chipShown(f.id) ? " on" : ""),
588
- role: "switch",
589
- "aria-checked": chipShown(f.id),
590
- "aria-label": (chipShown(f.id) ? "隐藏" : "显示") + f.name + "的会话页小控件",
591
- title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
592
- onClick: () => { setChipShown(f.id, !chipShown(f.id)); force(); }
593
- }))
594
- : null),
595
- f.planned
596
- ? react.createElement("div", { className: "dock-body" }, PLANNED_NOTES[f.id] || "待接入:见 README 路线图")
597
- : st.error ? react.createElement("div", { className: "dock-body dockm-err" }, "功能出错:" + st.error) : null,
598
- viewNode);
599
- }));
600
- }
601
-
602
- // ---- 会话输入区工具行 chips:已启用功能的随身小控件(模型选择器左侧) ----
603
- // 功能模块在 feature 描述符上挂可选 Chip 组件(props: { ctx, session, sessionId, input, feature }),
604
- // 启用即在 conversation.input.left 渲染;点击通常经 openPanel(...) 打开功能坞定位到对应功能页。
605
- function DockChips(props) {
606
- const [, force] = react.useReducer((n) => n + 1, 0);
607
- react.useEffect(() => subscribeFeatureState(() => force()), []);
608
- const items = [];
609
- for (const f of allModules()) {
610
- if (f.planned || !stateOf(f.id).enabled || !chipShown(f.id) || typeof f.Chip !== "function") continue;
611
- items.push(react.createElement(f.Chip, {
612
- key: f.id, ctx: props.ctx, feature: f,
613
- session: props.session, sessionId: props.sessionId, input: props.input,
614
- }));
615
- }
616
- if (items.length === 0) return null;
617
- return react.createElement("div", { className: "dockchip-row" }, items);
618
- }
619
-
620
- // ---- 功能全局浮层:模块可在描述符上挂 Overlay 组件(props: { ctx, feature }) ----
621
- // 与 Chip(会话输入区小控件)不同,Overlay 是常驻整页挂载的全局 UI(如任务动画的动效与通知栈),
622
- // 只要所属功能启用就渲染(功能停用即卸载,浮层自身负责按数据状态决定显示什么)。
623
- function FeatureOverlays() {
624
- const [, force] = react.useReducer((n) => n + 1, 0);
625
- useExternalVersion();
626
- react.useEffect(() => subscribeFeatureState(() => force()), []);
627
- const items = [];
628
- for (const f of allModules()) {
629
- if (f.planned || !stateOf(f.id).enabled || typeof f.Overlay !== "function") continue;
630
- items.push(react.createElement(FeatureBoundary, { key: f.id },
631
- react.createElement(f.Overlay, { ctx: ctxRef.current, feature: f })));
632
- }
633
- if (items.length === 0) return null;
634
- return react.createElement(react.Fragment, null, items);
635
- }
636
-
637
- // ---- 插件应用:注册四处 UI(入口按钮 / 弹层 / 设置页 / 会话区 chips),注入样式 ----
638
- // ctx 以 ref 供视图组件使用(timer/theme 等服务按需自取;重装/HMR 时更新)
639
- const ctxRef = { current: null };
640
-
641
- export function apply(ctx) {
642
- ctxRef.current = ctx;
643
- scheduleInitialCss();
644
- initFeatureState(BUILTIN_FEATURES.concat(PLANNED_FEATURES));
645
- const slots = ctx.get("slots");
646
- if (slots === undefined) return;
647
-
648
- slots.inject("sidebar.footer.action", () => slots.register(
649
- { name: "sidebar.footer.action", id: "dsh-dock", order: 1, label: "功能坞" },
650
- (props) => react.createElement(DockEntry, props)));
651
- slots.inject("shell.overlay", () => slots.register(
652
- { name: "shell.overlay", id: "dsh-dock-panel", order: 21, label: "功能坞面板" },
653
- () => react.createElement(DockModal, null)));
654
- // 功能全局浮层(任务动画的动效与通知等):已启用功能的 Overlay 常驻挂载
655
- slots.inject("shell.overlay", () => slots.register(
656
- { name: "shell.overlay", id: "dsh-dock-feature-overlays", order: 22, label: "功能坞全局浮层" },
657
- () => react.createElement(FeatureOverlays, null)));
658
- slots.inject("settings.section", () => slots.register(
659
- { name: "settings.section", id: "dsh-dock", order: 90, label: "功能坞" },
660
- () => react.createElement(DockPanel, null)));
661
- // 会话输入卡工具行左端(模型选择器左侧):已启用功能的随身小控件
662
- slots.inject("conversation.input.left", () => slots.register(
663
- { name: "conversation.input.left", id: "dsh-dock-chips", order: 10, label: "功能坞" },
664
- (zone) => react.createElement(DockChips, Object.assign({}, zone, { ctx: ctxRef.current }))));
665
- }
666
-
667
- export const inject = ["timer"];
668
- export { dockBridge };
1
+ // dsh-dock · Client 半部(浏览器外壳,源码入口;构建为根目录 client.js)
2
+ //
3
+ // v0.4.0 模块化架构:外壳只负责面板本身(入口按钮/居中弹窗/设置页/首页总揽),
4
+ // 每个功能是一个 features/<id>/view.js(x) 模块(自带描述符 + 视图 + 样式),在此 import 组装。
5
+ // 菜单次序由各模块 order 字段决定(首页固定第一,tokenlog=10 → 第二个菜单)。
6
+ //
7
+ // 外部功能桥(dockBridge):独立发布的功能包(scripts/extract-feature.mjs 生成的骨架)
8
+ // 在浏览器端 require("dsh-dock").dockBridge.register(def) 把自己的视图注册进本面板;
9
+ // dock 未安装时该包走自己的独立面板——「单独成包发布」与「完美集成功能坞」两全。
10
+ //
11
+ // 面板挂载在 settings.section:设置 → 功能坞;侧栏底部另有「功能坞」入口按钮
12
+ // (sidebar.footer.action),点击弹出居中功能面板(shell.overlay)。
13
+ // 弹层窗口:默认大窗,支持最大化/最小化、标题栏拖动、右下角缩放(几何页内记忆)。
14
+ import react from "react";
15
+ import {
16
+ FeatureBoundary,
17
+ initFeatureState, stateOf, toggleFeature, subscribeFeatureState,
18
+ chipShown, setChipShown,
19
+ panelNav, openPanel, setPanelOpen, navigatePanel, subscribePanel,
20
+ } from "./shared.js";
21
+ import { feature as fTokenlog } from "../features/tokenlog/view.jsx";
22
+ import { feature as fModelconfig } from "../features/modelconfig/view.js";
23
+ import { feature as fHeartbeat } from "../features/heartbeat/view.js";
24
+ import { feature as fTheme } from "../features/theme/view.js";
25
+ import { feature as fBalance } from "../features/balance/view.js";
26
+ import { feature as fAnimation } from "../features/animation/view.jsx";
27
+ import { feature as fGames } from "../features/games/view.jsx";
28
+ import { feature as fMobileRelay } from "../features/mobile-relay/view.jsx";
29
+
30
+ const name = "dsh-dock";
31
+ const DOCK_VERSION = "0.9.7";
32
+
33
+ // ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
34
+ const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance, fAnimation, fGames, fMobileRelay];
35
+ // 规划占位(路线图):接入后移除并建 features/<id>/ 模块
36
+ const PLANNED_FEATURES = [];
37
+ const PLANNED_NOTES = {};
38
+
39
+ // ---- 外部功能注册表(dockBridge 回装通道) ----
40
+ const externalDefs = [];
41
+ const externalListeners = new Set();
42
+ function notifyExternal() {
43
+ for (const fn of externalListeners) fn();
44
+ }
45
+ /**
46
+ * 外部功能桥:独立功能包经 require("dsh-dock").dockBridge 注册视图。
47
+ * def: { id, name, order?, accent?, description?, css?, View, HomeStat?, Overlay?, package? }
48
+ * 返回注销函数。重复 id 视为更新(HMR/重注册场景)。
49
+ */
50
+ const dockBridge = {
51
+ version: DOCK_VERSION,
52
+ register(def) {
53
+ if (!def || typeof def !== "object" || typeof def.id !== "string" || !def.id || typeof def.View !== "function") {
54
+ console.error("[dsh-dock] dockBridge.register 参数不符(需 { id, name, View }):", def);
55
+ return () => {};
56
+ }
57
+ const entry = {
58
+ id: def.id,
59
+ name: String(def.name || def.id),
60
+ order: typeof def.order === "number" ? def.order : 500,
61
+ accent: typeof def.accent === "string" ? def.accent : "#94a3b8",
62
+ description: String(def.description || ""),
63
+ css: typeof def.css === "string" ? def.css : "",
64
+ View: def.View,
65
+ HomeStat: typeof def.HomeStat === "function" ? def.HomeStat : null,
66
+ Overlay: typeof def.Overlay === "function" ? def.Overlay : null,
67
+ external: true,
68
+ package: typeof def.package === "string" ? def.package : "",
69
+ };
70
+ const i = externalDefs.findIndex((x) => x.id === entry.id);
71
+ if (i >= 0) externalDefs[i] = entry;
72
+ else externalDefs.push(entry);
73
+ if (entry.css) ensureCss(); // 外部样式后到:重算并更新已注入的 <style>
74
+ notifyExternal();
75
+ return () => {
76
+ const j = externalDefs.indexOf(entry);
77
+ if (j >= 0) { externalDefs.splice(j, 1); notifyExternal(); }
78
+ };
79
+ },
80
+ features() {
81
+ return externalDefs.map((x) => Object.assign({}, x));
82
+ },
83
+ };
84
+
85
+ // ---- 菜单模块表(内置 + 外部,按 order 升序;同序稳定保持注册顺序) ----
86
+ function allModules() {
87
+ return BUILTIN_FEATURES.concat(PLANNED_FEATURES, externalDefs)
88
+ .slice()
89
+ .sort((a, b) => ((a.order || 500) - (b.order || 500)));
90
+ }
91
+
92
+ // ---- 开关状态与面板导航总线在 src/shared.js(会话区 chips 与面板共用,避免外壳↔模块循环依赖) ----
93
+
94
+ // ---- 样式:外壳样式 + 各功能模块自带样式(css 字段)合并注入一个 <style> ----
95
+ // ⚠️ 变量名不得用 CSS:浏览器存在全局 window.CSS 命名空间,bundle 任何作用域解析歧义
96
+ // 都会把标识符解析成该全局对象(无 .join),曾导致插件应用失败、整页启动崩溃。
97
+ const SHELL_CSS = [
98
+ // ---- 主题桥接层(亮色适配的根基)----
99
+ // dsh 宿主没有 --dsw-alias-accent,也没有 --dsw-alias-state-warning-primary(正确名是 state-warn-primary),
100
+ // 旧写法全部落到暗色系回退值(浅蓝/琥珀),亮色白底下看不清。这里在 body 上按宿主主题
101
+ // (body / body[data-ds-dark-theme])声明两套 dock 语义变量,各模块统一引用:
102
+ // --dk-accent 主强调色(亮色用深蓝保证对比度,暗色用亮蓝)
103
+ // --dk-warn 警告色(亮色用深琥珀,暗色用亮琥珀)
104
+ // --dk-ok/--dk-err 成功/错误(亮色取宿主深色值,暗色保持宿主亮色值)
105
+ // --dk-tdim 表格分隔线/弱边框(亮色用中性灰蓝、暗色用白透明)
106
+ "body{--dk-accent:#2f6fed;--dk-accent-soft:color-mix(in srgb,#2f6fed 12%,transparent);--dk-warn:#b45309;--dk-ok:var(--dsw-alias-state-success-primary,#16a34a);--dk-err:var(--dsw-alias-state-error-primary,#dc2626);--dk-tdim:rgba(100,116,139,.22);--dk-hover-tint:rgba(47,111,237,.08);--dk-games-accent:#7c3aed;}",
107
+ "body[data-ds-dark-theme]{--dk-accent:#4d9fff;--dk-accent-soft:color-mix(in srgb,#4d9fff 12%,transparent);--dk-warn:#fbbf24;--dk-ok:var(--dsw-alias-state-success-primary,#34d399);--dk-err:var(--dsw-alias-state-error-primary,#f87171);--dk-tdim:rgba(122,132,152,.14);--dk-hover-tint:rgba(80,110,180,.12);--dk-games-accent:#a78bfa;}",
108
+ // 宿主语义变量兜底别名(部分模块引用了宿主不存在的名字,统一在此补齐,取值随主题自动切换)
109
+ "body{--dsw-alias-accent:var(--dk-accent);--dsw-alias-accent-soft:var(--dk-accent-soft);--dsw-alias-state-warning-primary:var(--dk-warn);}",
110
+ ".dock-root{display:flex;flex-direction:column;gap:12px;padding:4px 0;color:var(--dsw-alias-label-primary);font-size:13px;}",
111
+ ".dock-intro{color:var(--dsw-alias-label-secondary);line-height:1.6;}",
112
+ ".dock-card{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;}",
113
+ ".dock-card-head{display:flex;align-items:center;gap:8px;}",
114
+ ".dock-dot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-label-secondary);flex:none;}",
115
+ ".dock-dot.on{background:var(--dsw-alias-state-success-primary);}",
116
+ ".dock-dot.err{background:var(--dsw-alias-state-error-primary);}",
117
+ ".dock-name{font-weight:600;flex:none;}",
118
+ ".dock-desc{color:var(--dsw-alias-label-secondary);flex:1;}",
119
+ ".dock-switch{flex:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:3px 10px;cursor:pointer;font-size:12px;}",
120
+ ".dock-switch.on{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);}",
121
+ ".dock-badge{flex:none;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:3px 10px;font-size:12px;}",
122
+ ".dock-body{border-top:1px solid var(--dsw-alias-border-l1);padding-top:8px;color:var(--dsw-alias-label-secondary);display:flex;flex-direction:column;gap:4px;}",
123
+ // 侧栏入口按钮(docke2- 前缀)+ 功能坞弹出面板(dockm- 前缀,仿 dsh 设置的居中模态:遮罩 + 对话框,左导航 + 右内容)
124
+ // 侧栏入口按钮(docke2- 前缀):与 dsh 设置按钮(.VOzbGW_trigger)几何逐条对齐——
125
+ // 宽栏同为整宽左对齐行按钮(高 42、margin 4/-2、padding 0 10px 0 8px、圆角 12),窄栏同为 36 圆钮居中,
126
+ // 使两按钮上下成列、左右边缘完全对齐(此前右对齐/叠放都会错位)。
127
+ ".docke2-btn{box-sizing:border-box;cursor:pointer;display:flex;align-items:center;gap:8px;border:none;background:transparent;color:var(--dsw-alias-label-primary);border-radius:12px;width:calc(100% + 4px);height:42px;margin:4px -2px;padding:0 10px 0 8px;font-family:inherit;font-size:14px;line-height:22px;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
128
+ ".docke2-btn:hover{background:var(--dsw-alias-interactive-bg-hover);}",
129
+ ".docke2-btn.docke2-on{color:var(--dk-accent);background:var(--dk-accent-soft);}",
130
+ ".docke2-btn.docke2-rail{width:36px;height:36px;margin:8px 0 10px;padding:0;border-radius:50%;justify-content:center;gap:0;}",
131
+ ".docke2-label{white-space:nowrap;overflow:hidden;}",
132
+ // 遮罩固定用深色半透明:亮色主题下宿主 bg-layer-1 是纯白,白底遮罩会让弹窗与页面完全无层次
133
+ ".dockm-backdrop{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:rgba(15,17,21,.45);backdrop-filter:blur(4px);pointer-events:auto;animation:dockm-fade .15s var(--ds-ease-in-out);}",
134
+ "@keyframes dockm-fade{from{opacity:0}to{opacity:1}}",
135
+ ".dockm-dialog{box-sizing:border-box;position:relative;width:min(1080px,calc(100vw - 32px));height:min(700px,calc(100vh - 32px));display:flex;flex-direction:column;border-radius:16px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 20px 64px rgb(0 0 0 / .32);overflow:hidden;animation:dockm-pop .18s var(--ds-ease-in-out);}",
136
+ "@keyframes dockm-pop{from{opacity:0;transform:translateY(10px) scale(.985)}to{opacity:1;transform:none}}",
137
+ ".dockm-head{flex:none;display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l1);cursor:move;user-select:none;touch-action:none;}",
138
+ ".dockm-title{font-weight:600;font-size:14px;}",
139
+ ".dockm-sub{color:var(--dsw-alias-label-tertiary);font-size:12px;}",
140
+ ".dockm-ctrls{margin-left:auto;flex:none;display:flex;align-items:center;gap:2px;}",
141
+ ".dockm-close{cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;font-size:14px;}",
142
+ ".dockm-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
143
+ ".dockm-win{cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;font-size:13px;}",
144
+ ".dockm-win:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
145
+ ".dockm-dialog.dockm-max{border-radius:12px;}",
146
+ ".dockm-dialog.dockm-min{height:auto;min-height:0;}",
147
+ ".dockm-dialog.dockm-min .dockm-body{display:none;}",
148
+ ".dockm-resize{position:absolute;right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;z-index:3;touch-action:none;}",
149
+ ".dockm-body{flex:1;min-height:0;display:flex;}",
150
+ ".dockm-nav{flex:none;width:176px;display:flex;flex-direction:column;gap:2px;padding:10px 8px;overflow-y:auto;border-right:1px solid var(--dsw-alias-border-l1);}",
151
+ ".dockm-nav-item{display:flex;align-items:center;gap:8px;padding:8px 10px;border:none;background:transparent;border-radius:10px;cursor:pointer;color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:13px;text-align:left;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
152
+ ".dockm-nav-item:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
153
+ ".dockm-nav-item.on{background:var(--dk-accent-soft);color:var(--dsw-alias-label-primary);font-weight:600;}",
154
+ ".dockm-nav-item .dockm-badge{margin-left:auto;}",
155
+ ".dockm-dot{width:8px;height:8px;border-radius:50%;flex:none;}",
156
+ ".dockm-badge{flex:none;font-size:11px;border-radius:999px;padding:0 8px;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);}",
157
+ ".dockm-content{flex:1;min-width:0;display:flex;flex-direction:column;gap:12px;padding:16px 18px;overflow-y:auto;}",
158
+ ".dockm-content-head{display:flex;flex-direction:column;gap:4px;}",
159
+ ".dockm-name{display:flex;align-items:center;gap:8px;font-weight:600;font-size:15px;}",
160
+ ".dockm-desc{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;}",
161
+ ".dockm-view{display:flex;flex-direction:column;gap:8px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;}",
162
+ ".dockm-note{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;}",
163
+ ".dockm-err{color:var(--dsw-alias-state-error-primary);}",
164
+ ".dockm-foot{margin-top:auto;display:flex;align-items:center;gap:8px;color:var(--dsw-alias-label-tertiary);font-size:11px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;flex-wrap:wrap;}",
165
+ ".dockm-switch{cursor:pointer;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);border-radius:999px;padding:4px 12px;font-family:inherit;font-size:12px;margin-left:auto;}",
166
+ ".dockm-switch:hover{color:var(--dsw-alias-label-primary);}",
167
+ ".dockm-switch.on{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
168
+ // 首页总揽(dockh- 前缀):导航首项图标 + 模块卡片网格
169
+ ".dockm-navhome{display:inline-flex;align-items:center;color:var(--dk-accent);flex:none;}",
170
+ ".dockh-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px;}",
171
+ ".dockh-card{display:flex;flex-direction:column;gap:6px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:10px 12px;cursor:pointer;transition:border-color .15s var(--ds-ease-in-out);}",
172
+ ".dockh-card:hover,.dockh-card:focus-visible{border-color:var(--dk-accent);outline:none;}",
173
+ ".dockh-head{display:flex;align-items:center;gap:8px;}",
174
+ ".dockh-name{font-weight:600;flex:none;}",
175
+ ".dockh-badge{flex:none;font-size:11px;border-radius:999px;padding:0 8px;color:var(--dsw-alias-label-tertiary);border:1px solid var(--dsw-alias-border-l2);}",
176
+ // 状态标识(非交互:圆点+文字,与可点的开关一眼区分)
177
+ ".dockh-status{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;}",
178
+ ".dockh-sdot{width:7px;height:7px;border-radius:50%;flex:none;background:var(--dsw-alias-state-success-primary,#34d399);box-shadow:0 0 4px color-mix(in srgb,var(--dsw-alias-state-success-primary,#34d399) 55%,transparent);}",
179
+ ".dockh-status.off .dockh-sdot{background:transparent;border:1.5px solid var(--dsw-alias-label-tertiary);box-shadow:none;opacity:.75;}",
180
+ ".dockh-status.plan .dockh-sdot{background:transparent;border:1.5px dashed var(--dsw-alias-label-tertiary);box-shadow:none;}",
181
+ // iOS 风滑动开关(首页卡片 / 弹层页脚 / 设置页共用的启停控件)
182
+ ".dock-sw{flex:none;position:relative;width:34px;height:19px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;padding:0;transition:background .18s var(--ds-ease-in-out),border-color .18s var(--ds-ease-in-out);}",
183
+ ".dock-sw::after{content:\"\";position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgb(0 0 0 / .35);transition:transform .18s var(--ds-ease-in-out);}",
184
+ ".dock-sw:hover{border-color:var(--dk-accent);}",
185
+ ".dock-sw.on{background:var(--dsw-alias-state-success-primary,#34d399);border-color:transparent;}",
186
+ ".dock-sw.on::after{transform:translateX(15px);}",
187
+ // 弹层页脚:文字标签 + 开关 成组
188
+ ".dockm-foot-sw{margin-left:auto;flex:none;display:inline-flex;align-items:center;gap:8px;}",
189
+ ".dockm-foot-swlabel{font-size:11px;color:var(--dsw-alias-label-tertiary);}",
190
+ ".dockm-foot-swlabel.on{color:var(--dsw-alias-state-success-primary);}",
191
+ // 窄屏:功能坞是一级工作台而不是被压扁的桌面弹窗。全屏承接安全区,导航改横向,
192
+ // 禁用桌面拖拽/缩放,所有导航和关闭动作保持至少 44px 的可点击面积。
193
+ "@media (max-width:680px){.dockm-backdrop{align-items:stretch;justify-content:stretch;background:var(--dsw-alias-bg-layer-2);backdrop-filter:none}.dockm-dialog,.dockm-dialog.dockm-max{position:fixed!important;left:0!important;top:0!important;right:auto!important;bottom:auto!important;width:100dvw!important;height:100dvh!important;min-height:100dvh;border:0;border-radius:0;box-shadow:none}.dockm-dialog.dockm-min .dockm-body{display:flex}.dockm-head{min-height:60px;box-sizing:border-box;padding:calc(env(safe-area-inset-top) + 8px) 12px 8px;cursor:default;touch-action:manipulation}.dockm-sub,.dockm-win,.dockm-resize{display:none}.dockm-title{font-size:16px}.dockm-close{width:44px;height:44px;font-size:17px}.dockm-body{flex-direction:column;min-height:0}.dockm-nav{box-sizing:border-box;width:auto;max-width:100%;min-height:56px;flex-direction:row;gap:6px;padding:6px 10px;overflow-x:auto;overflow-y:hidden;border-right:0;border-bottom:1px solid var(--dsw-alias-border-l1);scrollbar-width:none;overscroll-behavior-x:contain}.dockm-nav::-webkit-scrollbar{display:none}.dockm-nav-item{flex:none;min-height:44px;padding:0 12px;font-size:13px;touch-action:manipulation}.dockm-nav-item .dockm-badge{display:none}.dockm-content{padding:16px max(16px,env(safe-area-inset-right)) calc(20px + env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left));gap:14px;overscroll-behavior:contain}.dockm-content-head{gap:5px}.dockm-name{font-size:16px}.dockm-desc{font-size:13px;line-height:1.55}.dockm-foot{padding-top:12px;font-size:11px}.dockm-foot-sw{width:100%;margin-left:0;justify-content:space-between}.dock-sw{width:44px;height:26px}.dock-sw::after{top:3px;left:3px;width:18px;height:18px}.dock-sw.on::after{transform:translateX(18px)}.dockh-grid{grid-template-columns:1fr;gap:10px}.dockh-card{padding:12px}.dockh-desc{font-size:13px}.dockh-go{font-size:12px}}",
194
+ "@media (prefers-reduced-motion:reduce){.dockm-backdrop,.dockm-dialog{animation:none}.dockm-nav-item,.dockm-close,.dockm-win,.dock-sw{transition:none}}",
195
+ ".dockh-desc{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;}",
196
+ ".dockh-stat{color:var(--dsw-alias-label-tertiary);font-size:12px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
197
+ ".dockh-foot{display:flex;align-items:center;gap:8px;}",
198
+ ".dockh-go{color:var(--dsw-alias-label-tertiary);font-size:11px;}",
199
+ // 会话输入区工具行 chips(dockchip- 前缀):余额/用量随身小控件,挂在模型选择器左侧
200
+ // 溢出兼容(两层防护,缺一不可):
201
+ // 1) 宽度上限:宿主 .row 是 flex-wrap:wrap 且换行优先于收缩,chips 一长 tools 整行变宽,
202
+ // 右侧模型选择器/发送键被挤到第二行(左右错位)。宿主 .row 带 container-type:inline-size,
203
+ // 用容器单位 cqw 让上限随输入卡宽度自适应(窄卡少占、宽卡多占);不支持 cqw 的老内核退回固定值。
204
+ // 注意:旧上限 min(280px,36cqw) 仍偏宽——标准 780px 输入卡下(.row 内容约 762px),
205
+ // add 钮(28)+工作区选择器(~214)+chips(280) 已与右侧模型选择器+发送键(~345px)合计超宽,
206
+ // 即便 chips 未截断 .row 也会换行把模型选择器/发送键挤到第二行。实测把上限收到 189px
207
+ // (≈25cqw)并使 chips 内距收紧(padding 2px 6px、gap 2px)后,实测 4~6M 用量 + 余额
208
+ // 两 chip 可完整显示且不再换行;超限时仍被省略号截断,完整值在 title 悬浮提示里。
209
+ // 2) 截断:超限部分用省略号截断(完整数值在 title 悬浮提示里),防 chips 凸出输入卡圆角(悬空)。
210
+ ".dockchip-row{display:inline-flex;align-items:center;gap:2px;min-width:0;flex:0 1 auto;overflow:hidden;max-width:189px;}",
211
+ "@supports (width:1cqw){.dockchip-row{max-width:min(189px,25cqw);}}",
212
+ ".dockchip{display:inline-flex;align-items:center;gap:5px;cursor:pointer;border:none;background:transparent;color:var(--dsw-alias-label-tertiary);border-radius:8px;padding:2px 6px;font-family:inherit;font-size:11px;line-height:18px;white-space:nowrap;min-width:0;overflow:hidden;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
213
+ ".dockchip > span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}",
214
+ ".dockchip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
215
+ ".dockchip .dockchip-dot{width:6px;height:6px;border-radius:50%;flex:none;}",
216
+ ".dockchip.err{color:var(--dsw-alias-state-error-primary);}"
217
+ ].join("\n");
218
+
219
+ let dockCssTag = null;
220
+ let initialCssSchedule = null;
221
+ function cancelInitialCssSchedule() {
222
+ if (!initialCssSchedule || typeof window === "undefined") return;
223
+ if (initialCssSchedule.type === "idle" && typeof window.cancelIdleCallback === "function") window.cancelIdleCallback(initialCssSchedule.id);
224
+ if (initialCssSchedule.type === "timer") window.clearTimeout(initialCssSchedule.id);
225
+ initialCssSchedule = null;
226
+ }
227
+ function fullDockCss() {
228
+ const parts = [SHELL_CSS];
229
+ for (const f of BUILTIN_FEATURES) if (f.css) parts.push(f.css);
230
+ for (const f of externalDefs) if (f.css) parts.push(f.css);
231
+ return parts.join("\n");
232
+ }
233
+ // 样式全局注入一次(入口按钮与右下角浮层挂在设置页之外,不能依赖页内 <style>)。
234
+ // 这里用 SHELL_CSS 唯一命名,并做防御:注入永不抛错、失败只降级不打断启动;
235
+ // 外部功能注册携带样式时可重算更新(外部样式后到)。
236
+ function ensureCss() {
237
+ if (typeof document === "undefined") return;
238
+ cancelInitialCssSchedule();
239
+ try {
240
+ if (!dockCssTag || !dockCssTag.isConnected) {
241
+ if (document.querySelector('style[data-plugin-css="dsh-dock"]')) {
242
+ dockCssTag = document.querySelector('style[data-plugin-css="dsh-dock"]');
243
+ } else {
244
+ dockCssTag = document.createElement("style");
245
+ dockCssTag.dataset.pluginCss = "dsh-dock";
246
+ document.head.appendChild(dockCssTag);
247
+ }
248
+ }
249
+ dockCssTag.textContent = fullDockCss();
250
+ } catch (e) {
251
+ console.error("[dsh-dock] ensureCss failed:", e && e.message ? e.message : String(e));
252
+ }
253
+ }
254
+ // 刷新页时宿主内容优先绘制:功能坞样式很大,首帧同步解析会让整页短暂停顿。
255
+ // 首次注入延后到浏览器空闲期;之后的外部模块注册仍走 ensureCss() 立即更新。
256
+ function scheduleInitialCss() {
257
+ if (typeof document === "undefined" || initialCssSchedule || (dockCssTag && dockCssTag.isConnected)) return;
258
+ const run = () => {
259
+ initialCssSchedule = null;
260
+ ensureCss();
261
+ };
262
+ if (typeof window !== "undefined" && typeof window.requestIdleCallback === "function") {
263
+ initialCssSchedule = { type: "idle", id: window.requestIdleCallback(run, { timeout: 350 }) };
264
+ } else {
265
+ initialCssSchedule = { type: "timer", id: setTimeout(run, 48) };
266
+ }
267
+ }
268
+
269
+ // ---- 外部功能变化订阅 hook(弹层/设置页重渲染用) ----
270
+ function useExternalVersion() {
271
+ const [, bump] = react.useReducer((n) => n + 1, 0);
272
+ react.useEffect(() => {
273
+ externalListeners.add(bump);
274
+ return () => { externalListeners.delete(bump); };
275
+ }, []);
276
+ }
277
+
278
+ // ---- 弹层窗口几何记忆(页面生命周期内):拖动/缩放后记住位置与尺寸,重开还原 ----
279
+ let lastGeom = { x: null, y: null, w: null, h: null };
280
+
281
+ function DockIcon(props) {
282
+ const size = (props && props.size) || 16;
283
+ return react.createElement("svg", { width: size, height: size, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, "aria-hidden": true },
284
+ react.createElement("rect", { x: 2.5, y: 2.5, width: 4.4, height: 4.4, rx: 1.2 }),
285
+ react.createElement("rect", { x: 9, y: 2.5, width: 4.4, height: 4.4, rx: 1.2 }),
286
+ react.createElement("rect", { x: 2.5, y: 9, width: 4.4, height: 4.4, rx: 1.2 }),
287
+ react.createElement("rect", { x: 9, y: 9, width: 4.4, height: 4.4, rx: 1.2 }));
288
+ }
289
+
290
+ function DockEntry(props) {
291
+ const wide = !!props.wide;
292
+ const [open, setOpen] = react.useState(panelNav.open);
293
+ react.useEffect(() => subscribePanel(() => setOpen(panelNav.open)), []);
294
+ return react.createElement("button", {
295
+ type: "button",
296
+ className: "docke2-btn" + (open ? " docke2-on" : "") + (wide ? "" : " docke2-rail"),
297
+ title: "功能坞",
298
+ "aria-label": "功能坞",
299
+ "aria-expanded": open,
300
+ onClick: () => setPanelOpen(!open)
301
+ }, react.createElement(DockIcon, { size: wide ? 16 : 18 }), wide ? react.createElement("span", { className: "docke2-label" }, "功能坞") : null);
302
+ }
303
+
304
+ // ---- 功能坞弹出面板:shell.overlay(仿 dsh 设置:居中模态 = 遮罩 + 对话框) ----
305
+ // 左侧导航:首项「首页」总揽(默认选中),其后是各功能模块(order 升序);planned 模块只占位展示
306
+ function DockModal() {
307
+ // 默认关闭;SSR/无浏览器环境下默认展开内容(便于冒烟测试渲染整棵弹层树)
308
+ // 导航总线(open/active/params)驱动:chips 点击 openPanel(...) 即可打开并定位
309
+ const [nav, setNav] = react.useState({ open: panelNav.open, active: panelNav.active, params: panelNav.params });
310
+ react.useEffect(() => subscribePanel(() => setNav({ open: panelNav.open, active: panelNav.active, params: panelNav.params })), []);
311
+ const open = nav.open || typeof document === "undefined";
312
+ const active = nav.active;
313
+ const setActive = navigatePanel;
314
+ const navParams = nav.params;
315
+ const [, force] = react.useReducer((n) => n + 1, 0);
316
+ useExternalVersion();
317
+ // ---- 窗口几何:普通(默认居中大窗)/最大化/最小化;拖动标题栏移动、右下角缩放 ----
318
+ // lastGeom 记住本页生命周期内最后一次几何,重开弹层时还原
319
+ const [win, setWin] = react.useState(() => ({ mode: "normal", x: null, y: null, w: null, h: null }));
320
+ const dlgRef = react.useRef(null);
321
+ const contentRef = react.useRef(null);
322
+ react.useEffect(() => {
323
+ if (lastGeom.w) setWin({ mode: "normal", x: lastGeom.x, y: lastGeom.y, w: lastGeom.w, h: lastGeom.h });
324
+ }, []);
325
+ // 切换左侧模块时总是从新页面顶部开始,避免把上一页的滚动位置带进小游戏等短页面。
326
+ react.useEffect(() => {
327
+ if (open && contentRef.current) contentRef.current.scrollTop = 0;
328
+ }, [active, open]);
329
+ function beginDrag(e, type) {
330
+ if (win.mode !== "normal" || e.button !== 0) return;
331
+ if (type === "move" && e.target && e.target.closest && e.target.closest("button,select,input")) return;
332
+ const node = dlgRef.current;
333
+ if (!node || typeof window === "undefined") return;
334
+ const rect = node.getBoundingClientRect();
335
+ const startX = e.clientX, startY = e.clientY;
336
+ const origin = { left: rect.left, top: rect.top, w: rect.width, h: rect.height };
337
+ const clampX = (v) => Math.min(Math.max(8, v), window.innerWidth - origin.w - 8);
338
+ const clampY = (v) => Math.min(Math.max(8, v), window.innerHeight - origin.h - 8);
339
+ const onMove = (ev) => {
340
+ const dx = ev.clientX - startX, dy = ev.clientY - startY;
341
+ if (type === "move") setWin((s) => Object.assign({}, s, { x: clampX(origin.left + dx), y: clampY(origin.top + dy) }));
342
+ else setWin((s) => Object.assign({}, s, {
343
+ w: Math.max(640, Math.min(origin.w + dx, window.innerWidth - 16)),
344
+ h: Math.max(420, Math.min(origin.h + dy, window.innerHeight - 16))
345
+ }));
346
+ };
347
+ const onUp = () => {
348
+ window.removeEventListener("pointermove", onMove);
349
+ window.removeEventListener("pointerup", onUp);
350
+ setWin((s) => { lastGeom = { x: s.x, y: s.y, w: s.w, h: s.h }; return s; });
351
+ };
352
+ window.addEventListener("pointermove", onMove);
353
+ window.addEventListener("pointerup", onUp);
354
+ }
355
+ if (!open) return null;
356
+ const MODULES = allModules();
357
+ const isHome = active === "home";
358
+ const mod = isHome ? null : (MODULES.find((m) => m.id === active) || MODULES[0]);
359
+ const st = mod ? stateOf(mod.id) : null;
360
+ // 外部包视图包错误边界;视图统一收到 { ctx, feature, params } props
361
+ //(params 来自导航总线:如 { provider } 高亮余额行、{ sessionId } 按会话筛选用量)
362
+ const View = mod ? mod.View : null;
363
+ const viewNode = mod && View
364
+ ? react.createElement("div", { className: "dockm-view" },
365
+ react.createElement(mod.external ? FeatureBoundary : react.Fragment, null,
366
+ react.createElement(View, { ctx: ctxRef.current, feature: mod, params: navParams })))
367
+ : null;
368
+ const enabledCount = MODULES.filter((m) => { const s = stateOf(m.id); return !!(s && s.enabled); }).length;
369
+ // 最大化:铺满视口(留 10px 边);被拖过/缩放过:固定坐标;否则 CSS 默认居中
370
+ const dialogStyle = win.mode === "max"
371
+ ? { position: "fixed", left: 10, top: 10, right: 10, bottom: 10, width: "auto", height: "auto" }
372
+ : (win.x != null || win.y != null || win.w != null || win.h != null)
373
+ ? {
374
+ position: "fixed",
375
+ left: win.x != null ? win.x : undefined,
376
+ top: win.y != null ? win.y : undefined,
377
+ width: win.w != null ? win.w : undefined,
378
+ height: win.mode === "min" ? "auto" : (win.h != null ? win.h : undefined)
379
+ }
380
+ : null;
381
+ return react.createElement("div", { className: "dockm-backdrop", onClick: () => setPanelOpen(false) },
382
+ react.createElement("div", {
383
+ className: "dockm-dialog" + (win.mode === "max" ? " dockm-max" : "") + (win.mode === "min" ? " dockm-min" : ""),
384
+ style: dialogStyle,
385
+ ref: dlgRef,
386
+ onClick: (e) => e.stopPropagation()
387
+ },
388
+ react.createElement("div", {
389
+ className: "dockm-head",
390
+ onPointerDown: (e) => beginDrag(e, "move"),
391
+ onDoubleClick: () => setWin((s) => Object.assign({}, s, { mode: s.mode === "max" ? "normal" : "max" }))
392
+ },
393
+ react.createElement(DockIcon, null),
394
+ react.createElement("span", { className: "dockm-title" }, "功能坞"),
395
+ react.createElement("span", { className: "dockm-sub" }, "dsh-dock · 也可在 设置 → 功能坞 打开管理页"),
396
+ react.createElement("span", { className: "dockm-ctrls" },
397
+ react.createElement("button", {
398
+ type: "button",
399
+ className: "dockm-win",
400
+ title: win.mode === "min" ? "还原" : "最小化",
401
+ onClick: () => setWin((s) => Object.assign({}, s, { mode: s.mode === "min" ? "normal" : "min" }))
402
+ }, "▁"),
403
+ react.createElement("button", {
404
+ type: "button",
405
+ className: "dockm-win",
406
+ title: win.mode === "max" ? "还原" : "最大化",
407
+ onClick: () => setWin((s) => Object.assign({}, s, { mode: s.mode === "max" ? "normal" : "max" }))
408
+ }, win.mode === "max" ? "❐" : "▢"),
409
+ react.createElement("button", { type: "button", className: "dockm-close", "aria-label": "关闭", title: "关闭", onClick: () => setPanelOpen(false) }, "✕"))),
410
+ react.createElement("div", { className: "dockm-body" },
411
+ react.createElement("nav", { className: "dockm-nav", "aria-label": "功能模块" },
412
+ react.createElement("button", {
413
+ type: "button",
414
+ key: "home",
415
+ className: "dockm-nav-item" + (isHome ? " on" : ""),
416
+ onClick: () => setActive("home")
417
+ },
418
+ react.createElement("span", { className: "dockm-navhome" }, react.createElement(DockIcon, null)),
419
+ react.createElement("span", null, "首页")),
420
+ MODULES.map((m) =>
421
+ react.createElement("button", {
422
+ type: "button",
423
+ key: m.id,
424
+ className: "dockm-nav-item" + (m.id === active ? " on" : ""),
425
+ onClick: () => setActive(m.id)
426
+ },
427
+ react.createElement("span", { className: "dockm-dot", style: { background: m.accent } }),
428
+ react.createElement("span", null, m.name),
429
+ m.planned ? react.createElement("span", { className: "dockm-badge" }, "规划中") : null,
430
+ m.external ? react.createElement("span", { className: "dockm-badge" }, "外部") : null))),
431
+ react.createElement("div", { className: "dockm-content", ref: contentRef },
432
+ react.createElement("div", { className: "dockm-content-head" },
433
+ react.createElement("div", { className: "dockm-name" },
434
+ isHome
435
+ ? react.createElement("span", { className: "dockm-navhome" }, react.createElement(DockIcon, null))
436
+ : react.createElement("span", { className: "dockm-dot", style: { background: mod.accent } }),
437
+ isHome ? "首页" : mod.name,
438
+ !isHome && mod.planned ? react.createElement("span", { className: "dockm-badge" }, "规划中") : null,
439
+ !isHome && mod.external ? react.createElement("span", { className: "dockm-badge" }, "外部包" + (mod.package ? " · " + mod.package : "")) : null),
440
+ react.createElement("div", { className: "dockm-desc" },
441
+ isHome
442
+ ? "所有子功能总揽:运行状态、概要与快捷开关,点击卡片进入对应功能页。"
443
+ : mod.description)),
444
+ isHome
445
+ ? react.createElement("div", { className: "dockm-view" }, react.createElement(HomeView, { ctx: ctxRef.current, onOpen: setActive, onToggle: force }))
446
+ : mod.planned
447
+ ? react.createElement("div", { className: "dockm-note" }, PLANNED_NOTES[mod.id] || "待接入:见 README 路线图")
448
+ : st && st.enabled && viewNode
449
+ ? viewNode
450
+ : st && st.error
451
+ ? react.createElement("div", { className: "dockm-note dockm-err" }, "功能出错:" + st.error)
452
+ : react.createElement("div", { className: "dockm-note" }, "该功能当前为停用状态(开关已持久化,重启后保持)"),
453
+ react.createElement("div", { className: "dockm-foot" },
454
+ react.createElement("span", null, isHome
455
+ ? "功能坞 v" + DOCK_VERSION + " · 共 " + MODULES.length + " 个功能模块," + enabledCount + " 个已启用"
456
+ : "功能坞 v" + DOCK_VERSION + " · 新功能按路线图追加"),
457
+ !isHome && mod && !mod.planned && st
458
+ ? react.createElement("span", { className: "dockm-foot-sw" },
459
+ react.createElement("span", { className: "dockm-foot-swlabel" + (st.enabled ? " on" : "") }, st.enabled ? "已启用" : "已停用"),
460
+ react.createElement("button", {
461
+ type: "button",
462
+ className: "dock-sw" + (st.enabled ? " on" : ""),
463
+ role: "switch",
464
+ "aria-checked": !!st.enabled,
465
+ "aria-label": (st.enabled ? "停用" : "启用") + mod.name,
466
+ title: st.enabled ? "停用「" + mod.name + "」" : "启用「" + mod.name + "」",
467
+ onClick: () => { toggleFeature(mod.id); force(); }
468
+ }))
469
+ : null,
470
+ !isHome && mod && typeof mod.Chip === "function"
471
+ ? react.createElement("span", { className: "dockm-foot-sw" },
472
+ react.createElement("span", { className: "dockm-foot-swlabel" + (chipShown(mod.id) ? " on" : "") }, "会话页小控件"),
473
+ react.createElement("button", {
474
+ type: "button",
475
+ className: "dock-sw" + (chipShown(mod.id) ? " on" : ""),
476
+ role: "switch",
477
+ "aria-checked": chipShown(mod.id),
478
+ "aria-label": (chipShown(mod.id) ? "隐藏" : "显示") + mod.name + "的会话页小控件",
479
+ title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
480
+ onClick: () => { setChipShown(mod.id, !chipShown(mod.id)); force(); }
481
+ }))
482
+ : null))),
483
+ win.mode === "normal"
484
+ ? react.createElement("div", { className: "dockm-resize", onPointerDown: (e) => beginDrag(e, "size") })
485
+ : null));
486
+ }
487
+
488
+ // ---- 首页总揽:每个功能模块一张卡片(状态标识 + 运行概要 + 启停开关),点卡片进对应功能 ----
489
+ // 交互约定:状态是「圆点+文字」纯标识(不可点);启停是 iOS 滑动开关(明显可点);点卡片其余区域跳详情页。
490
+ function HomeView(props) {
491
+ const ctx = props && props.ctx;
492
+ const [, force] = react.useReducer((n) => n + 1, 0);
493
+ useExternalVersion();
494
+ const open = (id) => { if (props && typeof props.onOpen === "function") props.onOpen(id); };
495
+ return react.createElement("div", { className: "dockh-grid" },
496
+ allModules().map((m) => {
497
+ const st = stateOf(m.id);
498
+ const enabled = !!(st && st.enabled);
499
+ const Stat = m.HomeStat;
500
+ const statNode = m.planned
501
+ ? react.createElement("span", null, PLANNED_NOTES[m.id] || "待接入:见 README 路线图")
502
+ : enabled && Stat
503
+ ? react.createElement(m.external ? FeatureBoundary : react.Fragment, null,
504
+ react.createElement(Stat, { ctx: ctx }))
505
+ : react.createElement("span", null, "已停用,启用后在此展示运行概要");
506
+ return react.createElement("div", {
507
+ key: m.id,
508
+ className: "dockh-card",
509
+ role: "button",
510
+ tabIndex: 0,
511
+ onClick: () => open(m.id),
512
+ onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(m.id); } }
513
+ },
514
+ react.createElement("div", { className: "dockh-head" },
515
+ react.createElement("span", { className: "dockm-dot", style: { background: m.accent } }),
516
+ react.createElement("span", { className: "dockh-name" }, m.name),
517
+ m.external ? react.createElement("span", { className: "dockh-badge", title: m.package || undefined }, "外部") : null,
518
+ // 状态标识:圆点 + 文字(纯展示,与开关视觉区分)
519
+ react.createElement("span", { className: "dockh-status" + (m.planned ? " plan" : enabled ? "" : " off") },
520
+ react.createElement("span", { className: "dockh-sdot" }),
521
+ react.createElement("span", null, m.planned ? "规划中" : enabled ? "运行中" : "已停用")),
522
+ // 启停开关(规划中的功能不显示)
523
+ m.planned ? null : react.createElement("button", {
524
+ type: "button",
525
+ className: "dock-sw" + (enabled ? " on" : ""),
526
+ role: "switch",
527
+ "aria-checked": enabled,
528
+ "aria-label": (enabled ? "停用" : "启用") + m.name,
529
+ title: enabled ? "停用「" + m.name + "」" : "启用「" + m.name + "」",
530
+ onClick: (e) => {
531
+ e.stopPropagation();
532
+ toggleFeature(m.id);
533
+ // onToggle = DockModal 的 force:连带刷新弹层脚注的「N 个已启用」统计
534
+ if (props && typeof props.onToggle === "function") props.onToggle();
535
+ force();
536
+ }
537
+ })),
538
+ react.createElement("div", { className: "dockh-desc" }, m.description),
539
+ react.createElement("div", { className: "dockh-stat" }, statNode),
540
+ react.createElement("div", { className: "dockh-foot" },
541
+ react.createElement("span", { className: "dockh-go" }, "查看详情 →")));
542
+ }));
543
+ }
544
+
545
+ // ---- 设置页管理面板(settings.section):所有功能模块卡片一览 ----
546
+ function DockPanel() {
547
+ const ctx = ctxRef.current;
548
+ const [, force] = react.useReducer((n) => n + 1, 0);
549
+ useExternalVersion();
550
+ const toggle = (id) => {
551
+ toggleFeature(id);
552
+ force();
553
+ };
554
+ return react.createElement("div", { className: "dock-root" },
555
+ react.createElement("div", { className: "dock-intro" },
556
+ "功能坞(dsh-dock)· 所有小功能集中在这一个面板里管理。v0.4.0 起每个功能是独立模块(features/<id>/),",
557
+ "可单独提取打包发布(scripts/extract-feature.mjs);独立发布的功能包装回后经 dockBridge 注册进本面板。"),
558
+ allModules().map((f) => {
559
+ const st = stateOf(f.id);
560
+ const View = f.View;
561
+ const viewNode = (!f.planned && st.enabled && View)
562
+ ? react.createElement("div", { className: "dock-body" },
563
+ react.createElement(f.external ? FeatureBoundary : react.Fragment, null,
564
+ react.createElement(View, { ctx: ctx, feature: f })))
565
+ : null;
566
+ return react.createElement("div", { className: "dock-card", key: f.id },
567
+ react.createElement("div", { className: "dock-card-head" },
568
+ react.createElement("span", { className: "dock-dot" + (st.error ? " err" : st.enabled ? " on" : "") }),
569
+ react.createElement("span", { className: "dock-name" }, f.name),
570
+ react.createElement("span", { className: "dock-desc" }, f.description + (f.external ? "(来自外部包" + (f.package ? " " + f.package : "") + ")" : "")),
571
+ f.planned
572
+ ? react.createElement("span", { className: "dock-badge" }, "规划中")
573
+ : react.createElement("button", {
574
+ type: "button",
575
+ className: "dock-sw" + (st.enabled ? " on" : ""),
576
+ role: "switch",
577
+ "aria-checked": st.enabled,
578
+ "aria-label": (st.enabled ? "停用" : "启用") + f.name,
579
+ title: st.enabled ? "停用「" + f.name + "」" : "启用「" + f.name + "」",
580
+ onClick: () => toggle(f.id)
581
+ }),
582
+ !f.planned && typeof f.Chip === "function"
583
+ ? react.createElement("span", { className: "dockm-foot-sw" },
584
+ react.createElement("span", { className: "dockm-foot-swlabel" + (chipShown(f.id) ? " on" : "") }, "会话页小控件"),
585
+ react.createElement("button", {
586
+ type: "button",
587
+ className: "dock-sw" + (chipShown(f.id) ? " on" : ""),
588
+ role: "switch",
589
+ "aria-checked": chipShown(f.id),
590
+ "aria-label": (chipShown(f.id) ? "隐藏" : "显示") + f.name + "的会话页小控件",
591
+ title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
592
+ onClick: () => { setChipShown(f.id, !chipShown(f.id)); force(); }
593
+ }))
594
+ : null),
595
+ f.planned
596
+ ? react.createElement("div", { className: "dock-body" }, PLANNED_NOTES[f.id] || "待接入:见 README 路线图")
597
+ : st.error ? react.createElement("div", { className: "dock-body dockm-err" }, "功能出错:" + st.error) : null,
598
+ viewNode);
599
+ }));
600
+ }
601
+
602
+ // ---- 会话输入区工具行 chips:已启用功能的随身小控件(模型选择器左侧) ----
603
+ // 功能模块在 feature 描述符上挂可选 Chip 组件(props: { ctx, session, sessionId, input, feature }),
604
+ // 启用即在 conversation.input.left 渲染;点击通常经 openPanel(...) 打开功能坞定位到对应功能页。
605
+ function DockChips(props) {
606
+ const [, force] = react.useReducer((n) => n + 1, 0);
607
+ react.useEffect(() => subscribeFeatureState(() => force()), []);
608
+ const items = [];
609
+ for (const f of allModules()) {
610
+ if (f.planned || !stateOf(f.id).enabled || !chipShown(f.id) || typeof f.Chip !== "function") continue;
611
+ items.push(react.createElement(f.Chip, {
612
+ key: f.id, ctx: props.ctx, feature: f,
613
+ session: props.session, sessionId: props.sessionId, input: props.input,
614
+ }));
615
+ }
616
+ if (items.length === 0) return null;
617
+ return react.createElement("div", { className: "dockchip-row" }, items);
618
+ }
619
+
620
+ // ---- 功能全局浮层:模块可在描述符上挂 Overlay 组件(props: { ctx, feature }) ----
621
+ // 与 Chip(会话输入区小控件)不同,Overlay 是常驻整页挂载的全局 UI(如任务动画的动效与通知栈),
622
+ // 只要所属功能启用就渲染(功能停用即卸载,浮层自身负责按数据状态决定显示什么)。
623
+ function FeatureOverlays() {
624
+ const [, force] = react.useReducer((n) => n + 1, 0);
625
+ useExternalVersion();
626
+ react.useEffect(() => subscribeFeatureState(() => force()), []);
627
+ const items = [];
628
+ for (const f of allModules()) {
629
+ if (f.planned || !stateOf(f.id).enabled || typeof f.Overlay !== "function") continue;
630
+ items.push(react.createElement(FeatureBoundary, { key: f.id },
631
+ react.createElement(f.Overlay, { ctx: ctxRef.current, feature: f })));
632
+ }
633
+ if (items.length === 0) return null;
634
+ return react.createElement(react.Fragment, null, items);
635
+ }
636
+
637
+ // ---- 插件应用:注册四处 UI(入口按钮 / 弹层 / 设置页 / 会话区 chips),注入样式 ----
638
+ // ctx 以 ref 供视图组件使用(timer/theme 等服务按需自取;重装/HMR 时更新)
639
+ const ctxRef = { current: null };
640
+
641
+ export function apply(ctx) {
642
+ ctxRef.current = ctx;
643
+ scheduleInitialCss();
644
+ initFeatureState(BUILTIN_FEATURES.concat(PLANNED_FEATURES));
645
+ const slots = ctx.get("slots");
646
+ if (slots === undefined) return;
647
+
648
+ slots.inject("sidebar.footer.action", () => slots.register(
649
+ { name: "sidebar.footer.action", id: "dsh-dock", order: 1, label: "功能坞" },
650
+ (props) => react.createElement(DockEntry, props)));
651
+ slots.inject("shell.overlay", () => slots.register(
652
+ { name: "shell.overlay", id: "dsh-dock-panel", order: 21, label: "功能坞面板" },
653
+ () => react.createElement(DockModal, null)));
654
+ // 功能全局浮层(任务动画的动效与通知等):已启用功能的 Overlay 常驻挂载
655
+ slots.inject("shell.overlay", () => slots.register(
656
+ { name: "shell.overlay", id: "dsh-dock-feature-overlays", order: 22, label: "功能坞全局浮层" },
657
+ () => react.createElement(FeatureOverlays, null)));
658
+ slots.inject("settings.section", () => slots.register(
659
+ { name: "settings.section", id: "dsh-dock", order: 90, label: "功能坞" },
660
+ () => react.createElement(DockPanel, null)));
661
+ // 会话输入卡工具行左端(模型选择器左侧):已启用功能的随身小控件
662
+ slots.inject("conversation.input.left", () => slots.register(
663
+ { name: "conversation.input.left", id: "dsh-dock-chips", order: 10, label: "功能坞" },
664
+ (zone) => react.createElement(DockChips, Object.assign({}, zone, { ctx: ctxRef.current }))));
665
+ }
666
+
667
+ export const inject = ["timer"];
668
+ export { dockBridge };