dsh-panel-control 0.9.1 → 0.9.2

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 CHANGED
@@ -26,7 +26,7 @@
26
26
 
27
27
  补出来的设置页入口也出现在同一个侧栏列表里,排在普通入口之后、本插件自己之前;标题沿用设置页自己的名称,重复时附带设置页 id 以区分。
28
28
 
29
- 改过名的入口仍把原来的注册名留在按钮属性里,引擎靠它把自己映射回同一条注册,所以宿主的排序、重渲染和后续重扫都不受影响;行尾标签显示的是插件包名(由 Host 的模块图解析,`dsh-` 前缀已省略),屏幕阅读器与工具提示读到的则是你设定的显示名。
29
+ 改过名的入口仍把原来的注册名留在按钮属性里,引擎靠它把自己映射回同一条注册,所以宿主的排序、重渲染和后续重扫都不受影响;行尾标签显示的是插件包名(由 Host 从安装目录的 `package.json` 解析,`dsh-` 前缀已省略),屏幕阅读器与工具提示读到的则是你设定的显示名。
30
30
 
31
31
  ## 安装
32
32
 
@@ -36,7 +36,7 @@
36
36
 
37
37
  ```bash
38
38
  cd C:/Users/<you>/.dsh/profiles/desktop
39
- npm install dsh-panel-control@0.9.1
39
+ npm install dsh-panel-control@0.9.2
40
40
  ```
41
41
 
42
42
  把包名加进同一目录 `package.json` 的 `dsh.profile.bundles`:
@@ -101,7 +101,7 @@ npm run doctor # 体检 profile 安装状态
101
101
  - `scripts/host-smoke.mjs`:驱动宿主侧路由,覆盖设置读写、分类请求与 bundle 兜底。
102
102
  - `scripts/layout-smoke.mjs`:断言侧栏布局的 CSS 钩子与关键数值,并校验 `BUILD_STAMP` 与版本号一致。
103
103
  - `scripts/bridge-smoke.mjs`:在假 ctx 上跑 `client.js`,断言只给「有设置页、没有侧栏入口」的第三方插件补行,且成对注册 main、开关关闭后整体撤回、桥接页经自有子槽交给宿主渲染并能返回对话。
104
- - `scripts/names-smoke.mjs`:用迷你 DOM 驱动真实引擎,断言改名覆盖宿主标题与 `aria-label`、包名标签的位置与开关、清空恢复、窄侧栏不出标签、重复名称仍能按位置映射、未知包回退到入口 id,以及 `dispose` 后 DOM 原样归还。
104
+ - `scripts/names-smoke.mjs`:用迷你 DOM 驱动真实引擎,断言改名覆盖宿主标题与 `aria-label`、包名标签的位置与开关、清空恢复、窄侧栏不出标签、重复名称仍能按位置映射、未知包不显示标签,以及 `dispose` 后 DOM 原样归还。
105
105
 
106
106
  进一步材料:[docs/sidebar-layout.md](docs/sidebar-layout.md)(布局与折叠实现)、[docs/renaming-panels.md](docs/renaming-panels.md)(重命名与包名标签)、[docs/bridged-settings-pages.md](docs/bridged-settings-pages.md)(设置页补入口的识别与渲染)、[docs/ui-design-notes.md](docs/ui-design-notes.md)(界面规范与踩坑)、[docs/discussion-bundle-evaluated-twice.md](docs/discussion-bundle-evaluated-twice.md)(bundle 被求值两次的事故复盘)。
107
107
 
@@ -110,7 +110,7 @@ npm run doctor # 体检 profile 安装状态
110
110
  - 自动分类会把入口名称发给当前 profile 配置的模型服务商;不想要就关掉开关,分组仍可手动编辑。
111
111
  - 分类结果只是建议,可以整体重排或改名。
112
112
  - 依赖宿主侧栏的 DOM 结构(`[class*="panelList"]` 与面板按钮的 `aria-label`),官方大改版时需要跟进。
