dsh-panel-control 0.7.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -229
- package/client.js +540 -8
- package/docs/bridged-settings-pages.md +85 -0
- package/package.json +2 -2
- package/scripts/bridge-smoke.mjs +324 -0
- package/scripts/layout-smoke.mjs +16 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# 设置页补入口:怎么识别,怎么渲染
|
|
2
|
+
|
|
3
|
+
## 问题
|
|
4
|
+
|
|
5
|
+
宿主把「侧栏面板」和「设置页」做成两套注册:
|
|
6
|
+
|
|
7
|
+
- 面板入口 = `sidebar.panellist` 里的一行图标 + 同 id 的 root 作用域 `main` keyed 条目;
|
|
8
|
+
- 设置页 = `settings.section` 里的一个 id。
|
|
9
|
+
|
|
10
|
+
多数插件两者都注册。少数只注册 `settings.section`,于是它的页面只能从设置弹窗里翻到——
|
|
11
|
+
侧栏没有入口,也没法被单独选中。设置弹窗的打开状态是宿主私有的,没有公开 API 能直接定位到
|
|
12
|
+
某个 section,所以「点一行就打开那个设置页」只能由本插件自己造一页。
|
|
13
|
+
|
|
14
|
+
## 识别
|
|
15
|
+
|
|
16
|
+
宿主的槽条目确实带有 `registrant`,但在当前客户端里它**不能用来区分插件**:client runner 把
|
|
17
|
+
大量插件放在同一个 fiber 里 apply,实测 `account/general/models/plugins/agent-presets` 以及
|
|
18
|
+
`plan-mode`、`bili`、`workspace-control-settings`、`composer-rewind-settings` 这些设置页的
|
|
19
|
+
`registrant` 都是同一个 `tk`,侧栏里大多数行也是 `tk`;只有插件市场是它自己的 `dsh-market`。
|
|
20
|
+
按 registrant 过滤会把所有第三方设置页都当成「已有入口」。
|
|
21
|
+
|
|
22
|
+
因此改为按**侧栏归属**判断,一个设置页有入口当且仅当满足任一条:
|
|
23
|
+
|
|
24
|
+
1. 它的 id 等于某个 `sidebar.panellist` 的 id;
|
|
25
|
+
2. 去掉末尾 `-settings` 后的 id 等于某个面板 id(`panel-control-settings` ↔ `panel-control`);
|
|
26
|
+
3. 某个面板 id 以 `-<id>` 结尾(`market` ↔ `dsh-market`);
|
|
27
|
+
4. 它的 label 与某个面板 label 相同(`market` / `workspace-control-settings` 都命中)。
|
|
28
|
+
|
|
29
|
+
内置设置页没有面板,因此单独列一组 id 排除:`account`、`general`、`models`、`agent-presets`、
|
|
30
|
+
`plugins`(`plugins` 同时也有面板,列在这里只是双保险)。剩下的每个 section 补一行,id 为
|
|
31
|
+
`dpc-bridge-<section id>`,order 85(在普通入口之后、本插件之前)。标题取该 section 的 label;
|
|
32
|
+
与已有行撞名时追加 ` · <section id>`。
|
|
33
|
+
|
|
34
|
+
这套判断不读第三方包内部文件,也不依赖插件白名单;代价是**启发式**:一个第三方设置页若恰好
|
|
35
|
+
与某个面板同名/id 同源,会被当成已有入口;未来新增的内置设置页若不在上面的 id 列表里,会被
|
|
36
|
+
多补一行(此时把它加进 `CORE_SECTION_IDS` 即可)。
|
|
37
|
+
|
|
38
|
+
## 渲染
|
|
39
|
+
|
|
40
|
+
桥接面板自己注册同 id 的 `main` keyed 条目,并在该条目上声明一个自有子槽(`dpc-bridge-slot-<section id>`)。
|
|
41
|
+
之所以不直接用 `ctx.slots.renderSlot('settings.section', ...)`:ctx 级 `renderSlot` 只渲染 `root`,
|
|
42
|
+
子槽只能通过「声明了它的那个条目的 props 面」渲染;`settings.section` 由设置壳声明,本插件无权调用。
|
|
43
|
+
因此桥接条目把原 section 的组件以同一份 `inject/store/locale` 重新注册进自有子槽,再通过宿主注入的
|
|
44
|
+
`props.renderSlot(slotName, { close: back }, {})` 渲染它。
|
|
45
|
+
|
|
46
|
+
这样 section 的 `inject`、`store`、locale `t`、`useStore` 和 error boundary 仍由宿主照常提供;直接调用
|
|
47
|
+
section 组件会缺这些。面板只负责外壳:顶部一条 40px 的栏(「返回对话」+ 来源 section id),
|
|
48
|
+
下面一块随页面滚动的主体。`close` 与设置壳里一样只做「关掉这一页」,这里落回对话
|
|
49
|
+
(`layout.selectPanel(null)`)。
|
|
50
|
+
|
|
51
|
+
桥接面板不常驻:开关关闭、section 消失、插件卸载时,`dispose()` 成对撤掉 `main` 与
|
|
52
|
+
`sidebar.panellist` 注册;若当时正停在这一页,先 `selectPanel(null)` 回到对话,避免选中一个
|
|
53
|
+
已经不存在的 key。
|
|
54
|
+
|
|
55
|
+
## 同步
|
|
56
|
+
|
|
57
|
+
`settings.section`、`sidebar.panellist` 与 locale 任意一处变化都会触发一次同步,120ms 去抖。
|
|
58
|
+
启动时还会按 `[350, 900, 2000, 4200, 8000, 15000]ms` 主动重扫:boot 清单的顺序不是应用顺序,
|
|
59
|
+
排在后面的插件可能在第一次同步之后才注册设置页。`settings.section` 槽声明时会重新建立订阅,
|
|
60
|
+
因为槽声明之前 `subscribe` 观察不到这个注册表。
|
|
61
|
+
|
|
62
|
+
同步本身只做增删对账,不重建未变化的行;注册成对失败时回滚已注册的那一半。
|
|
63
|
+
|
|
64
|
+
开关存在 `prefs.bridge`(默认 `true`),在「侧栏布局」卡片里;它和排序、隐藏、分组是正交的:
|
|
65
|
+
补出来的行可以像普通行一样拖动、隐藏、分组,关掉开关才会整体消失。
|
|
66
|
+
|
|
67
|
+
## 验证
|
|
68
|
+
|
|
69
|
+
- `npm run check`:`layout-smoke` 断言桥接的常量、持久化字段与自有子槽渲染没有被改掉;
|
|
70
|
+
`bridge-smoke` 在假 `ctx` 上验证识别、成对注册、开关撤回与返回对话。
|
|
71
|
+
- 刷新插件后看 `~/.dsh/panel-control/host.log`:`bridge-scan` 行记录每次扫描看到的 section 与
|
|
72
|
+
侧栏条目(`id:registrant`),`kind=bridge` 行记录最终补出的 id。目标 section 在 `sections`
|
|
73
|
+
里却没进 `ids`,说明它被归属规则当成了已有入口(或 id 不在内置列表里而被排除);一直不在
|
|
74
|
+
`sections` 里,是注册时序问题。
|
|
75
|
+
- `bridge-smoke` 里另有一条晚注册用例:先启动、后注册 section,订阅必须把新行补出来。
|
|
76
|
+
- 界面:侧栏应多出对应行;点开后是完整设置页,顶部「返回对话」可退回;设置 → 面板管理里
|
|
77
|
+
「侧栏布局」应出现开关。
|
|
78
|
+
|
|
79
|
+
## 边界
|
|
80
|
+
|
|
81
|
+
- 第三方设置页在设置壳外渲染时,若它依赖弹窗的私有上下文(宽度、滚动容器、`close` 之外的 props),
|
|
82
|
+
可能和设置弹窗里不完全一致;本插件只保证宿主注入的公开部分齐全。
|
|
83
|
+
- 补出来的行参与自动分类,会被分进 2–4 个组之一;想固定位置就手动拖动或分组。
|
|
84
|
+
- 识别是启发式:设置页 id / label 与面板重合时视为已有入口;新增内置设置页需要加进
|
|
85
|
+
`CORE_SECTION_IDS`。同一个包注册多个设置页会补多行,这是有意的。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-panel-control",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Manage the global panel navigation at the top of the DSH sidebar: reorder, hide and group every entry, with the layout persisted per installation.",
|
|
6
6
|
"keywords": [
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"url": "https://github.com/QIUZAIYOU/dsh-panel-control/issues"
|
|
51
51
|
},
|
|
52
52
|
"scripts": {
|
|
53
|
-
"check": "node --check index.js && node --check client.js && node scripts/host-smoke.mjs && node scripts/layout-smoke.mjs",
|
|
53
|
+
"check": "node --check index.js && node --check client.js && node scripts/host-smoke.mjs && node scripts/layout-smoke.mjs && node scripts/bridge-smoke.mjs",
|
|
54
54
|
"smoke": "node scripts/host-smoke.mjs",
|
|
55
55
|
"doctor": "node scripts/profile-doctor.mjs",
|
|
56
56
|
"enable": "node scripts/enable-local.mjs"
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// Scratch harness: does the bridge discover settings-only plugins, register the
|
|
2
|
+
// panellist+main pair, avoid plugins that already have a row, and honour the switch?
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import vm from 'node:vm';
|
|
5
|
+
|
|
6
|
+
const SRC = readFileSync(new URL('../client.js', import.meta.url), 'utf8');
|
|
7
|
+
|
|
8
|
+
const stubReact = {
|
|
9
|
+
createElement: (type, props, ...children) => ({ type, props: { ...(props || {}), children } }),
|
|
10
|
+
useState: (value) => [value, () => {}],
|
|
11
|
+
useEffect: () => {},
|
|
12
|
+
useRef: (value) => ({ current: value }),
|
|
13
|
+
useMemo: (fn) => fn(),
|
|
14
|
+
useCallback: (fn) => fn,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const element = () => ({
|
|
18
|
+
style: {},
|
|
19
|
+
dataset: {},
|
|
20
|
+
classList: { add() {}, remove() {}, toggle() {} },
|
|
21
|
+
setAttribute() {},
|
|
22
|
+
removeAttribute() {},
|
|
23
|
+
appendChild() {},
|
|
24
|
+
remove() {},
|
|
25
|
+
addEventListener() {},
|
|
26
|
+
removeEventListener() {},
|
|
27
|
+
contains() {
|
|
28
|
+
return false;
|
|
29
|
+
},
|
|
30
|
+
querySelector() {
|
|
31
|
+
return null;
|
|
32
|
+
},
|
|
33
|
+
querySelectorAll() {
|
|
34
|
+
return [];
|
|
35
|
+
},
|
|
36
|
+
textContent: '',
|
|
37
|
+
children: [],
|
|
38
|
+
childNodes: [],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const document = {
|
|
42
|
+
readyState: 'loading',
|
|
43
|
+
head: element(),
|
|
44
|
+
body: element(),
|
|
45
|
+
documentElement: element(),
|
|
46
|
+
title: '',
|
|
47
|
+
visibilityState: 'visible',
|
|
48
|
+
createElement: () => element(),
|
|
49
|
+
createTextNode: () => ({ textContent: '' }),
|
|
50
|
+
createDocumentFragment: () => element(),
|
|
51
|
+
querySelector: () => null,
|
|
52
|
+
querySelectorAll: () => [],
|
|
53
|
+
getElementById: () => null,
|
|
54
|
+
addEventListener() {},
|
|
55
|
+
removeEventListener() {},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function makeWindow(localPrefs) {
|
|
59
|
+
const memory = new Map();
|
|
60
|
+
if (localPrefs !== undefined) memory.set('dsh.panel-control.v1', JSON.stringify(localPrefs));
|
|
61
|
+
const localStorage = {
|
|
62
|
+
getItem: (key) => (memory.has(key) ? memory.get(key) : null),
|
|
63
|
+
setItem: (key, value) => memory.set(key, String(value)),
|
|
64
|
+
removeItem: (key) => memory.delete(key),
|
|
65
|
+
key: () => null,
|
|
66
|
+
length: 0,
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
document,
|
|
70
|
+
localStorage,
|
|
71
|
+
navigator: { language: 'zh-CN' },
|
|
72
|
+
location: { reload() {} },
|
|
73
|
+
CSS: { escape: (value) => String(value) },
|
|
74
|
+
addEventListener() {},
|
|
75
|
+
removeEventListener() {},
|
|
76
|
+
__DSH_BOOT__: {
|
|
77
|
+
rev: 'test-rev',
|
|
78
|
+
entries: [],
|
|
79
|
+
ids: [
|
|
80
|
+
'plugins',
|
|
81
|
+
'dsh-market',
|
|
82
|
+
'panel-control',
|
|
83
|
+
'dsh-flash-presets',
|
|
84
|
+
'billion-context',
|
|
85
|
+
'dshmarket',
|
|
86
|
+
'@deepseek-ai/dsh-client-ui-settings-general',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
__ModuleLoader__: { def: null, load(def) { this.def = def; } },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function section(options, registrant) {
|
|
94
|
+
return { options, component: () => null, registrant };
|
|
95
|
+
}
|
|
96
|
+
function panelEntry(options, registrant) {
|
|
97
|
+
return { options, component: () => null, registrant };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function install(ctx, slots) {
|
|
101
|
+
globalThis.window = ctx;
|
|
102
|
+
globalThis.document = document;
|
|
103
|
+
globalThis.localStorage = ctx.localStorage;
|
|
104
|
+
window.__ModuleLoader__.def = null;
|
|
105
|
+
vm.runInThisContext(SRC, { filename: 'client.js' });
|
|
106
|
+
return window.__ModuleLoader__.def.factory((name) => {
|
|
107
|
+
if (name === 'react') return stubReact;
|
|
108
|
+
throw new Error('unexpected require: ' + name);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const calls = { selectPanel: [], renders: [] };
|
|
113
|
+
const layout = {
|
|
114
|
+
panelInfo: { getSnapshot: () => ({ activePanelId: null }) },
|
|
115
|
+
selectPanel(id) {
|
|
116
|
+
calls.selectPanel.push(id);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
const locale = {
|
|
120
|
+
getLocale: () => ({ active: 'zh-CN' }),
|
|
121
|
+
register: () => {},
|
|
122
|
+
subscribe: () => () => {},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function makeCtx(seedSections = true) {
|
|
126
|
+
const registries = new Map();
|
|
127
|
+
const list = (name) => {
|
|
128
|
+
if (!registries.has(name)) registries.set(name, []);
|
|
129
|
+
return registries.get(name);
|
|
130
|
+
};
|
|
131
|
+
const subscribers = new Map();
|
|
132
|
+
const notify = (name) => {
|
|
133
|
+
for (const callback of subscribers.get(name) || []) Promise.resolve().then(callback);
|
|
134
|
+
};
|
|
135
|
+
const slotService = {
|
|
136
|
+
register(options, component) {
|
|
137
|
+
const record = { options, component, registrant: options.registrant || 'dsh-panel-control' };
|
|
138
|
+
list(options.name).push(record);
|
|
139
|
+
notify(options.name);
|
|
140
|
+
return () => {
|
|
141
|
+
const all = list(options.name);
|
|
142
|
+
const at = all.indexOf(record);
|
|
143
|
+
if (at >= 0) all.splice(at, 1);
|
|
144
|
+
notify(options.name);
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
entriesOfSlot(name) {
|
|
148
|
+
return list(name).slice();
|
|
149
|
+
},
|
|
150
|
+
inject(name, callback) {
|
|
151
|
+
return callback();
|
|
152
|
+
},
|
|
153
|
+
subscribe(name, callback) {
|
|
154
|
+
if (!subscribers.has(name)) subscribers.set(name, []);
|
|
155
|
+
subscribers.get(name).push(callback);
|
|
156
|
+
return () => {};
|
|
157
|
+
},
|
|
158
|
+
renderSlot(name, props, opts) {
|
|
159
|
+
calls.renders.push({ name, props, opts });
|
|
160
|
+
return stubReact.createElement('div', { 'data-slot': name, 'data-only': opts && opts.only });
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
list('sidebar.panellist').push(
|
|
164
|
+
panelEntry({ name: 'sidebar.panellist', id: 'plugins', order: 0, label: '插件' }, 'tk'),
|
|
165
|
+
);
|
|
166
|
+
list('sidebar.panellist').push(
|
|
167
|
+
panelEntry({ name: 'sidebar.panellist', id: 'dsh-market', order: 1, label: '插件市场' }, 'dsh-market'),
|
|
168
|
+
);
|
|
169
|
+
list('sidebar.panellist').push(
|
|
170
|
+
panelEntry({ name: 'sidebar.panellist', id: 'workspace-control', order: 20, label: '工作区管理' }, 'tk'),
|
|
171
|
+
);
|
|
172
|
+
list('settings.section').push(
|
|
173
|
+
section({ name: 'settings.section', id: 'general', order: 0, label: '通用' }, 'tk'),
|
|
174
|
+
);
|
|
175
|
+
list('settings.section').push(
|
|
176
|
+
section({ name: 'settings.section', id: 'market', order: 40, label: '插件市场' }, 'dsh-market'),
|
|
177
|
+
);
|
|
178
|
+
if (seedSections) {
|
|
179
|
+
list('settings.section').push(
|
|
180
|
+
section({ name: 'settings.section', id: 'plan-mode', order: 5, label: '计划模式' }, 'tk'),
|
|
181
|
+
);
|
|
182
|
+
list('settings.section').push(
|
|
183
|
+
section({ name: 'settings.section', id: 'bili', order: 100, label: () => '上下文' }, 'tk'),
|
|
184
|
+
);
|
|
185
|
+
list('settings.section').push(
|
|
186
|
+
section({ name: 'settings.section', id: 'workspace-control-settings', order: 30, label: '工作区管理' }, 'tk'),
|
|
187
|
+
);
|
|
188
|
+
list('settings.section').push(
|
|
189
|
+
section({ name: 'settings.section', id: 'composer-rewind-settings', order: 50, label: '输入历史' }, 'tk'),
|
|
190
|
+
);
|
|
191
|
+
for (const [id, label] of [
|
|
192
|
+
['account', '账户'],
|
|
193
|
+
['models', '模型'],
|
|
194
|
+
['agent-presets', '智能体预设'],
|
|
195
|
+
]) {
|
|
196
|
+
list('settings.section').push(section({ name: 'settings.section', id, order: 1, label }, 'tk'));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
registries,
|
|
201
|
+
subscribers,
|
|
202
|
+
ctx: {
|
|
203
|
+
fiber: { name: 'dsh-panel-control' },
|
|
204
|
+
get(name) {
|
|
205
|
+
if (name === 'layout') return layout;
|
|
206
|
+
if (name === 'locale') return locale;
|
|
207
|
+
return undefined;
|
|
208
|
+
},
|
|
209
|
+
effect(fn) {
|
|
210
|
+
return fn();
|
|
211
|
+
},
|
|
212
|
+
slots: slotService,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
218
|
+
|
|
219
|
+
async function run(localPrefs) {
|
|
220
|
+
const app = makeCtx();
|
|
221
|
+
const mod = install(makeWindow(localPrefs), app.registries);
|
|
222
|
+
mod.apply(app.ctx);
|
|
223
|
+
await sleep(260);
|
|
224
|
+
const rows = app.registries.get('sidebar.panellist') || [];
|
|
225
|
+
const mains = app.registries.get('main') || [];
|
|
226
|
+
const labelOf = (options) => (typeof options.label === 'function' ? options.label() : options.label);
|
|
227
|
+
return {
|
|
228
|
+
ids: rows.map((row) => row.options.id),
|
|
229
|
+
labels: rows.map((row) => labelOf(row.options)),
|
|
230
|
+
mains: mains.map((row) => row.options.key),
|
|
231
|
+
mainRows: mains,
|
|
232
|
+
rows,
|
|
233
|
+
registries: app.registries,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const failures = [];
|
|
238
|
+
const check = (ok, message) => {
|
|
239
|
+
if (!ok) failures.push(message);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Boot order is not registration order: a plugin that applies after us adds its
|
|
243
|
+
// settings section later. The subscription installed when the slot is declared
|
|
244
|
+
// must pick that up without a reload.
|
|
245
|
+
async function runLate() {
|
|
246
|
+
const app = makeCtx(false);
|
|
247
|
+
const mod = install(makeWindow(undefined), app.registries);
|
|
248
|
+
mod.apply(app.ctx);
|
|
249
|
+
await sleep(300);
|
|
250
|
+
const before = (app.registries.get('sidebar.panellist') || []).map((row) => row.options.id);
|
|
251
|
+
app.ctx.slots.register(
|
|
252
|
+
{ name: 'settings.section', id: 'plan-mode', order: 5, label: '计划模式', registrant: 'tk' },
|
|
253
|
+
() => null,
|
|
254
|
+
);
|
|
255
|
+
app.ctx.slots.register(
|
|
256
|
+
{ name: 'settings.section', id: 'bili', order: 100, label: () => '上下文', registrant: 'tk' },
|
|
257
|
+
() => null,
|
|
258
|
+
);
|
|
259
|
+
await sleep(420);
|
|
260
|
+
const after = (app.registries.get('sidebar.panellist') || []).map((row) => row.options.id);
|
|
261
|
+
return { before, after };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const late = await runLate();
|
|
265
|
+
check(!late.before.some((id) => id.startsWith('dpc-bridge-')), 'bridged a section before it was registered');
|
|
266
|
+
check(
|
|
267
|
+
late.after.includes('dpc-bridge-plan-mode') && late.after.includes('dpc-bridge-bili'),
|
|
268
|
+
'late-registered settings sections were not bridged',
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const on = await run(undefined);
|
|
272
|
+
check(on.ids.includes('dpc-bridge-plan-mode'), 'missing bridge row for dsh-flash-presets');
|
|
273
|
+
check(on.ids.includes('dpc-bridge-bili'), 'missing bridge row for billion-context');
|
|
274
|
+
check(on.ids.includes('dpc-bridge-composer-rewind-settings'), 'missing bridge row for dsh-composer-rewind');
|
|
275
|
+
check(!on.ids.includes('dpc-bridge-market'), 'bridged dshmarket even though it has a panel');
|
|
276
|
+
check(!on.ids.includes('dpc-bridge-general'), 'bridged a core settings section');
|
|
277
|
+
check(!on.ids.includes('dpc-bridge-account'), 'bridged a core settings section');
|
|
278
|
+
check(!on.ids.includes('dpc-bridge-models'), 'bridged a core settings section');
|
|
279
|
+
check(!on.ids.includes('dpc-bridge-agent-presets'), 'bridged a core settings section');
|
|
280
|
+
check(!on.ids.includes('dpc-bridge-workspace-control-settings'), 'bridged a plugin whose panel id is the section base');
|
|
281
|
+
check(on.mains.includes('dpc-bridge-plan-mode') && on.mains.includes('dpc-bridge-bili'), 'a bridge row has no main entry');
|
|
282
|
+
const planRow = on.rows.find((row) => row.options.id === 'dpc-bridge-plan-mode');
|
|
283
|
+
check(planRow && planRow.options.order === 85, 'bridge rows lost their order');
|
|
284
|
+
check(planRow && planRow.options.label() === '计划模式', 'bridge label is not the section label');
|
|
285
|
+
|
|
286
|
+
const tree = on.rows.find((row) => row.options.id === 'dpc-bridge-bili').component({ size: 18, active: false });
|
|
287
|
+
check(tree && tree.type === 'svg', 'bridge icon is not an svg');
|
|
288
|
+
|
|
289
|
+
const off = await run({ version: 2, bridge: false, order: ['plugins', 'dsh-market', 'panel-control'] });
|
|
290
|
+
check(!off.ids.some((id) => id.startsWith('dpc-bridge-')), 'the switch did not remove the bridge rows');
|
|
291
|
+
|
|
292
|
+
// The settings shell owns `settings.section`, so the ctx-level renderSlot cannot
|
|
293
|
+
// reach it. The bridge declares its own child slot, registers the section
|
|
294
|
+
// component into it, and renders it through the props face the host binds.
|
|
295
|
+
const biliMain = on.mainRows.find((row) => row.options.key === 'dpc-bridge-bili');
|
|
296
|
+
check(Boolean(biliMain), 'no main key for the bridged row');
|
|
297
|
+
const slotName = 'dpc-bridge-slot-bili';
|
|
298
|
+
const declared = biliMain && biliMain.options.children;
|
|
299
|
+
check(Boolean(declared && declared[slotName]), 'the bridge main entry declared no child slot');
|
|
300
|
+
check(declared && declared[slotName] && declared[slotName].kind === 'single', 'the bridge child slot is not single');
|
|
301
|
+
const face = (name, props, opts) => {
|
|
302
|
+
if (!declared || !declared[name]) throw new Error('slot ' + name + ' is not declared by this entry children');
|
|
303
|
+
calls.renders.push({ name, props, opts });
|
|
304
|
+
return stubReact.createElement('div', { 'data-slot': name });
|
|
305
|
+
};
|
|
306
|
+
const view = biliMain ? biliMain.component({ renderSlot: face }) : null;
|
|
307
|
+
const slot = calls.renders.find((render) => render.name === slotName);
|
|
308
|
+
check(Boolean(slot), 'the bridge page did not render its child slot');
|
|
309
|
+
check(slot && typeof slot.props.close === 'function', 'the bridge page gave the section no close');
|
|
310
|
+
check(Boolean(view) && view.type === 'div', 'the bridge page did not render its frame');
|
|
311
|
+
const childEntries = on.registries.get(slotName) || [];
|
|
312
|
+
check(childEntries.length === 1, 'the section component was not re-registered into the bridge slot');
|
|
313
|
+
check(childEntries[0] && typeof childEntries[0].component === 'function', 'the bridge slot has no component');
|
|
314
|
+
if (slot) slot.props.close();
|
|
315
|
+
check(calls.selectPanel.includes(null), 'closing the bridge page did not select the Conversation');
|
|
316
|
+
|
|
317
|
+
console.log(JSON.stringify({ on: on.ids, onLabels: on.labels, mains: on.mains, off: off.ids, renders: calls.renders.length }, null, 2));
|
|
318
|
+
if (failures.length) {
|
|
319
|
+
console.error('bridge-smoke: ' + failures.length + ' problem(s)');
|
|
320
|
+
for (const failure of failures) console.error(' - ' + failure);
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
console.log('bridge-smoke: ok');
|
|
324
|
+
process.exit(0);
|
package/scripts/layout-smoke.mjs
CHANGED
|
@@ -27,12 +27,28 @@ for (const marker of [
|
|
|
27
27
|
'layoutGuard',
|
|
28
28
|
'layoutMaxNone',
|
|
29
29
|
'toggleCollapse',
|
|
30
|
+
'layoutBridge',
|
|
31
|
+
'dpc-bridgeBar',
|
|
32
|
+
'registrantOf',
|
|
30
33
|
]) {
|
|
31
34
|
expect(client.includes(marker), 'client.js lost ' + marker);
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
// A bridged row is only as good as the two halves it registers: a keyed main
|
|
38
|
+
// entry the layout can select, and the host-rendered settings section it shows.
|
|
39
|
+
expect(client.includes('BRIDGE_ORDER = 85'), 'bridged rows lost their order slot');
|
|
40
|
+
expect(client.includes('out.bridge = raw.bridge !== false'), 'the bridge switch is not persisted');
|
|
41
|
+
expect(
|
|
42
|
+
client.includes('BRIDGE_SLOT_PREFIX') &&
|
|
43
|
+
client.includes('children: { [slotName]: { kind: \'single\', scope: \'root\' } }') &&
|
|
44
|
+
client.includes('renderSlot(slotName, { close: back }, {})'),
|
|
45
|
+
'the bridged page no longer renders through its own child slot',
|
|
46
|
+
);
|
|
47
|
+
expect(/\.dpc-bridgeBar\{[^}]*min-height:40px/.test(client), 'the bridge bar lost its 40px row height');
|
|
48
|
+
|
|
34
49
|
expect(client.includes("const BUILD_STAMP = 'dsh-panel-control/" + pkg.version + "'"), 'BUILD_STAMP and package.json version disagree');
|
|
35
50
|
expect(pkg.scripts && String(pkg.scripts.check).includes('layout-smoke'), 'npm run check no longer runs this gate');
|
|
51
|
+
expect(pkg.scripts && String(pkg.scripts.check).includes('bridge-smoke'), 'npm run check no longer runs the bridge gate');
|
|
36
52
|
expect(/\[data-dpc-list\]\{[^}]*overflow-y:auto/.test(client), 'the panel list is not scrollable');
|
|
37
53
|
expect(/\[data-dpc-list\]\{[^}]*max-height:max\(120px,var\(--dpc-panellist-max,45vh\)\)/.test(client), 'the panel list lost its height cap');
|
|
38
54
|
expect(/\[data-dpc-region\]\{[^}]*min-height/.test(client), 'the region below the list lost its floor');
|