dsh-dock 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -15
- package/README.md +170 -37
- package/client.js +2136 -97
- package/features/balance/host.js +494 -0
- package/features/balance/view.js +303 -0
- package/features/heartbeat/view.js +34 -0
- package/features/modelconfig/host.js +403 -0
- package/features/modelconfig/view.js +446 -0
- package/features/theme/view.js +35 -0
- package/features/tokenlog/host.js +678 -0
- package/features/tokenlog/view.jsx +568 -0
- package/features/visionproxy/host.js +301 -0
- package/index.js +50 -36
- package/package.json +19 -5
- package/src/client.jsx +549 -0
- package/src/host-core.js +54 -0
- package/src/shared.js +108 -0
package/src/client.jsx
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
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
|
+
|
|
27
|
+
const name = "dsh-dock";
|
|
28
|
+
const DOCK_VERSION = "0.4.0";
|
|
29
|
+
|
|
30
|
+
// ---- 内置功能注册表:新功能 = features/<id>/ 加模块 + 这里 import 一行 ----
|
|
31
|
+
const BUILTIN_FEATURES = [fTokenlog, fModelconfig, fHeartbeat, fTheme, fBalance];
|
|
32
|
+
// 规划占位(路线图):接入后移除并建 features/<id>/ 模块
|
|
33
|
+
const PLANNED_FEATURES = [
|
|
34
|
+
{ id: "animation", name: "任务动画", order: 130, accent: "#f472b6", planned: true, description: "接入路线图 0.5.0:任务进度动画与通知" },
|
|
35
|
+
];
|
|
36
|
+
const PLANNED_NOTES = {
|
|
37
|
+
animation: "待接入(路线图 0.5.0):任务进度动画与完成通知。",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ---- 外部功能注册表(dockBridge 回装通道) ----
|
|
41
|
+
const externalDefs = [];
|
|
42
|
+
const externalListeners = new Set();
|
|
43
|
+
function notifyExternal() {
|
|
44
|
+
for (const fn of externalListeners) fn();
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 外部功能桥:独立功能包经 require("dsh-dock").dockBridge 注册视图。
|
|
48
|
+
* def: { id, name, order?, accent?, description?, css?, View, HomeStat?, package? }
|
|
49
|
+
* 返回注销函数。重复 id 视为更新(HMR/重注册场景)。
|
|
50
|
+
*/
|
|
51
|
+
const dockBridge = {
|
|
52
|
+
version: DOCK_VERSION,
|
|
53
|
+
register(def) {
|
|
54
|
+
if (!def || typeof def !== "object" || typeof def.id !== "string" || !def.id || typeof def.View !== "function") {
|
|
55
|
+
console.error("[dsh-dock] dockBridge.register 参数不符(需 { id, name, View }):", def);
|
|
56
|
+
return () => {};
|
|
57
|
+
}
|
|
58
|
+
const entry = {
|
|
59
|
+
id: def.id,
|
|
60
|
+
name: String(def.name || def.id),
|
|
61
|
+
order: typeof def.order === "number" ? def.order : 500,
|
|
62
|
+
accent: typeof def.accent === "string" ? def.accent : "#94a3b8",
|
|
63
|
+
description: String(def.description || ""),
|
|
64
|
+
css: typeof def.css === "string" ? def.css : "",
|
|
65
|
+
View: def.View,
|
|
66
|
+
HomeStat: typeof def.HomeStat === "function" ? def.HomeStat : 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
|
+
".dock-root{display:flex;flex-direction:column;gap:12px;padding:4px 0;color:var(--dsw-alias-label-primary);font-size:13px;}",
|
|
99
|
+
".dock-intro{color:var(--dsw-alias-label-secondary);line-height:1.6;}",
|
|
100
|
+
".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;}",
|
|
101
|
+
".dock-card-head{display:flex;align-items:center;gap:8px;}",
|
|
102
|
+
".dock-dot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-label-secondary);flex:none;}",
|
|
103
|
+
".dock-dot.on{background:var(--dsw-alias-state-success-primary);}",
|
|
104
|
+
".dock-dot.err{background:var(--dsw-alias-state-error-primary);}",
|
|
105
|
+
".dock-name{font-weight:600;flex:none;}",
|
|
106
|
+
".dock-desc{color:var(--dsw-alias-label-secondary);flex:1;}",
|
|
107
|
+
".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;}",
|
|
108
|
+
".dock-switch.on{color:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary);}",
|
|
109
|
+
".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;}",
|
|
110
|
+
".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;}",
|
|
111
|
+
// 侧栏入口按钮(docke2- 前缀)+ 功能坞弹出面板(dockm- 前缀,仿 dsh 设置的居中模态:遮罩 + 对话框,左导航 + 右内容)
|
|
112
|
+
".docke2-btn{box-sizing:border-box;cursor:pointer;display:inline-flex;align-items:center;gap:6px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);border-radius:10px;height:32px;font-family:inherit;font-size:13px;line-height:32px;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
|
|
113
|
+
".docke2-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
114
|
+
".docke2-btn.docke2-on{color:var(--dsw-alias-accent,#4d9fff);background:var(--dsw-alias-accent-soft,color-mix(in srgb,var(--dsw-alias-accent,#4d9fff) 12%,transparent));}",
|
|
115
|
+
".docke2-label{white-space:nowrap;overflow:hidden;}",
|
|
116
|
+
".dockm-backdrop{position:fixed;inset:0;z-index:80;display:flex;align-items:center;justify-content:center;background:color-mix(in srgb,var(--dsw-alias-bg-layer-1) 55%,transparent);backdrop-filter:blur(4px);pointer-events:auto;animation:dockm-fade .15s var(--ds-ease-in-out);}",
|
|
117
|
+
"@keyframes dockm-fade{from{opacity:0}to{opacity:1}}",
|
|
118
|
+
".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);}",
|
|
119
|
+
"@keyframes dockm-pop{from{opacity:0;transform:translateY(10px) scale(.985)}to{opacity:1;transform:none}}",
|
|
120
|
+
".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;}",
|
|
121
|
+
".dockm-title{font-weight:600;font-size:14px;}",
|
|
122
|
+
".dockm-sub{color:var(--dsw-alias-label-tertiary);font-size:12px;}",
|
|
123
|
+
".dockm-ctrls{margin-left:auto;flex:none;display:flex;align-items:center;gap:2px;}",
|
|
124
|
+
".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;}",
|
|
125
|
+
".dockm-close:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
126
|
+
".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;}",
|
|
127
|
+
".dockm-win:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
128
|
+
".dockm-dialog.dockm-max{border-radius:12px;}",
|
|
129
|
+
".dockm-dialog.dockm-min{height:auto;min-height:0;}",
|
|
130
|
+
".dockm-dialog.dockm-min .dockm-body{display:none;}",
|
|
131
|
+
".dockm-resize{position:absolute;right:0;bottom:0;width:16px;height:16px;cursor:nwse-resize;z-index:3;touch-action:none;}",
|
|
132
|
+
".dockm-body{flex:1;min-height:0;display:flex;}",
|
|
133
|
+
".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);}",
|
|
134
|
+
".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);}",
|
|
135
|
+
".dockm-nav-item:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
136
|
+
".dockm-nav-item.on{background:var(--dsw-alias-accent-soft,color-mix(in srgb,var(--dsw-alias-accent,#4d9fff) 12%,transparent));color:var(--dsw-alias-label-primary);font-weight:600;}",
|
|
137
|
+
".dockm-nav-item .dockm-badge{margin-left:auto;}",
|
|
138
|
+
".dockm-dot{width:8px;height:8px;border-radius:50%;flex:none;}",
|
|
139
|
+
".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);}",
|
|
140
|
+
".dockm-content{flex:1;min-width:0;display:flex;flex-direction:column;gap:12px;padding:16px 18px;overflow-y:auto;}",
|
|
141
|
+
".dockm-content-head{display:flex;flex-direction:column;gap:4px;}",
|
|
142
|
+
".dockm-name{display:flex;align-items:center;gap:8px;font-weight:600;font-size:15px;}",
|
|
143
|
+
".dockm-desc{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.6;}",
|
|
144
|
+
".dockm-view{display:flex;flex-direction:column;gap:8px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:10px;}",
|
|
145
|
+
".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;}",
|
|
146
|
+
".dockm-err{color:var(--dsw-alias-state-error-primary);}",
|
|
147
|
+
".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;}",
|
|
148
|
+
".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;}",
|
|
149
|
+
".dockm-switch:hover{color:var(--dsw-alias-label-primary);}",
|
|
150
|
+
".dockm-switch.on{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
|
|
151
|
+
// 首页总揽(dockh- 前缀):导航首项图标 + 模块卡片网格
|
|
152
|
+
".dockm-navhome{display:inline-flex;align-items:center;color:var(--dsw-alias-accent,#4d9fff);flex:none;}",
|
|
153
|
+
".dockh-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:10px;}",
|
|
154
|
+
".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);}",
|
|
155
|
+
".dockh-card:hover,.dockh-card:focus-visible{border-color:var(--dsw-alias-accent,#4d9fff);outline:none;}",
|
|
156
|
+
".dockh-head{display:flex;align-items:center;gap:8px;}",
|
|
157
|
+
".dockh-name{font-weight:600;flex:none;}",
|
|
158
|
+
".dockh-badge{margin-left:auto;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);}",
|
|
159
|
+
".dockh-badge.on{color:var(--dsw-alias-state-success-primary);border-color:currentColor;}",
|
|
160
|
+
".dockh-badge.off{border-style:dashed;}",
|
|
161
|
+
".dockh-desc{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;}",
|
|
162
|
+
".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;}",
|
|
163
|
+
".dockh-foot{display:flex;align-items:center;gap:8px;}",
|
|
164
|
+
".dockh-go{color:var(--dsw-alias-label-tertiary);font-size:11px;}",
|
|
165
|
+
// 会话输入区工具行 chips(dockchip- 前缀):余额/用量随身小控件,挂在模型选择器左侧
|
|
166
|
+
".dockchip-row{display:inline-flex;align-items:center;gap:4px;min-width:0;}",
|
|
167
|
+
".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 8px;font-family:inherit;font-size:11px;line-height:18px;white-space:nowrap;transition:background .15s var(--ds-ease-in-out),color .15s var(--ds-ease-in-out);}",
|
|
168
|
+
".dockchip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);}",
|
|
169
|
+
".dockchip .dockchip-dot{width:6px;height:6px;border-radius:50%;flex:none;}",
|
|
170
|
+
".dockchip.err{color:var(--dsw-alias-state-error-primary);}"
|
|
171
|
+
].join("\n");
|
|
172
|
+
|
|
173
|
+
let dockCssTag = null;
|
|
174
|
+
function fullDockCss() {
|
|
175
|
+
const parts = [SHELL_CSS];
|
|
176
|
+
for (const f of BUILTIN_FEATURES) if (f.css) parts.push(f.css);
|
|
177
|
+
for (const f of externalDefs) if (f.css) parts.push(f.css);
|
|
178
|
+
return parts.join("\n");
|
|
179
|
+
}
|
|
180
|
+
// 样式全局注入一次(入口按钮与右下角浮层挂在设置页之外,不能依赖页内 <style>)。
|
|
181
|
+
// 这里用 SHELL_CSS 唯一命名,并做防御:注入永不抛错、失败只降级不打断启动;
|
|
182
|
+
// 外部功能注册携带样式时可重算更新(外部样式后到)。
|
|
183
|
+
function ensureCss() {
|
|
184
|
+
if (typeof document === "undefined") return;
|
|
185
|
+
try {
|
|
186
|
+
if (!dockCssTag || !dockCssTag.isConnected) {
|
|
187
|
+
if (document.querySelector('style[data-plugin-css="dsh-dock"]')) {
|
|
188
|
+
dockCssTag = document.querySelector('style[data-plugin-css="dsh-dock"]');
|
|
189
|
+
} else {
|
|
190
|
+
dockCssTag = document.createElement("style");
|
|
191
|
+
dockCssTag.dataset.pluginCss = "dsh-dock";
|
|
192
|
+
document.head.appendChild(dockCssTag);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
dockCssTag.textContent = fullDockCss();
|
|
196
|
+
} catch (e) {
|
|
197
|
+
console.error("[dsh-dock] ensureCss failed:", e && e.message ? e.message : String(e));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---- 外部功能变化订阅 hook(弹层/设置页重渲染用) ----
|
|
202
|
+
function useExternalVersion() {
|
|
203
|
+
const [, bump] = react.useReducer((n) => n + 1, 0);
|
|
204
|
+
react.useEffect(() => {
|
|
205
|
+
externalListeners.add(bump);
|
|
206
|
+
return () => { externalListeners.delete(bump); };
|
|
207
|
+
}, []);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---- 弹层窗口几何记忆(页面生命周期内):拖动/缩放后记住位置与尺寸,重开还原 ----
|
|
211
|
+
let lastGeom = { x: null, y: null, w: null, h: null };
|
|
212
|
+
|
|
213
|
+
function DockIcon() {
|
|
214
|
+
return react.createElement("svg", { width: 16, height: 16, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, "aria-hidden": true },
|
|
215
|
+
react.createElement("rect", { x: 2.5, y: 2.5, width: 4.4, height: 4.4, rx: 1.2 }),
|
|
216
|
+
react.createElement("rect", { x: 9, y: 2.5, width: 4.4, height: 4.4, rx: 1.2 }),
|
|
217
|
+
react.createElement("rect", { x: 2.5, y: 9, width: 4.4, height: 4.4, rx: 1.2 }),
|
|
218
|
+
react.createElement("rect", { x: 9, y: 9, width: 4.4, height: 4.4, rx: 1.2 }));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function DockEntry(props) {
|
|
222
|
+
const wide = !!props.wide;
|
|
223
|
+
const [open, setOpen] = react.useState(panelNav.open);
|
|
224
|
+
react.useEffect(() => subscribePanel(() => setOpen(panelNav.open)), []);
|
|
225
|
+
return react.createElement("button", {
|
|
226
|
+
type: "button",
|
|
227
|
+
className: "docke2-btn" + (open ? " docke2-on" : ""),
|
|
228
|
+
style: wide
|
|
229
|
+
// 与设置按钮精确同框:设置 trigger 宽栏 = 42px 高、margin-top 4 → 下移 46px、高度同为 42,
|
|
230
|
+
// 使本按钮的四条边与设置按钮完全重合(同一水平线);rail = 36px、margin-top 8 → 下移 44px、高 36 亦然。
|
|
231
|
+
// zIndex 保证盖在满宽设置按钮之上(其右侧为空白区,不遮齿轮图标)
|
|
232
|
+
? { marginLeft: "auto", transform: "translateY(46px)", zIndex: 1, height: 42, lineHeight: "42px", padding: "0 12px" }
|
|
233
|
+
: { transform: "translateY(44px)", zIndex: 1, width: 36, height: 36, justifyContent: "center", padding: 0 },
|
|
234
|
+
title: "功能坞",
|
|
235
|
+
"aria-label": "功能坞",
|
|
236
|
+
"aria-expanded": open,
|
|
237
|
+
onClick: () => setPanelOpen(!open)
|
|
238
|
+
}, react.createElement(DockIcon, null), wide ? react.createElement("span", { className: "docke2-label" }, "功能坞") : null);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---- 功能坞弹出面板:shell.overlay(仿 dsh 设置:居中模态 = 遮罩 + 对话框) ----
|
|
242
|
+
// 左侧导航:首项「首页」总揽(默认选中),其后是各功能模块(order 升序);planned 模块只占位展示
|
|
243
|
+
function DockModal() {
|
|
244
|
+
// 默认关闭;SSR/无浏览器环境下默认展开内容(便于冒烟测试渲染整棵弹层树)
|
|
245
|
+
// 导航总线(open/active/params)驱动:chips 点击 openPanel(...) 即可打开并定位
|
|
246
|
+
const [nav, setNav] = react.useState({ open: panelNav.open, active: panelNav.active, params: panelNav.params });
|
|
247
|
+
react.useEffect(() => subscribePanel(() => setNav({ open: panelNav.open, active: panelNav.active, params: panelNav.params })), []);
|
|
248
|
+
const open = nav.open || typeof document === "undefined";
|
|
249
|
+
const active = nav.active;
|
|
250
|
+
const setActive = navigatePanel;
|
|
251
|
+
const navParams = nav.params;
|
|
252
|
+
const [, force] = react.useReducer((n) => n + 1, 0);
|
|
253
|
+
useExternalVersion();
|
|
254
|
+
// ---- 窗口几何:普通(默认居中大窗)/最大化/最小化;拖动标题栏移动、右下角缩放 ----
|
|
255
|
+
// lastGeom 记住本页生命周期内最后一次几何,重开弹层时还原
|
|
256
|
+
const [win, setWin] = react.useState(() => ({ mode: "normal", x: null, y: null, w: null, h: null }));
|
|
257
|
+
const dlgRef = react.useRef(null);
|
|
258
|
+
react.useEffect(() => {
|
|
259
|
+
if (lastGeom.w) setWin({ mode: "normal", x: lastGeom.x, y: lastGeom.y, w: lastGeom.w, h: lastGeom.h });
|
|
260
|
+
}, []);
|
|
261
|
+
function beginDrag(e, type) {
|
|
262
|
+
if (win.mode !== "normal" || e.button !== 0) return;
|
|
263
|
+
if (type === "move" && e.target && e.target.closest && e.target.closest("button,select,input")) return;
|
|
264
|
+
const node = dlgRef.current;
|
|
265
|
+
if (!node || typeof window === "undefined") return;
|
|
266
|
+
const rect = node.getBoundingClientRect();
|
|
267
|
+
const startX = e.clientX, startY = e.clientY;
|
|
268
|
+
const origin = { left: rect.left, top: rect.top, w: rect.width, h: rect.height };
|
|
269
|
+
const clampX = (v) => Math.min(Math.max(8, v), window.innerWidth - origin.w - 8);
|
|
270
|
+
const clampY = (v) => Math.min(Math.max(8, v), window.innerHeight - origin.h - 8);
|
|
271
|
+
const onMove = (ev) => {
|
|
272
|
+
const dx = ev.clientX - startX, dy = ev.clientY - startY;
|
|
273
|
+
if (type === "move") setWin((s) => Object.assign({}, s, { x: clampX(origin.left + dx), y: clampY(origin.top + dy) }));
|
|
274
|
+
else setWin((s) => Object.assign({}, s, {
|
|
275
|
+
w: Math.max(640, Math.min(origin.w + dx, window.innerWidth - 16)),
|
|
276
|
+
h: Math.max(420, Math.min(origin.h + dy, window.innerHeight - 16))
|
|
277
|
+
}));
|
|
278
|
+
};
|
|
279
|
+
const onUp = () => {
|
|
280
|
+
window.removeEventListener("pointermove", onMove);
|
|
281
|
+
window.removeEventListener("pointerup", onUp);
|
|
282
|
+
setWin((s) => { lastGeom = { x: s.x, y: s.y, w: s.w, h: s.h }; return s; });
|
|
283
|
+
};
|
|
284
|
+
window.addEventListener("pointermove", onMove);
|
|
285
|
+
window.addEventListener("pointerup", onUp);
|
|
286
|
+
}
|
|
287
|
+
if (!open) return null;
|
|
288
|
+
const MODULES = allModules();
|
|
289
|
+
const isHome = active === "home";
|
|
290
|
+
const mod = isHome ? null : (MODULES.find((m) => m.id === active) || MODULES[0]);
|
|
291
|
+
const st = mod ? stateOf(mod.id) : null;
|
|
292
|
+
// 外部包视图包错误边界;视图统一收到 { ctx, feature, params } props
|
|
293
|
+
//(params 来自导航总线:如 { provider } 高亮余额行、{ sessionId } 按会话筛选用量)
|
|
294
|
+
const View = mod ? mod.View : null;
|
|
295
|
+
const viewNode = mod && View
|
|
296
|
+
? react.createElement("div", { className: "dockm-view" },
|
|
297
|
+
react.createElement(mod.external ? FeatureBoundary : react.Fragment, null,
|
|
298
|
+
react.createElement(View, { ctx: ctxRef.current, feature: mod, params: navParams })))
|
|
299
|
+
: null;
|
|
300
|
+
const enabledCount = MODULES.filter((m) => { const s = stateOf(m.id); return !!(s && s.enabled); }).length;
|
|
301
|
+
// 最大化:铺满视口(留 10px 边);被拖过/缩放过:固定坐标;否则 CSS 默认居中
|
|
302
|
+
const dialogStyle = win.mode === "max"
|
|
303
|
+
? { position: "fixed", left: 10, top: 10, right: 10, bottom: 10, width: "auto", height: "auto" }
|
|
304
|
+
: (win.x != null || win.y != null || win.w != null || win.h != null)
|
|
305
|
+
? {
|
|
306
|
+
position: "fixed",
|
|
307
|
+
left: win.x != null ? win.x : undefined,
|
|
308
|
+
top: win.y != null ? win.y : undefined,
|
|
309
|
+
width: win.w != null ? win.w : undefined,
|
|
310
|
+
height: win.mode === "min" ? "auto" : (win.h != null ? win.h : undefined)
|
|
311
|
+
}
|
|
312
|
+
: null;
|
|
313
|
+
return react.createElement("div", { className: "dockm-backdrop", onClick: () => setPanelOpen(false) },
|
|
314
|
+
react.createElement("div", {
|
|
315
|
+
className: "dockm-dialog" + (win.mode === "max" ? " dockm-max" : "") + (win.mode === "min" ? " dockm-min" : ""),
|
|
316
|
+
style: dialogStyle,
|
|
317
|
+
ref: dlgRef,
|
|
318
|
+
onClick: (e) => e.stopPropagation()
|
|
319
|
+
},
|
|
320
|
+
react.createElement("div", {
|
|
321
|
+
className: "dockm-head",
|
|
322
|
+
onPointerDown: (e) => beginDrag(e, "move"),
|
|
323
|
+
onDoubleClick: () => setWin((s) => Object.assign({}, s, { mode: s.mode === "max" ? "normal" : "max" }))
|
|
324
|
+
},
|
|
325
|
+
react.createElement(DockIcon, null),
|
|
326
|
+
react.createElement("span", { className: "dockm-title" }, "功能坞"),
|
|
327
|
+
react.createElement("span", { className: "dockm-sub" }, "dsh-dock · 也可在 设置 → 功能坞 打开管理页"),
|
|
328
|
+
react.createElement("span", { className: "dockm-ctrls" },
|
|
329
|
+
react.createElement("button", {
|
|
330
|
+
type: "button",
|
|
331
|
+
className: "dockm-win",
|
|
332
|
+
title: win.mode === "min" ? "还原" : "最小化",
|
|
333
|
+
onClick: () => setWin((s) => Object.assign({}, s, { mode: s.mode === "min" ? "normal" : "min" }))
|
|
334
|
+
}, "▁"),
|
|
335
|
+
react.createElement("button", {
|
|
336
|
+
type: "button",
|
|
337
|
+
className: "dockm-win",
|
|
338
|
+
title: win.mode === "max" ? "还原" : "最大化",
|
|
339
|
+
onClick: () => setWin((s) => Object.assign({}, s, { mode: s.mode === "max" ? "normal" : "max" }))
|
|
340
|
+
}, win.mode === "max" ? "❐" : "▢"),
|
|
341
|
+
react.createElement("button", { type: "button", className: "dockm-close", "aria-label": "关闭", title: "关闭", onClick: () => setPanelOpen(false) }, "✕"))),
|
|
342
|
+
react.createElement("div", { className: "dockm-body" },
|
|
343
|
+
react.createElement("nav", { className: "dockm-nav", "aria-label": "功能模块" },
|
|
344
|
+
react.createElement("button", {
|
|
345
|
+
type: "button",
|
|
346
|
+
key: "home",
|
|
347
|
+
className: "dockm-nav-item" + (isHome ? " on" : ""),
|
|
348
|
+
onClick: () => setActive("home")
|
|
349
|
+
},
|
|
350
|
+
react.createElement("span", { className: "dockm-navhome" }, react.createElement(DockIcon, null)),
|
|
351
|
+
react.createElement("span", null, "首页")),
|
|
352
|
+
MODULES.map((m) =>
|
|
353
|
+
react.createElement("button", {
|
|
354
|
+
type: "button",
|
|
355
|
+
key: m.id,
|
|
356
|
+
className: "dockm-nav-item" + (m.id === active ? " on" : ""),
|
|
357
|
+
onClick: () => setActive(m.id)
|
|
358
|
+
},
|
|
359
|
+
react.createElement("span", { className: "dockm-dot", style: { background: m.accent } }),
|
|
360
|
+
react.createElement("span", null, m.name),
|
|
361
|
+
m.planned ? react.createElement("span", { className: "dockm-badge" }, "规划中") : null,
|
|
362
|
+
m.external ? react.createElement("span", { className: "dockm-badge" }, "外部") : null))),
|
|
363
|
+
react.createElement("div", { className: "dockm-content" },
|
|
364
|
+
react.createElement("div", { className: "dockm-content-head" },
|
|
365
|
+
react.createElement("div", { className: "dockm-name" },
|
|
366
|
+
isHome
|
|
367
|
+
? react.createElement("span", { className: "dockm-navhome" }, react.createElement(DockIcon, null))
|
|
368
|
+
: react.createElement("span", { className: "dockm-dot", style: { background: mod.accent } }),
|
|
369
|
+
isHome ? "首页" : mod.name,
|
|
370
|
+
!isHome && mod.planned ? react.createElement("span", { className: "dockm-badge" }, "规划中") : null,
|
|
371
|
+
!isHome && mod.external ? react.createElement("span", { className: "dockm-badge" }, "外部包" + (mod.package ? " · " + mod.package : "")) : null),
|
|
372
|
+
react.createElement("div", { className: "dockm-desc" },
|
|
373
|
+
isHome
|
|
374
|
+
? "所有子功能总揽:运行状态、概要与快捷开关,点击卡片进入对应功能页。"
|
|
375
|
+
: mod.description)),
|
|
376
|
+
isHome
|
|
377
|
+
? react.createElement("div", { className: "dockm-view" }, react.createElement(HomeView, { ctx: ctxRef.current, onOpen: setActive, onToggle: force }))
|
|
378
|
+
: mod.planned
|
|
379
|
+
? react.createElement("div", { className: "dockm-note" }, PLANNED_NOTES[mod.id] || "待接入:见 README 路线图")
|
|
380
|
+
: st && st.enabled && viewNode
|
|
381
|
+
? viewNode
|
|
382
|
+
: st && st.error
|
|
383
|
+
? react.createElement("div", { className: "dockm-note dockm-err" }, "功能出错:" + st.error)
|
|
384
|
+
: react.createElement("div", { className: "dockm-note" }, "该功能当前为停用状态(记忆态随页面生命周期,0.5.0 起持久化)"),
|
|
385
|
+
react.createElement("div", { className: "dockm-foot" },
|
|
386
|
+
react.createElement("span", null, isHome
|
|
387
|
+
? "功能坞 v" + DOCK_VERSION + " · 共 " + MODULES.length + " 个功能模块," + enabledCount + " 个已启用"
|
|
388
|
+
: "功能坞 v" + DOCK_VERSION + " · 新功能按路线图追加"),
|
|
389
|
+
!isHome && mod && !mod.planned && st
|
|
390
|
+
? react.createElement("button", {
|
|
391
|
+
type: "button",
|
|
392
|
+
className: "dockm-switch" + (st.enabled ? " on" : ""),
|
|
393
|
+
onClick: () => { toggleFeature(mod.id); force(); }
|
|
394
|
+
}, st.enabled ? "已启用(点击停用)" : "已停用(点击启用)")
|
|
395
|
+
: null,
|
|
396
|
+
!isHome && mod && typeof mod.Chip === "function"
|
|
397
|
+
? react.createElement("button", {
|
|
398
|
+
type: "button",
|
|
399
|
+
className: "dockm-switch" + (chipShown(mod.id) ? " on" : ""),
|
|
400
|
+
title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
|
|
401
|
+
onClick: () => { setChipShown(mod.id, !chipShown(mod.id)); force(); }
|
|
402
|
+
}, chipShown(mod.id) ? "会话页显示中" : "会话页已隐藏")
|
|
403
|
+
: null))),
|
|
404
|
+
win.mode === "normal"
|
|
405
|
+
? react.createElement("div", { className: "dockm-resize", onPointerDown: (e) => beginDrag(e, "size") })
|
|
406
|
+
: null));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ---- 首页总揽:每个功能模块一张卡片(状态徽章 + 运行概要 + 快捷开关),点击进入对应功能 ----
|
|
410
|
+
function HomeView(props) {
|
|
411
|
+
const ctx = props && props.ctx;
|
|
412
|
+
const [, force] = react.useReducer((n) => n + 1, 0);
|
|
413
|
+
useExternalVersion();
|
|
414
|
+
const open = (id) => { if (props && typeof props.onOpen === "function") props.onOpen(id); };
|
|
415
|
+
return react.createElement("div", { className: "dockh-grid" },
|
|
416
|
+
allModules().map((m) => {
|
|
417
|
+
const st = stateOf(m.id);
|
|
418
|
+
const enabled = !!(st && st.enabled);
|
|
419
|
+
const Stat = m.HomeStat;
|
|
420
|
+
const statNode = m.planned
|
|
421
|
+
? react.createElement("span", null, PLANNED_NOTES[m.id] || "待接入:见 README 路线图")
|
|
422
|
+
: enabled && Stat
|
|
423
|
+
? react.createElement(m.external ? FeatureBoundary : react.Fragment, null,
|
|
424
|
+
react.createElement(Stat, { ctx: ctx }))
|
|
425
|
+
: react.createElement("span", null, "已停用,启用后在此展示运行概要");
|
|
426
|
+
return react.createElement("div", {
|
|
427
|
+
key: m.id,
|
|
428
|
+
className: "dockh-card",
|
|
429
|
+
role: "button",
|
|
430
|
+
tabIndex: 0,
|
|
431
|
+
onClick: () => open(m.id),
|
|
432
|
+
onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(m.id); } }
|
|
433
|
+
},
|
|
434
|
+
react.createElement("div", { className: "dockh-head" },
|
|
435
|
+
react.createElement("span", { className: "dockm-dot", style: { background: m.accent } }),
|
|
436
|
+
react.createElement("span", { className: "dockh-name" }, m.name),
|
|
437
|
+
m.external ? react.createElement("span", { className: "dockh-badge", title: m.package || undefined }, "外部") : null,
|
|
438
|
+
react.createElement("span", { className: "dockh-badge" + (m.planned ? "" : enabled ? " on" : " off") },
|
|
439
|
+
m.planned ? "规划中" : enabled ? "已启用" : "已停用")),
|
|
440
|
+
react.createElement("div", { className: "dockh-desc" }, m.description),
|
|
441
|
+
react.createElement("div", { className: "dockh-stat" }, statNode),
|
|
442
|
+
react.createElement("div", { className: "dockh-foot" },
|
|
443
|
+
react.createElement("span", { className: "dockh-go" }, "查看详情 →"),
|
|
444
|
+
m.planned ? null : react.createElement("button", {
|
|
445
|
+
type: "button",
|
|
446
|
+
className: "dockm-switch" + (enabled ? " on" : ""),
|
|
447
|
+
onClick: (e) => {
|
|
448
|
+
e.stopPropagation();
|
|
449
|
+
toggleFeature(m.id);
|
|
450
|
+
// onToggle = DockModal 的 force:连带刷新弹层脚注的「N 个已启用」统计
|
|
451
|
+
if (props && typeof props.onToggle === "function") props.onToggle();
|
|
452
|
+
force();
|
|
453
|
+
}
|
|
454
|
+
}, enabled ? "停用" : "启用")));
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ---- 设置页管理面板(settings.section):所有功能模块卡片一览 ----
|
|
459
|
+
function DockPanel() {
|
|
460
|
+
const ctx = ctxRef.current;
|
|
461
|
+
const [, force] = react.useReducer((n) => n + 1, 0);
|
|
462
|
+
useExternalVersion();
|
|
463
|
+
const toggle = (id) => {
|
|
464
|
+
toggleFeature(id);
|
|
465
|
+
force();
|
|
466
|
+
};
|
|
467
|
+
return react.createElement("div", { className: "dock-root" },
|
|
468
|
+
react.createElement("div", { className: "dock-intro" },
|
|
469
|
+
"功能坞(dsh-dock)· 所有小功能集中在这一个面板里管理。v0.4.0 起每个功能是独立模块(features/<id>/),",
|
|
470
|
+
"可单独提取打包发布(scripts/extract-feature.mjs);独立发布的功能包装回后经 dockBridge 注册进本面板。"),
|
|
471
|
+
allModules().map((f) => {
|
|
472
|
+
const st = stateOf(f.id);
|
|
473
|
+
const View = f.View;
|
|
474
|
+
const viewNode = (!f.planned && st.enabled && View)
|
|
475
|
+
? react.createElement("div", { className: "dock-body" },
|
|
476
|
+
react.createElement(f.external ? FeatureBoundary : react.Fragment, null,
|
|
477
|
+
react.createElement(View, { ctx: ctx, feature: f })))
|
|
478
|
+
: null;
|
|
479
|
+
return react.createElement("div", { className: "dock-card", key: f.id },
|
|
480
|
+
react.createElement("div", { className: "dock-card-head" },
|
|
481
|
+
react.createElement("span", { className: "dock-dot" + (st.error ? " err" : st.enabled ? " on" : "") }),
|
|
482
|
+
react.createElement("span", { className: "dock-name" }, f.name),
|
|
483
|
+
react.createElement("span", { className: "dock-desc" }, f.description + (f.external ? "(来自外部包" + (f.package ? " " + f.package : "") + ")" : "")),
|
|
484
|
+
f.planned
|
|
485
|
+
? react.createElement("span", { className: "dock-badge" }, "规划中")
|
|
486
|
+
: react.createElement("button", {
|
|
487
|
+
className: "dock-switch" + (st.enabled ? " on" : ""),
|
|
488
|
+
onClick: () => toggle(f.id)
|
|
489
|
+
}, st.enabled ? "已启用" : "已停用"),
|
|
490
|
+
!f.planned && typeof f.Chip === "function"
|
|
491
|
+
? react.createElement("button", {
|
|
492
|
+
className: "dock-switch" + (chipShown(f.id) ? " on" : ""),
|
|
493
|
+
title: "控制会话输入区(模型选择器左侧)是否显示本功能的随身小控件",
|
|
494
|
+
onClick: () => { setChipShown(f.id, !chipShown(f.id)); force(); }
|
|
495
|
+
}, chipShown(f.id) ? "会话页显示" : "会话页隐藏")
|
|
496
|
+
: null),
|
|
497
|
+
f.planned
|
|
498
|
+
? react.createElement("div", { className: "dock-body" }, PLANNED_NOTES[f.id] || "待接入:见 README 路线图")
|
|
499
|
+
: st.error ? react.createElement("div", { className: "dock-body dockm-err" }, "功能出错:" + st.error) : null,
|
|
500
|
+
viewNode);
|
|
501
|
+
}));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---- 会话输入区工具行 chips:已启用功能的随身小控件(模型选择器左侧) ----
|
|
505
|
+
// 功能模块在 feature 描述符上挂可选 Chip 组件(props: { ctx, session, sessionId, input, feature }),
|
|
506
|
+
// 启用即在 conversation.input.left 渲染;点击通常经 openPanel(...) 打开功能坞定位到对应功能页。
|
|
507
|
+
function DockChips(props) {
|
|
508
|
+
const [, force] = react.useReducer((n) => n + 1, 0);
|
|
509
|
+
react.useEffect(() => subscribeFeatureState(() => force()), []);
|
|
510
|
+
const items = [];
|
|
511
|
+
for (const f of allModules()) {
|
|
512
|
+
if (f.planned || !stateOf(f.id).enabled || !chipShown(f.id) || typeof f.Chip !== "function") continue;
|
|
513
|
+
items.push(react.createElement(f.Chip, {
|
|
514
|
+
key: f.id, ctx: props.ctx, feature: f,
|
|
515
|
+
session: props.session, sessionId: props.sessionId, input: props.input,
|
|
516
|
+
}));
|
|
517
|
+
}
|
|
518
|
+
if (items.length === 0) return null;
|
|
519
|
+
return react.createElement("div", { className: "dockchip-row" }, items);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ---- 插件应用:注册四处 UI(入口按钮 / 弹层 / 设置页 / 会话区 chips),注入样式 ----
|
|
523
|
+
// ctx 以 ref 供视图组件使用(timer/theme 等服务按需自取;重装/HMR 时更新)
|
|
524
|
+
const ctxRef = { current: null };
|
|
525
|
+
|
|
526
|
+
export function apply(ctx) {
|
|
527
|
+
ctxRef.current = ctx;
|
|
528
|
+
ensureCss();
|
|
529
|
+
initFeatureState(BUILTIN_FEATURES.concat(PLANNED_FEATURES));
|
|
530
|
+
const slots = ctx.get("slots");
|
|
531
|
+
if (slots === undefined) return;
|
|
532
|
+
|
|
533
|
+
slots.inject("sidebar.footer.action", () => slots.register(
|
|
534
|
+
{ name: "sidebar.footer.action", id: "dsh-dock", order: 1, label: "功能坞" },
|
|
535
|
+
(props) => react.createElement(DockEntry, props)));
|
|
536
|
+
slots.inject("shell.overlay", () => slots.register(
|
|
537
|
+
{ name: "shell.overlay", id: "dsh-dock-panel", order: 21, label: "功能坞面板" },
|
|
538
|
+
() => react.createElement(DockModal, null)));
|
|
539
|
+
slots.inject("settings.section", () => slots.register(
|
|
540
|
+
{ name: "settings.section", id: "dsh-dock", order: 90, label: "功能坞" },
|
|
541
|
+
() => react.createElement(DockPanel, null)));
|
|
542
|
+
// 会话输入卡工具行左端(模型选择器左侧):已启用功能的随身小控件
|
|
543
|
+
slots.inject("conversation.input.left", () => slots.register(
|
|
544
|
+
{ name: "conversation.input.left", id: "dsh-dock-chips", order: 10, label: "功能坞" },
|
|
545
|
+
(zone) => react.createElement(DockChips, Object.assign({}, zone, { ctx: ctxRef.current }))));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export const inject = ["timer"];
|
|
549
|
+
export { dockBridge };
|
package/src/host-core.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// dsh-dock · 宿主共享内核:各功能模块(features/*/host.js)共用的常量与工具。
|
|
2
|
+
// 放在 src/ 而非 features/:不属于任何单个功能;提取功能模块独立成包时按需随包复制。
|
|
3
|
+
import z from '@deepseek-ai/schemastery'
|
|
4
|
+
|
|
5
|
+
/** dsh-dock 自有 settings 命名空间(插件级配置,如图片理解代理)。 */
|
|
6
|
+
export const DOCK_NS = 'dsh-dock'
|
|
7
|
+
|
|
8
|
+
/** 自有命名空间的 schema(当前仅图片理解代理配置)。 */
|
|
9
|
+
export const DockConfig = z.object({
|
|
10
|
+
visionProxy: z.object({
|
|
11
|
+
enabled: z.boolean().default(false),
|
|
12
|
+
provider: z.string().default(''),
|
|
13
|
+
model: z.string().default(''),
|
|
14
|
+
}).default({}),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
/** 沿 settingsPath 走一层对象(user/base/value 都可用)。 */
|
|
18
|
+
export function walkPath(node, path) {
|
|
19
|
+
for (const key of path || []) {
|
|
20
|
+
node = node && typeof node === 'object' ? node[key] : undefined
|
|
21
|
+
}
|
|
22
|
+
return node
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 序列化 JSON 响应。 */
|
|
26
|
+
export function sendJson(res, status, value) {
|
|
27
|
+
res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
|
|
28
|
+
res.end(JSON.stringify(value))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 解析 POST 请求体 JSON(限长 2MB)。 */
|
|
32
|
+
export function readBody(req) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
let size = 0
|
|
35
|
+
const chunks = []
|
|
36
|
+
req.on('data', (c) => {
|
|
37
|
+
size += c.length
|
|
38
|
+
if (size > 2 * 1024 * 1024) {
|
|
39
|
+
reject(new Error('请求体过大'))
|
|
40
|
+
req.destroy()
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
chunks.push(c)
|
|
44
|
+
})
|
|
45
|
+
req.on('end', () => {
|
|
46
|
+
try {
|
|
47
|
+
resolve(chunks.length === 0 ? {} : JSON.parse(Buffer.concat(chunks).toString('utf8')))
|
|
48
|
+
} catch (e) {
|
|
49
|
+
reject(new Error('请求体不是合法 JSON'))
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
req.on('error', reject)
|
|
53
|
+
})
|
|
54
|
+
}
|