113
- - 重命名只覆盖侧栏的显示层:宿主标题文本、`aria-label` 与包名标签都是本插件写的,插件自己的配置和入口 id 不变;宿主重渲染后由下一次扫描补回。图标条模式没有标题文本,只改可访问名称,也不显示包名标签。包名由 Host 模块图解析,接口不可用时标签回退为入口 id。
113
+ - 重命名只覆盖侧栏的显示层:宿主标题文本、`aria-label` 与包名标签都是本插件写的,插件自己的配置和入口 id 不变;宿主重渲染后由下一次扫描补回。图标条模式没有标题文本,只改可访问名称,也不显示包名标签。包名由 Host 从模块所在安装目录的 `package.json` 解析(模块 id 不等于包名,例如 `dsh-enhance-ui` → `@wufeng0105/dsh-enhance`),接口不可用或解析不到时该行不显示标签。
114
114
  - 只写 `style.order`、`style.display` 与 `data-dpc-*` 属性,不移动 React 节点,不改宿主代码。
115
115
  - 排序、隐藏、分组都只影响本机这份安装。
116
116
  - 设置页入口按「section id / label 是否已被某个面板占用」识别归属,不依赖插件声明的注册者(client runner 会把多个插件报成同一个名字);代价是启发式:与面板同名的第三方设置页会被视为已有入口,新增内置设置页需要更新 `CORE_SECTION_IDS`。第三方设置页按宿主渲染器原位渲染,若它自己依赖设置弹窗的私有上下文,可能出现布局差异。
package/client.js CHANGED
@@ -5,7 +5,7 @@
5
5
  // Everything below therefore lives inside this closure. Re-evaluation must stay possible: when the
6
6
  // plugin is switched off the loader drops our factory, so switching it back on re-executes this
7
7
  // script and needs the registration that follows.
8
- const BUILD_STAMP = 'dsh-panel-control/0.9.1';
8
+ const BUILD_STAMP = 'dsh-panel-control/0.9.2';
9
9
  try { window.__dpcEvaluated = BUILD_STAMP; } catch { /* ignore */ }
