dsh-dock 0.11.2 → 0.11.5
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 +18 -18
- package/README.md +255 -396
- package/client.js +596 -98
- package/cordis.patch.yml +2 -2
- package/features/animation/host.js +119 -119
- package/features/animation/view.jsx +1662 -1662
- package/features/balance/host.js +494 -494
- package/features/balance/view.js +351 -351
- package/features/games/games/breakout.jsx +227 -227
- package/features/games/games/gomoku.jsx +244 -244
- package/features/games/games/racer.jsx +169 -169
- package/features/games/games/shared.jsx +118 -118
- package/features/games/games/snake.jsx +144 -144
- package/features/games/games/sokoban.jsx +180 -180
- package/features/games/games/tank.jsx +128 -128
- package/features/games/games/tetris.jsx +308 -308
- package/features/games/games/xiangqi.jsx +277 -277
- package/features/games/tank-core.js +152 -152
- package/features/games/view.jsx +639 -639
- package/features/games/xiangqi-core.js +299 -299
- package/features/heartbeat/view.js +35 -35
- package/features/mobile-relay/gateway.js +472 -472
- package/features/mobile-relay/host.js +622 -570
- package/features/mobile-relay/view.jsx +380 -300
- package/features/modelconfig/host.js +406 -406
- package/features/modelconfig/view.js +490 -490
- package/features/notify/host.js +374 -374
- package/features/notify/view.jsx +751 -751
- package/features/theme/view.js +61 -61
- package/features/tokenlog/host.js +882 -883
- package/features/tokenlog/view.jsx +1369 -932
- package/features/visionproxy/host.js +315 -315
- package/index.js +195 -195
- package/package.json +66 -64
- package/screenshots/dock-tokenlog-charts-2.png +0 -0
- package/screenshots/dock-tokenlog-charts-3.png +0 -0
- package/screenshots/dock-tokenlog-charts.png +0 -0
- package/src/client.jsx +677 -677
- package/src/host-core.js +207 -207
- package/src/shared.js +192 -192
- package/src/task-track.js +354 -359
package/src/shared.js
CHANGED
|
@@ -1,192 +1,192 @@
|
|
|
1
|
-
// dsh-dock · 客户端共享工具(外壳与功能模块共用;提取功能模块独立成包时随包复制)
|
|
2
|
-
import react from "react";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 功能视图错误边界:视图渲染抛错时降级为一行错误提示,不拖垮整块面板 UI。
|
|
6
|
-
*
|
|
7
|
-
* ⚠️ 内置视图也必须包(v0.9.9 起):宿主把每个插槽条目包在 SlotErrorBoundary 里,
|
|
8
|
-
* 条目内任何渲染抛错都会被宿主换成空 div——表现为「点开功能坞界面整个没了」而侧栏
|
|
9
|
-
* 入口按钮仍是选中态。v0.9.3~0.9.8 的 animation 视图就是这样被一个 ReferenceError 打没的。
|
|
10
|
-
* label 用于提示里点明是哪个功能;调用处应带 key(切换功能时重建,否则错误态会跟着串页)。
|
|
11
|
-
*/
|
|
12
|
-
export class FeatureBoundary extends react.Component {
|
|
13
|
-
constructor(props) {
|
|
14
|
-
super(props);
|
|
15
|
-
this.state = { error: null };
|
|
16
|
-
}
|
|
17
|
-
static getDerivedStateFromError(error) {
|
|
18
|
-
return { error: error };
|
|
19
|
-
}
|
|
20
|
-
componentDidCatch(error) {
|
|
21
|
-
console.error("[dsh-dock] 功能视图渲染出错:", error);
|
|
22
|
-
}
|
|
23
|
-
render() {
|
|
24
|
-
if (this.state.error) {
|
|
25
|
-
const msg = this.state.error && this.state.error.message ? this.state.error.message : String(this.state.error);
|
|
26
|
-
const who = this.props && this.props.label ? "「" + this.props.label + "」" : "功能视图";
|
|
27
|
-
return react.createElement("div", { className: "dockm-note dockm-err" },
|
|
28
|
-
who + "渲染出错:" + msg + "(已隔离,不影响面板其他功能;详情见浏览器控制台)");
|
|
29
|
-
}
|
|
30
|
-
return this.props.children;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ---- 功能开关状态(外壳面板与会话区 chips 共用;每功能开关 localStorage 持久化,重启后保持) ----
|
|
35
|
-
const featureState = new Map();
|
|
36
|
-
const stateListeners = new Set();
|
|
37
|
-
function notifyState() {
|
|
38
|
-
for (const fn of stateListeners) fn();
|
|
39
|
-
}
|
|
40
|
-
// 开关持久化(dsh-dock/features/v1):id -> boolean;未记录过的功能用模块默认值
|
|
41
|
-
const FEATURE_STORE_KEY = "dsh-dock/features/v1";
|
|
42
|
-
const featurePersist = { map: {} };
|
|
43
|
-
try {
|
|
44
|
-
if (typeof localStorage !== "undefined") {
|
|
45
|
-
const raw = localStorage.getItem(FEATURE_STORE_KEY);
|
|
46
|
-
const obj = raw ? JSON.parse(raw) : null;
|
|
47
|
-
if (obj && typeof obj === "object") featurePersist.map = obj;
|
|
48
|
-
}
|
|
49
|
-
} catch { /* localStorage 不可用时用默认值 */ }
|
|
50
|
-
function persistFeatureEnabled() {
|
|
51
|
-
try {
|
|
52
|
-
if (typeof localStorage !== "undefined") localStorage.setItem(FEATURE_STORE_KEY, JSON.stringify(featurePersist.map));
|
|
53
|
-
} catch { /* 持久化失败静默 */ }
|
|
54
|
-
}
|
|
55
|
-
/** 外壳 apply 时初始化内置功能的默认开关(持久化过的值优先于模块默认)。
|
|
56
|
-
* 默认全部停用、按需开启:仅当模块显式声明 defaultEnabled: true 才默认启用。 */
|
|
57
|
-
export function initFeatureState(defs) {
|
|
58
|
-
for (const f of defs) {
|
|
59
|
-
if (featureState.has(f.id)) continue;
|
|
60
|
-
let enabled = !f.planned && f.defaultEnabled === true;
|
|
61
|
-
const saved = featurePersist.map[f.id];
|
|
62
|
-
if (typeof saved === "boolean") enabled = saved;
|
|
63
|
-
featureState.set(f.id, { enabled, error: null });
|
|
64
|
-
}
|
|
65
|
-
// 宿主侧持久化开关(settings,跨浏览器/重启)优先:拉过来覆盖本地默认值。
|
|
66
|
-
// localStorage 里已有记录的 id 视为用户在这台浏览器已表态,保持本地值;
|
|
67
|
-
// 若宿主从未收到过该功能的开关,则把本浏览器的选择补推一次(见 syncFeatureStateFromHost)。
|
|
68
|
-
syncFeatureStateFromHost(defs);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** 从宿主 /dsh-dock/features 拉取持久化开关,覆盖尚未本地表态的功能默认值。
|
|
72
|
-
* 拉取失败(宿主旧版本无此路由)静默降级——沿用本地默认。 */
|
|
73
|
-
function syncFeatureStateFromHost(defs) {
|
|
74
|
-
if (typeof fetch !== "function") return;
|
|
75
|
-
fetch("/dsh-dock/features").then(async (res) => {
|
|
76
|
-
if (!res.ok) return;
|
|
77
|
-
const data = await res.json().catch(() => null);
|
|
78
|
-
const persisted = data && data.ok && data.data && data.data.persisted;
|
|
79
|
-
if (!persisted || typeof persisted !== "object") return;
|
|
80
|
-
let changed = false;
|
|
81
|
-
for (const f of defs) {
|
|
82
|
-
const local = featurePersist.map[f.id];
|
|
83
|
-
const host = persisted[f.id];
|
|
84
|
-
if (local === undefined) {
|
|
85
|
-
// 本浏览器没表态:跟随宿主持久化值
|
|
86
|
-
if (typeof host === "boolean") {
|
|
87
|
-
const st = stateOf(f.id);
|
|
88
|
-
if (st.enabled !== host) { st.enabled = host; changed = true; }
|
|
89
|
-
}
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
// 本浏览器已表态,但宿主从未收到过这个功能的开关(persisted 里根本没有它):
|
|
93
|
-
// 补推一次本浏览器的选择。否则会出现「面板显示已启用、宿主侧却从未 setup」——
|
|
94
|
-
// 该功能的路由整片 404,面板还会误报「宿主进程是旧版本」。宿主刚升级、
|
|
95
|
-
// 或新功能刚拆出来时最容易踩(「任务通知」拆分时就是这样:点开关时宿主
|
|
96
|
-
// 还是旧进程 → POST 404 → 开关只留在浏览器本地,重启后宿主永远不知道)。
|
|
97
|
-
// 注意只在「宿主没有记录」时补推:宿主已显式记过 false 说明是别处关掉的,
|
|
98
|
-
// 不能拿本浏览器的旧值去覆盖(多设备场景)。
|
|
99
|
-
if (typeof host !== "boolean") pushFeatureEnabledToHost(f.id, local);
|
|
100
|
-
}
|
|
101
|
-
if (changed) notifyState();
|
|
102
|
-
}).catch(() => { /* 宿主不可达时用本地默认 */ });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** 把开关同步到宿主(settings 持久化 + 宿主半部即时 setup/dispose)。失败只 warn 不打断:
|
|
106
|
-
* 宿主侧开关只影响路由注册等 Host 能力,本地 UI 状态仍然即时生效。
|
|
107
|
-
* (注意 fetch 对 404 这类 HTTP 错误是正常 resolve,必须查 res.ok——曾因 id 不一致
|
|
108
|
-
* 开关永远 404 而这里毫无声息,排查了两个版本才发现。) */
|
|
109
|
-
function pushFeatureEnabledToHost(id, enabled) {
|
|
110
|
-
if (typeof fetch !== "function") return;
|
|
111
|
-
fetch("/dsh-dock/features", {
|
|
112
|
-
method: "POST",
|
|
113
|
-
headers: { "content-type": "application/json" },
|
|
114
|
-
body: JSON.stringify({ id, enabled }),
|
|
115
|
-
}).then((res) => {
|
|
116
|
-
if (!res.ok) console.warn(`[dsh-dock] 功能开关同步宿主失败:${id}=${enabled} → HTTP ${res.status}(宿主不认识该 id 或插件未就绪)`);
|
|
117
|
-
}).catch(() => { /* 宿主旧版本或不可达:本地开关仍生效(仅本浏览器) */ });
|
|
118
|
-
}
|
|
119
|
-
export function toggleFeature(id) {
|
|
120
|
-
const st = stateOf(id);
|
|
121
|
-
st.enabled = !st.enabled;
|
|
122
|
-
featurePersist.map[id] = st.enabled;
|
|
123
|
-
persistFeatureEnabled();
|
|
124
|
-
pushFeatureEnabledToHost(id, st.enabled);
|
|
125
|
-
notifyState();
|
|
126
|
-
}
|
|
127
|
-
/** 取功能开关(外部功能默认启用;未登记的 id 惰性建项)。 */
|
|
128
|
-
export function stateOf(id) {
|
|
129
|
-
let st = featureState.get(id);
|
|
130
|
-
if (!st) { st = { enabled: true, error: null }; featureState.set(id, st); }
|
|
131
|
-
return st;
|
|
132
|
-
}
|
|
133
|
-
export function subscribeFeatureState(fn) {
|
|
134
|
-
stateListeners.add(fn);
|
|
135
|
-
return () => { stateListeners.delete(fn); };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ---- 会话页 chips 显示开关(每功能;localStorage 持久化,缺省显示) ----
|
|
139
|
-
// 与功能启用正交:功能启用决定面板页与宿主功能,本开关只控制会话输入区的随身小控件。
|
|
140
|
-
const CHIP_STORE_KEY = "dsh-dock/chips/v1";
|
|
141
|
-
const chipVisible = { map: {} };
|
|
142
|
-
try {
|
|
143
|
-
if (typeof localStorage !== "undefined") {
|
|
144
|
-
const raw = localStorage.getItem(CHIP_STORE_KEY);
|
|
145
|
-
const obj = raw ? JSON.parse(raw) : null;
|
|
146
|
-
if (obj && typeof obj === "object") chipVisible.map = obj;
|
|
147
|
-
}
|
|
148
|
-
} catch { /* localStorage 不可用时用默认值 */ }
|
|
149
|
-
function persistChipVisible() {
|
|
150
|
-
try {
|
|
151
|
-
if (typeof localStorage !== "undefined") localStorage.setItem(CHIP_STORE_KEY, JSON.stringify(chipVisible.map));
|
|
152
|
-
} catch { /* 持久化失败静默 */ }
|
|
153
|
-
}
|
|
154
|
-
/** 功能的会话页 chip 是否显示(未记录过的功能默认显示)。 */
|
|
155
|
-
export function chipShown(id) {
|
|
156
|
-
return chipVisible.map[id] !== false;
|
|
157
|
-
}
|
|
158
|
-
/** 设置功能的会话页 chip 显示开关(立即生效并持久化)。 */
|
|
159
|
-
export function setChipShown(id, value) {
|
|
160
|
-
chipVisible.map[id] = value !== false;
|
|
161
|
-
persistChipVisible();
|
|
162
|
-
notifyState();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ---- 面板导航总线:功能坞外任何位置(会话区 chips 等)可请求打开面板并跳到某功能 ----
|
|
166
|
-
// openPanel("balance", { provider }) → 打开功能坞并定位到模型余额页,高亮指定 Provider
|
|
167
|
-
// openPanel("tokenlog", { sessionId }) → 打开功能坞并定位到用量记录页,按会话筛选
|
|
168
|
-
export const panelNav = { open: false, active: "home", params: null };
|
|
169
|
-
const navListeners = new Set();
|
|
170
|
-
function notifyNav() {
|
|
171
|
-
for (const fn of navListeners) fn();
|
|
172
|
-
}
|
|
173
|
-
export function openPanel(active, params) {
|
|
174
|
-
panelNav.open = true;
|
|
175
|
-
panelNav.active = typeof active === "string" && active ? active : "home";
|
|
176
|
-
panelNav.params = params && typeof params === "object" ? params : null;
|
|
177
|
-
notifyNav();
|
|
178
|
-
}
|
|
179
|
-
export function setPanelOpen(value) {
|
|
180
|
-
panelNav.open = !!value;
|
|
181
|
-
if (!value) panelNav.params = null;
|
|
182
|
-
notifyNav();
|
|
183
|
-
}
|
|
184
|
-
/** 面板内切换功能页(不携带参数)。 */
|
|
185
|
-
export function navigatePanel(active) {
|
|
186
|
-
panelNav.active = typeof active === "string" && active ? active : "home";
|
|
187
|
-
notifyNav();
|
|
188
|
-
}
|
|
189
|
-
export function subscribePanel(fn) {
|
|
190
|
-
navListeners.add(fn);
|
|
191
|
-
return () => { navListeners.delete(fn); };
|
|
192
|
-
}
|
|
1
|
+
// dsh-dock · 客户端共享工具(外壳与功能模块共用;提取功能模块独立成包时随包复制)
|
|
2
|
+
import react from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 功能视图错误边界:视图渲染抛错时降级为一行错误提示,不拖垮整块面板 UI。
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ 内置视图也必须包(v0.9.9 起):宿主把每个插槽条目包在 SlotErrorBoundary 里,
|
|
8
|
+
* 条目内任何渲染抛错都会被宿主换成空 div——表现为「点开功能坞界面整个没了」而侧栏
|
|
9
|
+
* 入口按钮仍是选中态。v0.9.3~0.9.8 的 animation 视图就是这样被一个 ReferenceError 打没的。
|
|
10
|
+
* label 用于提示里点明是哪个功能;调用处应带 key(切换功能时重建,否则错误态会跟着串页)。
|
|
11
|
+
*/
|
|
12
|
+
export class FeatureBoundary extends react.Component {
|
|
13
|
+
constructor(props) {
|
|
14
|
+
super(props);
|
|
15
|
+
this.state = { error: null };
|
|
16
|
+
}
|
|
17
|
+
static getDerivedStateFromError(error) {
|
|
18
|
+
return { error: error };
|
|
19
|
+
}
|
|
20
|
+
componentDidCatch(error) {
|
|
21
|
+
console.error("[dsh-dock] 功能视图渲染出错:", error);
|
|
22
|
+
}
|
|
23
|
+
render() {
|
|
24
|
+
if (this.state.error) {
|
|
25
|
+
const msg = this.state.error && this.state.error.message ? this.state.error.message : String(this.state.error);
|
|
26
|
+
const who = this.props && this.props.label ? "「" + this.props.label + "」" : "功能视图";
|
|
27
|
+
return react.createElement("div", { className: "dockm-note dockm-err" },
|
|
28
|
+
who + "渲染出错:" + msg + "(已隔离,不影响面板其他功能;详情见浏览器控制台)");
|
|
29
|
+
}
|
|
30
|
+
return this.props.children;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---- 功能开关状态(外壳面板与会话区 chips 共用;每功能开关 localStorage 持久化,重启后保持) ----
|
|
35
|
+
const featureState = new Map();
|
|
36
|
+
const stateListeners = new Set();
|
|
37
|
+
function notifyState() {
|
|
38
|
+
for (const fn of stateListeners) fn();
|
|
39
|
+
}
|
|
40
|
+
// 开关持久化(dsh-dock/features/v1):id -> boolean;未记录过的功能用模块默认值
|
|
41
|
+
const FEATURE_STORE_KEY = "dsh-dock/features/v1";
|
|
42
|
+
const featurePersist = { map: {} };
|
|
43
|
+
try {
|
|
44
|
+
if (typeof localStorage !== "undefined") {
|
|
45
|
+
const raw = localStorage.getItem(FEATURE_STORE_KEY);
|
|
46
|
+
const obj = raw ? JSON.parse(raw) : null;
|
|
47
|
+
if (obj && typeof obj === "object") featurePersist.map = obj;
|
|
48
|
+
}
|
|
49
|
+
} catch { /* localStorage 不可用时用默认值 */ }
|
|
50
|
+
function persistFeatureEnabled() {
|
|
51
|
+
try {
|
|
52
|
+
if (typeof localStorage !== "undefined") localStorage.setItem(FEATURE_STORE_KEY, JSON.stringify(featurePersist.map));
|
|
53
|
+
} catch { /* 持久化失败静默 */ }
|
|
54
|
+
}
|
|
55
|
+
/** 外壳 apply 时初始化内置功能的默认开关(持久化过的值优先于模块默认)。
|
|
56
|
+
* 默认全部停用、按需开启:仅当模块显式声明 defaultEnabled: true 才默认启用。 */
|
|
57
|
+
export function initFeatureState(defs) {
|
|
58
|
+
for (const f of defs) {
|
|
59
|
+
if (featureState.has(f.id)) continue;
|
|
60
|
+
let enabled = !f.planned && f.defaultEnabled === true;
|
|
61
|
+
const saved = featurePersist.map[f.id];
|
|
62
|
+
if (typeof saved === "boolean") enabled = saved;
|
|
63
|
+
featureState.set(f.id, { enabled, error: null });
|
|
64
|
+
}
|
|
65
|
+
// 宿主侧持久化开关(settings,跨浏览器/重启)优先:拉过来覆盖本地默认值。
|
|
66
|
+
// localStorage 里已有记录的 id 视为用户在这台浏览器已表态,保持本地值;
|
|
67
|
+
// 若宿主从未收到过该功能的开关,则把本浏览器的选择补推一次(见 syncFeatureStateFromHost)。
|
|
68
|
+
syncFeatureStateFromHost(defs);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 从宿主 /dsh-dock/features 拉取持久化开关,覆盖尚未本地表态的功能默认值。
|
|
72
|
+
* 拉取失败(宿主旧版本无此路由)静默降级——沿用本地默认。 */
|
|
73
|
+
function syncFeatureStateFromHost(defs) {
|
|
74
|
+
if (typeof fetch !== "function") return;
|
|
75
|
+
fetch("/dsh-dock/features").then(async (res) => {
|
|
76
|
+
if (!res.ok) return;
|
|
77
|
+
const data = await res.json().catch(() => null);
|
|
78
|
+
const persisted = data && data.ok && data.data && data.data.persisted;
|
|
79
|
+
if (!persisted || typeof persisted !== "object") return;
|
|
80
|
+
let changed = false;
|
|
81
|
+
for (const f of defs) {
|
|
82
|
+
const local = featurePersist.map[f.id];
|
|
83
|
+
const host = persisted[f.id];
|
|
84
|
+
if (local === undefined) {
|
|
85
|
+
// 本浏览器没表态:跟随宿主持久化值
|
|
86
|
+
if (typeof host === "boolean") {
|
|
87
|
+
const st = stateOf(f.id);
|
|
88
|
+
if (st.enabled !== host) { st.enabled = host; changed = true; }
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
// 本浏览器已表态,但宿主从未收到过这个功能的开关(persisted 里根本没有它):
|
|
93
|
+
// 补推一次本浏览器的选择。否则会出现「面板显示已启用、宿主侧却从未 setup」——
|
|
94
|
+
// 该功能的路由整片 404,面板还会误报「宿主进程是旧版本」。宿主刚升级、
|
|
95
|
+
// 或新功能刚拆出来时最容易踩(「任务通知」拆分时就是这样:点开关时宿主
|
|
96
|
+
// 还是旧进程 → POST 404 → 开关只留在浏览器本地,重启后宿主永远不知道)。
|
|
97
|
+
// 注意只在「宿主没有记录」时补推:宿主已显式记过 false 说明是别处关掉的,
|
|
98
|
+
// 不能拿本浏览器的旧值去覆盖(多设备场景)。
|
|
99
|
+
if (typeof host !== "boolean") pushFeatureEnabledToHost(f.id, local);
|
|
100
|
+
}
|
|
101
|
+
if (changed) notifyState();
|
|
102
|
+
}).catch(() => { /* 宿主不可达时用本地默认 */ });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 把开关同步到宿主(settings 持久化 + 宿主半部即时 setup/dispose)。失败只 warn 不打断:
|
|
106
|
+
* 宿主侧开关只影响路由注册等 Host 能力,本地 UI 状态仍然即时生效。
|
|
107
|
+
* (注意 fetch 对 404 这类 HTTP 错误是正常 resolve,必须查 res.ok——曾因 id 不一致
|
|
108
|
+
* 开关永远 404 而这里毫无声息,排查了两个版本才发现。) */
|
|
109
|
+
function pushFeatureEnabledToHost(id, enabled) {
|
|
110
|
+
if (typeof fetch !== "function") return;
|
|
111
|
+
fetch("/dsh-dock/features", {
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: { "content-type": "application/json" },
|
|
114
|
+
body: JSON.stringify({ id, enabled }),
|
|
115
|
+
}).then((res) => {
|
|
116
|
+
if (!res.ok) console.warn(`[dsh-dock] 功能开关同步宿主失败:${id}=${enabled} → HTTP ${res.status}(宿主不认识该 id 或插件未就绪)`);
|
|
117
|
+
}).catch(() => { /* 宿主旧版本或不可达:本地开关仍生效(仅本浏览器) */ });
|
|
118
|
+
}
|
|
119
|
+
export function toggleFeature(id) {
|
|
120
|
+
const st = stateOf(id);
|
|
121
|
+
st.enabled = !st.enabled;
|
|
122
|
+
featurePersist.map[id] = st.enabled;
|
|
123
|
+
persistFeatureEnabled();
|
|
124
|
+
pushFeatureEnabledToHost(id, st.enabled);
|
|
125
|
+
notifyState();
|
|
126
|
+
}
|
|
127
|
+
/** 取功能开关(外部功能默认启用;未登记的 id 惰性建项)。 */
|
|
128
|
+
export function stateOf(id) {
|
|
129
|
+
let st = featureState.get(id);
|
|
130
|
+
if (!st) { st = { enabled: true, error: null }; featureState.set(id, st); }
|
|
131
|
+
return st;
|
|
132
|
+
}
|
|
133
|
+
export function subscribeFeatureState(fn) {
|
|
134
|
+
stateListeners.add(fn);
|
|
135
|
+
return () => { stateListeners.delete(fn); };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---- 会话页 chips 显示开关(每功能;localStorage 持久化,缺省显示) ----
|
|
139
|
+
// 与功能启用正交:功能启用决定面板页与宿主功能,本开关只控制会话输入区的随身小控件。
|
|
140
|
+
const CHIP_STORE_KEY = "dsh-dock/chips/v1";
|
|
141
|
+
const chipVisible = { map: {} };
|
|
142
|
+
try {
|
|
143
|
+
if (typeof localStorage !== "undefined") {
|
|
144
|
+
const raw = localStorage.getItem(CHIP_STORE_KEY);
|
|
145
|
+
const obj = raw ? JSON.parse(raw) : null;
|
|
146
|
+
if (obj && typeof obj === "object") chipVisible.map = obj;
|
|
147
|
+
}
|
|
148
|
+
} catch { /* localStorage 不可用时用默认值 */ }
|
|
149
|
+
function persistChipVisible() {
|
|
150
|
+
try {
|
|
151
|
+
if (typeof localStorage !== "undefined") localStorage.setItem(CHIP_STORE_KEY, JSON.stringify(chipVisible.map));
|
|
152
|
+
} catch { /* 持久化失败静默 */ }
|
|
153
|
+
}
|
|
154
|
+
/** 功能的会话页 chip 是否显示(未记录过的功能默认显示)。 */
|
|
155
|
+
export function chipShown(id) {
|
|
156
|
+
return chipVisible.map[id] !== false;
|
|
157
|
+
}
|
|
158
|
+
/** 设置功能的会话页 chip 显示开关(立即生效并持久化)。 */
|
|
159
|
+
export function setChipShown(id, value) {
|
|
160
|
+
chipVisible.map[id] = value !== false;
|
|
161
|
+
persistChipVisible();
|
|
162
|
+
notifyState();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---- 面板导航总线:功能坞外任何位置(会话区 chips 等)可请求打开面板并跳到某功能 ----
|
|
166
|
+
// openPanel("balance", { provider }) → 打开功能坞并定位到模型余额页,高亮指定 Provider
|
|
167
|
+
// openPanel("tokenlog", { sessionId }) → 打开功能坞并定位到用量记录页,按会话筛选
|
|
168
|
+
export const panelNav = { open: false, active: "home", params: null };
|
|
169
|
+
const navListeners = new Set();
|
|
170
|
+
function notifyNav() {
|
|
171
|
+
for (const fn of navListeners) fn();
|
|
172
|
+
}
|
|
173
|
+
export function openPanel(active, params) {
|
|
174
|
+
panelNav.open = true;
|
|
175
|
+
panelNav.active = typeof active === "string" && active ? active : "home";
|
|
176
|
+
panelNav.params = params && typeof params === "object" ? params : null;
|
|
177
|
+
notifyNav();
|
|
178
|
+
}
|
|
179
|
+
export function setPanelOpen(value) {
|
|
180
|
+
panelNav.open = !!value;
|
|
181
|
+
if (!value) panelNav.params = null;
|
|
182
|
+
notifyNav();
|
|
183
|
+
}
|
|
184
|
+
/** 面板内切换功能页(不携带参数)。 */
|
|
185
|
+
export function navigatePanel(active) {
|
|
186
|
+
panelNav.active = typeof active === "string" && active ? active : "home";
|
|
187
|
+
notifyNav();
|
|
188
|
+
}
|
|
189
|
+
export function subscribePanel(fn) {
|
|
190
|
+
navListeners.add(fn);
|
|
191
|
+
return () => { navListeners.delete(fn); };
|
|
192
|
+
}
|