dsh-dock 0.9.5 → 0.9.7

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 -415
  23. package/features/mobile-relay/host.js +570 -546
  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 +678 -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/host-core.js CHANGED
@@ -1,95 +1,98 @@
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
- remoteAuth: z.object({
11
- /** 远程访问登录账号(明文用户名;密码只存加盐哈希)。 */
12
- username: z.string().default(''),
13
- passwordHash: z.string().default(''),
14
- salt: z.string().default(''),
15
- }).default({}),
16
- visionProxy: z.object({
17
- enabled: z.boolean().default(false),
18
- provider: z.string().default(''),
19
- model: z.string().default(''),
20
- }).default({}),
21
- animation: z.object({
22
- // 动画与通知两个独立开关(可只开其一)
23
- animationEnabled: z.boolean().default(true),
24
- effectMode: z.string().default('flow'),
25
- // 桌面伙伴场景缩放(0.85~2.2;浮层右下角也可直接拖动缩放)
26
- robotScale: z.number().default(1.35),
27
- notifyEnabled: z.boolean().default(true),
28
- notifyOnComplete: z.boolean().default(true),
29
- notifyOnError: z.boolean().default(true),
30
- // 工具等待用户确认/批准时提醒(dsh 会话流 approval/asked → decided)
31
- notifyOnConfirm: z.boolean().default(true),
32
- // 通知停留毫秒数(0 = 常驻直到手动关闭)
33
- notifyStayMs: z.number().default(8000),
34
- // 浏览器系统通知(页面后台时推送)
35
- systemNotify: z.boolean().default(false),
36
- // 任务结束提示音(WebAudio 合成,macOS/Windows 通用,无音频文件依赖)
37
- soundNotify: z.boolean().default(true),
38
- // 提示音音效(完成场景音名;异常音由音效包内配套)
39
- soundEffect: z.string().default('chime'),
40
- // 钉钉群机器人推送(宿主侧直发,浏览器关着也能推;事件跟随 notifyOnComplete/notifyOnError)
41
- dingtalkEnabled: z.boolean().default(false),
42
- dingtalkWebhook: z.string().default(''),
43
- // 飞书群机器人推送(宿主侧直发,浏览器关着也能推;事件跟随 notifyOnComplete/notifyOnError)
44
- feishuEnabled: z.boolean().default(false),
45
- feishuWebhook: z.string().default(''),
46
- }).default({}),
47
- })
48
-
49
- /** 任务动画 effectMode 合法值(客户端动画模式)。 */
50
- export const ANIMATION_MODES = [
51
- 'flow', 'breathe', 'ring', 'orbit', 'robot', 'matrix', 'stars', 'aurora', 'space',
52
- 'nebula', 'warp', 'radar', 'constellation', 'fireflies', 'ocean', 'prism', 'circuit', 'gravity', 'lantern',
53
- ]
54
-
55
- /** 提示音 soundEffect 合法值(客户端音效库键名)。 */
56
- export const SOUND_EFFECTS = ['chime', 'ding', 'coin', 'bell', 'pulse', 'arp']
57
-
58
- /** 沿 settingsPath 走一层对象(user/base/value 都可用)。 */
59
- export function walkPath(node, path) {
60
- for (const key of path || []) {
61
- node = node && typeof node === 'object' ? node[key] : undefined
62
- }
63
- return node
64
- }
65
-
66
- /** 序列化 JSON 响应。 */
67
- export function sendJson(res, status, value) {
68
- res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
69
- res.end(JSON.stringify(value))
70
- }
71
-
72
- /** 解析 POST 请求体 JSON(限长 2MB)。 */
73
- export function readBody(req) {
74
- return new Promise((resolve, reject) => {
75
- let size = 0
76
- const chunks = []
77
- req.on('data', (c) => {
78
- size += c.length
79
- if (size > 2 * 1024 * 1024) {
80
- reject(new Error('请求体过大'))
81
- req.destroy()
82
- return
83
- }
84
- chunks.push(c)
85
- })
86
- req.on('end', () => {
87
- try {
88
- resolve(chunks.length === 0 ? {} : JSON.parse(Buffer.concat(chunks).toString('utf8')))
89
- } catch (e) {
90
- reject(new Error('请求体不是合法 JSON'))
91
- }
92
- })
93
- req.on('error', reject)
94
- })
95
- }
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
+ // 宿主侧功能开关(id -> boolean):客户端面板 toggle 即时同步过来,
11
+ // 重启 dsh 后按此表决定宿主半部 setup 哪些功能(路由注册、事件订阅等)。
12
+ features: z.dict(z.boolean()).default({}),
13
+ remoteAuth: z.object({
14
+ /** 远程访问登录账号(明文用户名;密码只存加盐哈希)。 */
15
+ username: z.string().default(''),
16
+ passwordHash: z.string().default(''),
17
+ salt: z.string().default(''),
18
+ }).default({}),
19
+ visionProxy: z.object({
20
+ enabled: z.boolean().default(false),
21
+ provider: z.string().default(''),
22
+ model: z.string().default(''),
23
+ }).default({}),
24
+ animation: z.object({
25
+ // 动画与通知两个独立开关(可只开其一)
26
+ animationEnabled: z.boolean().default(true),
27
+ effectMode: z.string().default('flow'),
28
+ // 桌面伙伴场景缩放(0.85~2.2;浮层右下角也可直接拖动缩放)
29
+ robotScale: z.number().default(1.35),
30
+ notifyEnabled: z.boolean().default(true),
31
+ notifyOnComplete: z.boolean().default(true),
32
+ notifyOnError: z.boolean().default(true),
33
+ // 工具等待用户确认/批准时提醒(dsh 会话流 approval/asked → decided)
34
+ notifyOnConfirm: z.boolean().default(true),
35
+ // 通知停留毫秒数(0 = 常驻直到手动关闭)
36
+ notifyStayMs: z.number().default(8000),
37
+ // 浏览器系统通知(页面后台时推送)
38
+ systemNotify: z.boolean().default(false),
39
+ // 任务结束提示音(WebAudio 合成,macOS/Windows 通用,无音频文件依赖)
40
+ soundNotify: z.boolean().default(true),
41
+ // 提示音音效(完成场景音名;异常音由音效包内配套)
42
+ soundEffect: z.string().default('chime'),
43
+ // 钉钉群机器人推送(宿主侧直发,浏览器关着也能推;事件跟随 notifyOnComplete/notifyOnError)
44
+ dingtalkEnabled: z.boolean().default(false),
45
+ dingtalkWebhook: z.string().default(''),
46
+ // 飞书群机器人推送(宿主侧直发,浏览器关着也能推;事件跟随 notifyOnComplete/notifyOnError)
47
+ feishuEnabled: z.boolean().default(false),
48
+ feishuWebhook: z.string().default(''),
49
+ }).default({}),
50
+ })
51
+
52
+ /** 任务动画 effectMode 合法值(客户端动画模式)。 */
53
+ export const ANIMATION_MODES = [
54
+ 'flow', 'breathe', 'ring', 'orbit', 'robot', 'matrix', 'stars', 'aurora', 'space',
55
+ 'nebula', 'warp', 'radar', 'constellation', 'fireflies', 'ocean', 'prism', 'circuit', 'gravity', 'lantern',
56
+ ]
57
+
58
+ /** 提示音 soundEffect 合法值(客户端音效库键名)。 */
59
+ export const SOUND_EFFECTS = ['chime', 'ding', 'coin', 'bell', 'pulse', 'arp']
60
+
61
+ /** 沿 settingsPath 走一层对象(user/base/value 都可用)。 */
62
+ export function walkPath(node, path) {
63
+ for (const key of path || []) {
64
+ node = node && typeof node === 'object' ? node[key] : undefined
65
+ }
66
+ return node
67
+ }
68
+
69
+ /** 序列化 JSON 响应。 */
70
+ export function sendJson(res, status, value) {
71
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
72
+ res.end(JSON.stringify(value))
73
+ }
74
+
75
+ /** 解析 POST 请求体 JSON(限长 2MB)。 */
76
+ export function readBody(req) {
77
+ return new Promise((resolve, reject) => {
78
+ let size = 0
79
+ const chunks = []
80
+ req.on('data', (c) => {
81
+ size += c.length
82
+ if (size > 2 * 1024 * 1024) {
83
+ reject(new Error('请求体过大'))
84
+ req.destroy()
85
+ return
86
+ }
87
+ chunks.push(c)
88
+ })
89
+ req.on('end', () => {
90
+ try {
91
+ resolve(chunks.length === 0 ? {} : JSON.parse(Buffer.concat(chunks).toString('utf8')))
92
+ } catch (e) {
93
+ reject(new Error('请求体不是合法 JSON'))
94
+ }
95
+ })
96
+ req.on('error', reject)
97
+ })
98
+ }
package/src/shared.js CHANGED
@@ -1,130 +1,167 @@
1
- // dsh-dock · 客户端共享工具(外壳与功能模块共用;提取功能模块独立成包时随包复制)
2
- import react from "react";
3
-
4
- /**
5
- * 功能视图错误边界:外部功能包注册进来的视图渲染抛错时降级为错误提示,
6
- * 不拖垮整个功能坞面板(内置视图不包——它们与外壳同包发布、同生命周期)。
7
- */
8
- export class FeatureBoundary extends react.Component {
9
- constructor(props) {
10
- super(props);
11
- this.state = { error: null };
12
- }
13
- static getDerivedStateFromError(error) {
14
- return { error: error };
15
- }
16
- render() {
17
- if (this.state.error) {
18
- const msg = this.state.error && this.state.error.message ? this.state.error.message : String(this.state.error);
19
- return react.createElement("div", { className: "dockm-note dockm-err" },
20
- "功能视图渲染出错:" + msg + "(该功能来自外部包,不影响面板其他功能)");
21
- }
22
- return this.props.children;
23
- }
24
- }
25
-
26
- // ---- 功能开关状态(外壳面板与会话区 chips 共用;每功能开关 localStorage 持久化,重启后保持) ----
27
- const featureState = new Map();
28
- const stateListeners = new Set();
29
- function notifyState() {
30
- for (const fn of stateListeners) fn();
31
- }
32
- // 开关持久化(dsh-dock/features/v1):id -> boolean;未记录过的功能用模块默认值
33
- const FEATURE_STORE_KEY = "dsh-dock/features/v1";
34
- const featurePersist = { map: {} };
35
- try {
36
- if (typeof localStorage !== "undefined") {
37
- const raw = localStorage.getItem(FEATURE_STORE_KEY);
38
- const obj = raw ? JSON.parse(raw) : null;
39
- if (obj && typeof obj === "object") featurePersist.map = obj;
40
- }
41
- } catch { /* localStorage 不可用时用默认值 */ }
42
- function persistFeatureEnabled() {
43
- try {
44
- if (typeof localStorage !== "undefined") localStorage.setItem(FEATURE_STORE_KEY, JSON.stringify(featurePersist.map));
45
- } catch { /* 持久化失败静默 */ }
46
- }
47
- /** 外壳 apply 时初始化内置功能的默认开关(持久化过的值优先于模块默认)。
48
- * 默认全部停用、按需开启:仅当模块显式声明 defaultEnabled: true 才默认启用。 */
49
- export function initFeatureState(defs) {
50
- for (const f of defs) {
51
- if (featureState.has(f.id)) continue;
52
- let enabled = !f.planned && f.defaultEnabled === true;
53
- const saved = featurePersist.map[f.id];
54
- if (typeof saved === "boolean") enabled = saved;
55
- featureState.set(f.id, { enabled, error: null });
56
- }
57
- }
58
- /** 取功能开关(外部功能默认启用;未登记的 id 惰性建项)。 */
59
- export function stateOf(id) {
60
- let st = featureState.get(id);
61
- if (!st) { st = { enabled: true, error: null }; featureState.set(id, st); }
62
- return st;
63
- }
64
- export function toggleFeature(id) {
65
- const st = stateOf(id);
66
- st.enabled = !st.enabled;
67
- featurePersist.map[id] = st.enabled;
68
- persistFeatureEnabled();
69
- notifyState();
70
- }
71
- export function subscribeFeatureState(fn) {
72
- stateListeners.add(fn);
73
- return () => { stateListeners.delete(fn); };
74
- }
75
-
76
- // ---- 会话页 chips 显示开关(每功能;localStorage 持久化,缺省显示) ----
77
- // 与功能启用正交:功能启用决定面板页与宿主功能,本开关只控制会话输入区的随身小控件。
78
- const CHIP_STORE_KEY = "dsh-dock/chips/v1";
79
- const chipVisible = { map: {} };
80
- try {
81
- if (typeof localStorage !== "undefined") {
82
- const raw = localStorage.getItem(CHIP_STORE_KEY);
83
- const obj = raw ? JSON.parse(raw) : null;
84
- if (obj && typeof obj === "object") chipVisible.map = obj;
85
- }
86
- } catch { /* localStorage 不可用时用默认值 */ }
87
- function persistChipVisible() {
88
- try {
89
- if (typeof localStorage !== "undefined") localStorage.setItem(CHIP_STORE_KEY, JSON.stringify(chipVisible.map));
90
- } catch { /* 持久化失败静默 */ }
91
- }
92
- /** 功能的会话页 chip 是否显示(未记录过的功能默认显示)。 */
93
- export function chipShown(id) {
94
- return chipVisible.map[id] !== false;
95
- }
96
- /** 设置功能的会话页 chip 显示开关(立即生效并持久化)。 */
97
- export function setChipShown(id, value) {
98
- chipVisible.map[id] = value !== false;
99
- persistChipVisible();
100
- notifyState();
101
- }
102
-
103
- // ---- 面板导航总线:功能坞外任何位置(会话区 chips 等)可请求打开面板并跳到某功能 ----
104
- // openPanel("balance", { provider }) → 打开功能坞并定位到模型余额页,高亮指定 Provider
105
- // openPanel("tokenlog", { sessionId }) → 打开功能坞并定位到用量记录页,按会话筛选
106
- export const panelNav = { open: false, active: "home", params: null };
107
- const navListeners = new Set();
108
- function notifyNav() {
109
- for (const fn of navListeners) fn();
110
- }
111
- export function openPanel(active, params) {
112
- panelNav.open = true;
113
- panelNav.active = typeof active === "string" && active ? active : "home";
114
- panelNav.params = params && typeof params === "object" ? params : null;
115
- notifyNav();
116
- }
117
- export function setPanelOpen(value) {
118
- panelNav.open = !!value;
119
- if (!value) panelNav.params = null;
120
- notifyNav();
121
- }
122
- /** 面板内切换功能页(不携带参数)。 */
123
- export function navigatePanel(active) {
124
- panelNav.active = typeof active === "string" && active ? active : "home";
125
- notifyNav();
126
- }
127
- export function subscribePanel(fn) {
128
- navListeners.add(fn);
129
- return () => { navListeners.delete(fn); };
130
- }
1
+ // dsh-dock · 客户端共享工具(外壳与功能模块共用;提取功能模块独立成包时随包复制)
2
+ import react from "react";
3
+
4
+ /**
5
+ * 功能视图错误边界:外部功能包注册进来的视图渲染抛错时降级为错误提示,
6
+ * 不拖垮整个功能坞面板(内置视图不包——它们与外壳同包发布、同生命周期)。
7
+ */
8
+ export class FeatureBoundary extends react.Component {
9
+ constructor(props) {
10
+ super(props);
11
+ this.state = { error: null };
12
+ }
13
+ static getDerivedStateFromError(error) {
14
+ return { error: error };
15
+ }
16
+ render() {
17
+ if (this.state.error) {
18
+ const msg = this.state.error && this.state.error.message ? this.state.error.message : String(this.state.error);
19
+ return react.createElement("div", { className: "dockm-note dockm-err" },
20
+ "功能视图渲染出错:" + msg + "(该功能来自外部包,不影响面板其他功能)");
21
+ }
22
+ return this.props.children;
23
+ }
24
+ }
25
+
26
+ // ---- 功能开关状态(外壳面板与会话区 chips 共用;每功能开关 localStorage 持久化,重启后保持) ----
27
+ const featureState = new Map();
28
+ const stateListeners = new Set();
29
+ function notifyState() {
30
+ for (const fn of stateListeners) fn();
31
+ }
32
+ // 开关持久化(dsh-dock/features/v1):id -> boolean;未记录过的功能用模块默认值
33
+ const FEATURE_STORE_KEY = "dsh-dock/features/v1";
34
+ const featurePersist = { map: {} };
35
+ try {
36
+ if (typeof localStorage !== "undefined") {
37
+ const raw = localStorage.getItem(FEATURE_STORE_KEY);
38
+ const obj = raw ? JSON.parse(raw) : null;
39
+ if (obj && typeof obj === "object") featurePersist.map = obj;
40
+ }
41
+ } catch { /* localStorage 不可用时用默认值 */ }
42
+ function persistFeatureEnabled() {
43
+ try {
44
+ if (typeof localStorage !== "undefined") localStorage.setItem(FEATURE_STORE_KEY, JSON.stringify(featurePersist.map));
45
+ } catch { /* 持久化失败静默 */ }
46
+ }
47
+ /** 外壳 apply 时初始化内置功能的默认开关(持久化过的值优先于模块默认)。
48
+ * 默认全部停用、按需开启:仅当模块显式声明 defaultEnabled: true 才默认启用。 */
49
+ export function initFeatureState(defs) {
50
+ for (const f of defs) {
51
+ if (featureState.has(f.id)) continue;
52
+ let enabled = !f.planned && f.defaultEnabled === true;
53
+ const saved = featurePersist.map[f.id];
54
+ if (typeof saved === "boolean") enabled = saved;
55
+ featureState.set(f.id, { enabled, error: null });
56
+ }
57
+ // 宿主侧持久化开关(settings,跨浏览器/重启)优先:拉过来覆盖本地默认值。
58
+ // localStorage 里已有记录的 id 视为用户在这台浏览器已表态,保持本地值。
59
+ syncFeatureStateFromHost(defs);
60
+ }
61
+
62
+ /** 从宿主 /dsh-dock/features 拉取持久化开关,覆盖尚未本地表态的功能默认值。
63
+ * 拉取失败(宿主旧版本无此路由)静默降级——沿用本地默认。 */
64
+ function syncFeatureStateFromHost(defs) {
65
+ if (typeof fetch !== "function") return;
66
+ fetch("/dsh-dock/features").then(async (res) => {
67
+ if (!res.ok) return;
68
+ const data = await res.json().catch(() => null);
69
+ const persisted = data && data.ok && data.data && data.data.persisted;
70
+ if (!persisted || typeof persisted !== "object") return;
71
+ let changed = false;
72
+ for (const f of defs) {
73
+ if (featurePersist.map[f.id] !== undefined) continue;
74
+ const v = persisted[f.id];
75
+ if (typeof v === "boolean") {
76
+ const st = stateOf(f.id);
77
+ if (st.enabled !== v) { st.enabled = v; changed = true; }
78
+ }
79
+ }
80
+ if (changed) notifyState();
81
+ }).catch(() => { /* 宿主不可达时用本地默认 */ });
82
+ }
83
+
84
+ /** 把开关同步到宿主(settings 持久化 + 宿主半部即时 setup/dispose)。失败静默:
85
+ * 宿主侧开关只影响路由注册等 Host 能力,本地 UI 状态仍然即时生效。 */
86
+ function pushFeatureEnabledToHost(id, enabled) {
87
+ if (typeof fetch !== "function") return;
88
+ fetch("/dsh-dock/features", {
89
+ method: "POST",
90
+ headers: { "content-type": "application/json" },
91
+ body: JSON.stringify({ id, enabled }),
92
+ }).catch(() => { /* 宿主旧版本或不可达:本地开关仍生效(仅本浏览器) */ });
93
+ }
94
+ export function toggleFeature(id) {
95
+ const st = stateOf(id);
96
+ st.enabled = !st.enabled;
97
+ featurePersist.map[id] = st.enabled;
98
+ persistFeatureEnabled();
99
+ pushFeatureEnabledToHost(id, st.enabled);
100
+ notifyState();
101
+ }
102
+ /** 取功能开关(外部功能默认启用;未登记的 id 惰性建项)。 */
103
+ export function stateOf(id) {
104
+ let st = featureState.get(id);
105
+ if (!st) { st = { enabled: true, error: null }; featureState.set(id, st); }
106
+ return st;
107
+ }
108
+ export function subscribeFeatureState(fn) {
109
+ stateListeners.add(fn);
110
+ return () => { stateListeners.delete(fn); };
111
+ }
112
+
113
+ // ---- 会话页 chips 显示开关(每功能;localStorage 持久化,缺省显示) ----
114
+ // 与功能启用正交:功能启用决定面板页与宿主功能,本开关只控制会话输入区的随身小控件。
115
+ const CHIP_STORE_KEY = "dsh-dock/chips/v1";
116
+ const chipVisible = { map: {} };
117
+ try {
118
+ if (typeof localStorage !== "undefined") {
119
+ const raw = localStorage.getItem(CHIP_STORE_KEY);
120
+ const obj = raw ? JSON.parse(raw) : null;
121
+ if (obj && typeof obj === "object") chipVisible.map = obj;
122
+ }
123
+ } catch { /* localStorage 不可用时用默认值 */ }
124
+ function persistChipVisible() {
125
+ try {
126
+ if (typeof localStorage !== "undefined") localStorage.setItem(CHIP_STORE_KEY, JSON.stringify(chipVisible.map));
127
+ } catch { /* 持久化失败静默 */ }
128
+ }
129
+ /** 功能的会话页 chip 是否显示(未记录过的功能默认显示)。 */
130
+ export function chipShown(id) {
131
+ return chipVisible.map[id] !== false;
132
+ }
133
+ /** 设置功能的会话页 chip 显示开关(立即生效并持久化)。 */
134
+ export function setChipShown(id, value) {
135
+ chipVisible.map[id] = value !== false;
136
+ persistChipVisible();
137
+ notifyState();
138
+ }
139
+
140
+ // ---- 面板导航总线:功能坞外任何位置(会话区 chips 等)可请求打开面板并跳到某功能 ----
141
+ // openPanel("balance", { provider }) → 打开功能坞并定位到模型余额页,高亮指定 Provider
142
+ // openPanel("tokenlog", { sessionId }) → 打开功能坞并定位到用量记录页,按会话筛选
143
+ export const panelNav = { open: false, active: "home", params: null };
144
+ const navListeners = new Set();
145
+ function notifyNav() {
146
+ for (const fn of navListeners) fn();
147
+ }
148
+ export function openPanel(active, params) {
149
+ panelNav.open = true;
150
+ panelNav.active = typeof active === "string" && active ? active : "home";
151
+ panelNav.params = params && typeof params === "object" ? params : null;
152
+ notifyNav();
153
+ }
154
+ export function setPanelOpen(value) {
155
+ panelNav.open = !!value;
156
+ if (!value) panelNav.params = null;
157
+ notifyNav();
158
+ }
159
+ /** 面板内切换功能页(不携带参数)。 */
160
+ export function navigatePanel(active) {
161
+ panelNav.active = typeof active === "string" && active ? active : "home";
162
+ notifyNav();
163
+ }
164
+ export function subscribePanel(fn) {
165
+ navListeners.add(fn);
166
+ return () => { navListeners.delete(fn); };
167
+ }