10
10
  /**
11
11
  * Client half of dsh-panel-control.
@@ -1038,8 +1038,10 @@ function build(require) {
1038
1038
  };
1039
1039
 
1040
1040
  function packageTagFor(id) {
1041
- const pkg = packageState.map[id];
1042
- return packageLabel(pkg) || packageLabel(id);
1041
+ // No fallback to the row id: a bridge row's id is `dpc-bridge-*` and an
1042
+ // unresolved panel id is not a package name. An empty tag is better than
1043
+ // a second, wrong spelling of the same plugin.
1044
+ return packageLabel(packageState.map[id]);
1043
1045
  }
1044
1046
 
1045
1047
  /**
@@ -2496,7 +2498,15 @@ function build(require) {
2496
2498
  'span',
2497
2499
  { className: 'dpc-bridgeTags' },
2498
2500
  h('span', { className: 'dpc-bridgeTag' }, t('bridgeTag')),
2499
- h('span', { className: 'dpc-bridgeFrom' }, fill(t('bridgeFrom'), { p: candidate.sectionId })),
2501
+ h(
2502
+ 'span',
2503
+ { className: 'dpc-bridgeFrom' },
2504
+ fill(t('bridgeFrom'), {
2505
+ // The same package short name the row tag shows; the section
2506
+ // id is only a fallback while the Host lookup is in flight.
2507
+ p: packageTagFor(candidate.bridgeId) || candidate.sectionId,
2508
+ }),
2509
+ ),
2500
2510
  ),
2501
2511
  ),
2502
2512
  node
@@ -44,8 +44,8 @@
44
44
  `props.renderSlot(slotName, { close: back }, {})` 渲染它。
45
45
 
46
46
  这样 section 的 `inject`、`store`、locale `t`、`useStore` 和 error boundary 仍由宿主照常提供;直接调用
47
- section 组件会缺这些。面板只负责外壳:顶部一条 40px 的栏(「返回对话」+ 来源 section id),
48
- 下面一块随页面滚动的主体。`close` 与设置壳里一样只做「关掉这一页」,这里落回对话
47
+ section 组件会缺这些。面板只负责外壳:顶部一条 40px 的栏(「返回对话」+ 来源包名短名,
48
+ 查不到包名时退回 section id),下面一块随页面滚动的主体。`close` 与设置壳里一样只做「关掉这一页」,这里落回对话
49
49
  (`layout.selectPanel(null)`)。
50
50
 
51
51
  桥接面板不常驻:开关关闭、section 消失、插件卸载时,`dispose()` 成对撤掉 `main` 与
@@ -17,20 +17,21 @@
17
17
  `sidebar.panellist` 条目只有 `id/order/label/registrant`,没有包名字段;`registrant` 是 Cordis fiber 名,运行时大多数插件共享同一个 fiber,无法拿去区分插件。包名只存在于宿主自己的模块图里,所以:
18
18
 
19
19
  1. 客户端把当前面板 id 与桥接设置页 id 发给 Host 的 `packages` RPC;
20
- 2. Host 用 `clientModules.graph().entries` 拿到模块 id(即包名)与 `clientModules.clientPath(id)` 返回的 bundle 路径;
20
+ 2. Host 用 `clientModules.graph().entries` 拿到模块 id 与 `clientModules.clientPath(id)` 返回的 bundle 路径;模块 id 不一定是包名(例如 `dsh-enhance-ui` 是 `@wufeng0105/dsh-enhance` 的 UI 子包);
21
21
  3. 逐个 bundle 查找槽字面量 `sidebar.panellist` / `settings.section`,并优先接受紧邻 `id:` / `key:`(或赋值)的该 id;没有注册字段时只接受距离 400 字符以内的提及,避免把注释里顺带提到的通用词当成归属;
22
- 4. 命中的模块 id 就是包名,按模块图 revision 缓存。
22
+ 4. 命中的模块再按路径归位到 `node_modules` 下最近的安装包目录,读该目录 `package.json` 的 `name`;子目录里的 `ui/package.json` 不会覆盖根包名。结果按模块图 revision 缓存。
23
23
 
24
24
  标签文本会去掉 scope 与 `dsh-` 前缀,再退掉 `client-ui-`:
25
25
 
26
- | 模块 id | 标签 |
26
+ | 包名 | 标签 |
27
27
  | --- | --- |
28
28
  | `dsh-panel-control` | `panel-control` |
29
29
  | `@furongjun1999/dsh-memory` | `memory` |
30
30
  | `dshmarket` | `market` |
31
31
  | `@deepseek-ai/dsh-client-ui-plugin-manager` | `plugin-manager` |
32
+ | `@wufeng0105/dsh-enhance`(模块 id `dsh-enhance-ui`) | `enhance` |
32
33
 
33
- Host 尚未返回或确实找不到时,回退到入口 id 本身(同样去 `dsh-` 前缀),所以标签绝不会空白。
34
+ Host 尚未返回或确实找不到包名时,这一行不显示标签——入口 id 不是包名,宁可空白也不要第二种写法。
34
35
 
35
36
  ## 映射回原条目
36
37
 
@@ -65,10 +66,10 @@ Host 尚未返回或确实找不到时,回退到入口 id 本身(同样去 `
65
66
  ## 边界
66
67
 
67
68
  - 只覆盖显示,不改变插件加载、入口 id 或插件自己的配置;停用本插件后宿主标题原样恢复。
68
- - 包名识别依赖 Host 的模块图接口与 bundle 内容。接口不存在、模块读不到或代码里没有可识别的注册字面量时,标签退化为入口 id;不会影响改名本身。
69
+ - 包名识别依赖 Host 的模块图接口与 bundle 内容。接口不存在、模块读不到或代码里没有可识别的注册字面量时,该行不显示标签;不会影响改名本身。
69
70
  - 宿主大改版若移除 `span.panelTitle` 或改写按钮结构,重命名会退化为只改 `aria-label`;找不到对应行时该行保持原样,不会报错。
70
71
  - 标签 `pointer-events: none`,不参与点击与拖拽;分组下拉菜单铺了不透明底色,避免透出下面的行。
71
72
 
72
73
  ## 验证
73
74
 
74
- `scripts/names-smoke.mjs` 用迷你 DOM 驱动真实引擎,覆盖改名、关标签、清空恢复、窄侧栏、重复名称映射、包名标签、未知包回退与 `dispose` 还原;`npm run check` 会运行它。
75
+ `scripts/names-smoke.mjs` 用迷你 DOM 驱动真实引擎,覆盖改名、关标签、清空恢复、窄侧栏、重复名称映射、包名标签、未知包不显示标签与 `dispose` 还原;`npm run check` 会运行它。
package/index.js CHANGED
@@ -602,6 +602,42 @@ async function readModuleText(modules, id, texts) {
602
602
  return text;
603
603
  }
604
604
 
605
+ /**
606
+ * The installed package that owns a served module.
607
+ *
608
+ * A client entry id is not a package name: `dsh-enhance-ui` is the UI
609
+ * subpackage of `@wufeng0105/dsh-enhance`, and the plugin manager labels the
610
+ * whole plugin with the latter. The package directory is the one directly
611
+ * under the last `node_modules` segment; its package.json `name` is what the
612
+ * plugin manager shows, scoped and `dsh-`-stripped by the Client.
613
+ */
614
+ function installedPackageDir(file) {
615
+ const parts = String(file).split(/[\\/]/);
616
+ const at = parts.lastIndexOf('node_modules');
617
+ if (at === -1) return path.dirname(file);
618
+ const rest = parts.slice(at + 1);
619
+ const pkg = rest[0] && rest[0].startsWith('@') && rest[1] ? [rest[0], rest[1]] : [rest[0]];
620
+ if (!pkg[0]) return path.dirname(file);
621
+ return path.join(parts.slice(0, at + 1).join(path.sep), ...pkg);
622
+ }
623
+
624
+ async function packageNameOf(modules, moduleId, names) {
625
+ if (names.has(moduleId)) return names.get(moduleId);
626
+ let name = moduleId;
627
+ try {
628
+ const file = modules.clientPath(moduleId);
629
+ if (file) {
630
+ const manifest = path.join(installedPackageDir(file), 'package.json');
631
+ const parsed = JSON.parse(await fs.readFile(manifest, 'utf8'));
632
+ if (parsed && typeof parsed.name === 'string' && parsed.name.trim()) name = parsed.name.trim();
633
+ }
634
+ } catch {
635
+ name = moduleId;
636
+ }
637
+ names.set(moduleId, name);
638
+ return name;
639
+ }
640
+
605
641
  async function packageMap(scope, payload) {
606
642
  const modules = clientModules || serviceOf(scope, 'clientModules');
607
643
  if (!modules || typeof modules.graph !== 'function' || typeof modules.clientPath !== 'function') {
@@ -630,6 +666,7 @@ async function packageMap(scope, payload) {
630
666
  const packages = {};
631
667
  const best = new Map();
632
668
  const texts = new Map();
669
+ const names = new Map();
633
670
  for (const entry of graph.entries) {
634
671
  const moduleId = entry && typeof entry.id === 'string' ? entry.id : '';
635
672
  if (!moduleId) continue;
@@ -650,7 +687,7 @@ async function packageMap(scope, payload) {
650
687
  best.set(want.id, { moduleId, strong: match.strong, distance: match.distance });
651
688
  }
652
689
  }
653
- for (const [id, match] of best) packages[id] = match.moduleId;
690
+ for (const [id, match] of best) packages[id] = await packageNameOf(modules, match.moduleId, names);
654
691
  const value = { packages, rev: typeof graph.rev === 'string' ? graph.rev : null };
655
692
  packageCache.set(key, value);
656
693
  while (packageCache.size > PACKAGE_CACHE_KEPT) packageCache.delete(packageCache.keys().next().value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-panel-control",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "dsh": ">=0.2.1-rc.3"
@@ -71,6 +71,20 @@ const pkgPaths = {
71
71
  'billion-context': await pkgFile('bili.js', "register({ name: 'settings.section', id: 'bili' });\n"),
72
72
  };
73
73
 
74
+ // A module id can differ from the installed package: the UI entry of an
75
+ // enhance-style plugin is `dsh-widget-ui`, while the plugin manager labels the
76
+ // plugin with the root package `@acme/dsh-widget`. The nested `ui/package.json`
77
+ // must not win over the package directory.
78
+ const widgetDir = path.join(PKG_DIR, 'node_modules', '@acme', 'dsh-widget');
79
+ await fs.mkdir(path.join(widgetDir, 'ui'), { recursive: true });
80
+ await fs.writeFile(path.join(widgetDir, 'package.json'), JSON.stringify({ name: '@acme/dsh-widget' }));
81
+ await fs.writeFile(path.join(widgetDir, 'ui', 'package.json'), JSON.stringify({ name: 'dsh-widget-ui' }));
82
+ await fs.writeFile(
83
+ path.join(widgetDir, 'ui', 'client.js'),
84
+ "register({ name: 'settings.section', id: 'widget' });\n",
85
+ );
86
+ pkgPaths['@acme/dsh-widget-ui'] = path.join(widgetDir, 'ui', 'client.js');
87
+
74
88
  const clientModulesStub = {
75
89
  served: [],
76
90
  clientPath: (id) => pkgPaths[id] || null,
@@ -83,6 +97,7 @@ const clientModulesStub = {
83
97
  { id: 'dsh-panel-control', rev: 'rev-now', url: 'plugins/??dsh-panel-control/client.js&rev=rev-now' },
84
98
  { id: 'dshmarket', rev: 'rev-now', url: 'plugins/??dshmarket/client.js&rev=rev-now' },
85
99
  { id: 'billion-context', rev: 'rev-now', url: 'plugins/??billion-context/client.js&rev=rev-now' },
100
+ { id: '@acme/dsh-widget-ui', rev: 'rev-now', url: 'plugins/??@acme/dsh-widget-ui/client.js&rev=rev-now' },
86
101
  // The plugin that sits next to this one in the real profile: it carries no
87
102
  // fallback of its own, so a retired revision used to drop it silently
88
103
  // while the manager still reported it running.
@@ -265,7 +280,8 @@ if (route) {
265
280
  check(
266
281
  'inventory ids',
267
282
  Array.isArray(body.result.value.ids) &&
268
- body.result.value.ids.join() === 'a,b,dsh-panel-control,dshmarket,billion-context,dsh-workspace-control',
283
+ body.result.value.ids.join() ===
284
+ 'a,b,dsh-panel-control,dshmarket,billion-context,@acme/dsh-widget-ui,dsh-workspace-control',
269
285
  body.result.value && body.result.value.ids.join(),
270
286
  );
271
287
  const pkgResponse = await route.fetch(
@@ -275,6 +291,7 @@ if (route) {
275
291
  { id: 'panel-control', slot: 'sidebar.panellist' },
276
292
  { id: 'dsh-market', slot: 'sidebar.panellist' },
277
293
  { id: 'bili', slot: 'settings.section' },
294
+ { id: 'widget', slot: 'settings.section' },
278
295
  { id: 'nobody', slot: 'sidebar.panellist' },
279
296
  ],
280
297
  }),
@@ -285,6 +302,11 @@ if (route) {
285
302
  check('packages maps panel id', packages['panel-control'] === 'dsh-panel-control', JSON.stringify(packages));
286
303
  check('packages maps market id', packages['dsh-market'] === 'dshmarket', JSON.stringify(packages));
287
304
  check('packages maps settings id', packages.bili === 'billion-context', JSON.stringify(packages));
305
+ check(
306
+ 'packages resolves the installed package, not the module id',
307
+ packages.widget === '@acme/dsh-widget',
308
+ JSON.stringify(packages),
309
+ );
288
310
  check('packages leaves unknown ids unmapped', !packages.nobody, JSON.stringify(packages));
289
311
  } catch (error) {
290
312
  check('inventory answers', false, error.message);
@@ -389,9 +389,9 @@ check(dupButtons[0].getAttribute('aria-label') === '甲', 'the first duplicate-l
389
389
  check(dupButtons[1].getAttribute('aria-label') === '相同', 'the second duplicate-label row was renamed by mistake');
390
390
  check(tagOf(dupButtons[0]).textContent === 'dup-a', 'the duplicate row tag did not use its package');
391
391
 
392
- // 5b. An id the Host cannot map falls back to the id itself, with `dsh-` gone.
392
+ // 5b. An id the Host cannot map has no tag: the row id is not a package name.
393
393
  const unmapped = await run({ version: 2, names: { 'workspace-control': '工作区' }, tags: true });
394
- check(tagOf(unmapped.buttons[2]).textContent === 'workspace-control', 'an unmapped package did not fall back to its id');
394
+ check(!tagOf(unmapped.buttons[2]), 'an unmapped package fell back to its row id');
395
395
 
396
396
  // 6. Dispose restores the host DOM.
397
397
  const disposed = await run({ version: 2, names: { plugins: '我的插件' }, tags: true });