dsh-model-organizer 0.3.2 → 0.3.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/DEVELOPING.md CHANGED
@@ -4,7 +4,29 @@
4
4
 
5
5
  ## ⚠️ 改代码前务必先读:踩过的坑
6
6
 
7
- 1. **profile bundle 必须声明 `dsh.bundle.patch`**(指向 `cordis.patch.yml`)。缺了它 dsh 启动即报
7
+ 0. **服务改名会让座位"静默消失",必须注入新旧两个名字。**
8
+ DSH 0.1.7 把客户端设置镜像服务从 `settingsScope` 改名为 `configForms`(由 `@deepseek-ai/dsh-client-ui-settings` 提供,`describe()` 返回的还是同一个 mirror 对象)。
9
+ cordis 的 `ctx.inject([...])` **只要有一个服务永不出现,回调就永远不执行** —— 不报错、不打日志。所以 0.1.7 一升级,本插件的设置页浮窗就凭空消失了(输入框的模型菜单照常,因为它注入的 `slots/modelDirectories/sessions` 都没变)。
10
+ 现在 `apply()` 里同时注入两个名字,用一次性标志保证只挂载一次:
11
+ ```js
12
+ ctx.inject(["slots", "configForms", "remote", "remote.settings"], (s) => mountOrderPanel(s, s.configForms.describe()));
13
+ ctx.inject(["slots", "settingsScope", "remote", "remote.settings"], (s) => mountOrderPanel(s, s.settingsScope.describe()));
14
+ ```
15
+ **推论**:升级 DSH 后如果某个座位不见了,第一件事是去新版本的 `dsh-client-ui-*` 包里 grep 自己注入的每个服务名。
16
+
17
+ 1. **`file:` 依赖是打包快照,`link:` 才是符号链接 —— 改仓库可能根本不生效。**
18
+ profile 里写 `"dsh-model-organizer": "file:../../../dsh-model-organizer"` 时,pnpm 把目录**打包复制**进自己的 store;只有 `pnpm add ./dir`(直接路径)或 `link:` 才是符号链接。
19
+ 症状:改完仓库代码、强刷浏览器,界面毫无变化;查 `window.__dshModelOrganizerBuild` 会发现跑的还是旧构建号,而服务端 bundle 里明明是新代码 —— 因为 DSH 加载的是 `profiles/<p>/node_modules/dsh-model-organizer/lib/client.js` 那份**副本**。
20
+ 排查:直接 grep 那份副本的 `BUILD` 常量,和仓库对比。
21
+ 修法:`dsh plugin --profile web remove dsh-model-organizer && dsh plugin --profile web add link:./dsh-model-organizer`(换成符号链接后只改一处)。在此之前必须**两份同步改**。
22
+
23
+ 2. **0.1.7 的菜单色 token 是半透明的,浮层必须配 `backdrop-filter`。**
24
+ `--dsw-specific-menu` 现在解析成 `rgba(48,49,54,0.5)`,官方菜单靠 `backdrop-filter: var(--dsw-menu-backdrop-filter)` 配合。只取颜色不取模糊 → 面板是「透视」的,底下的官方卡片按钮直接透上来。
25
+
26
+ 3. **浮层要 `ReactDOM.createPortal` 到 `document.body`。**
27
+ `settings.models.footer` 座位在设置页很深的子树里,`position: fixed` 会被困在那个子树的堆叠上下文里,被别的插件的悬浮组件(鲸鱼、地球图标等)盖住 —— 提高 z-index 也没用。挂到 body 之后 z-index 才是全局的(本插件用 10000,官方菜单是 1100)。
28
+
29
+ 4. **profile bundle 必须声明 `dsh.bundle.patch`**(指向 `cordis.patch.yml`)。缺了它 dsh 启动即报
8
30
  `profile bundle "..." declares no dsh.bundle in its package.json`。
9
31
 
10
32
  2. **顶层 `inject` 必须声明座位 standardProps 会读取的每一个服务**(本插件:`locale, slots, sessions, remote, remote.session, remote.settings`)。
package/README.md CHANGED
@@ -36,6 +36,8 @@ dsh plugin --profile web add github:NimoXie15/dsh-model-organizer
36
36
  dsh plugin --profile web add link:/absolute/path/to/dsh-model-organizer
37
37
  ```
38
38
 
39
+ > **兼容性**:已在 DSH `0.1.7-rc.1` 上验证,同时兼容 `0.1.5`/`0.1.6`(0.1.7 把客户端设置镜像服务从 `settingsScope` 改名为 `configForms`,本插件两个名字都注入,所以两个版本都能用)。
40
+ >
39
41
  > 升级插件后如果界面没变化,按 **Ctrl+Shift+R** 强刷一次(DSH 的客户端 bundle 带一年 immutable 缓存,普通刷新不会重新下载)。
40
42
 
41
43
  ## 截图
package/lib/client.js CHANGED
@@ -90,7 +90,7 @@ window.__ModuleLoader__.load({
90
90
  code changes, so a browser can keep running an old build until a hard reload.
91
91
  Kept OFF the UI (it is not user-facing): read it as
92
92
  `window.__dshModelOrganizerBuild` when a stale build is suspected. */
93
- const BUILD = "b23";
93
+ const BUILD = "b24";
94
94
 
95
95
  let officialClasses = null;
96
96
  let officialClassesSheets = -1;
@@ -265,11 +265,16 @@ window.__ModuleLoader__.load({
265
265
  /* Floating presentation: the panel sits in the corner of the Models page so
266
266
  provider/API configuration and reordering are visible at the same time. */
267
267
  floatPanel: {
268
- position: "fixed", right: 20, bottom: 20, zIndex: 900, width: 380, maxWidth: "calc(100vw - 40px)",
268
+ position: "fixed", right: 20, bottom: 20, zIndex: 10000, width: 380, maxWidth: "calc(100vw - 40px)",
269
269
  display: "flex", flexDirection: "column", padding: 4,
270
270
  border: "1px solid var(--dsw-alias-border-l1)", borderRadius: 14,
271
271
  background: "var(--dsw-specific-menu, var(--dsw-alias-bg-module, #222))",
272
- boxShadow: "0 10px 30px rgba(0,0,0,.34)", color: "var(--dsw-alias-label-primary)"
272
+ /* 0.1.7's menu token is translucent and the shipped menu pairs it with this
273
+ blur; without it the panel reads as see-through and the cards behind it
274
+ bleed through. The elevation shadow matches the shipped menu too. */
275
+ backdropFilter: "var(--dsw-menu-backdrop-filter)",
276
+ boxShadow: "var(--dsw-elevation-prominent, 0 10px 30px rgba(0,0,0,.34))",
277
+ color: "var(--dsw-alias-label-primary)"
273
278
  },
274
279
  /* The shipped `cell` sets min-width:100%; the head must stay inside the panel. */
275
280
  panelHeadCell: { display: "flex", alignItems: "center", gap: 6, flex: "1 1 auto", minWidth: 0 },
@@ -1105,6 +1110,38 @@ window.__ModuleLoader__.load({
1105
1110
  return known.concat(rest);
1106
1111
  }
1107
1112
 
1113
+ /* A seat entry that throws is silently abdicated, so "my panel is missing" and
1114
+ "my panel threw" look identical from the page. This boundary turns the throw
1115
+ into one visible line, so the failure can be reported instead of guessed. */
1116
+ class PanelBoundary extends React.Component {
1117
+ constructor(props) {
1118
+ super(props);
1119
+ this.state = { err: null };
1120
+ }
1121
+ static getDerivedStateFromError(err) {
1122
+ return { err: err };
1123
+ }
1124
+ render() {
1125
+ if (this.state.err !== null) {
1126
+ const message = String((this.state.err && this.state.err.message) || this.state.err);
1127
+ return h("div", {
1128
+ "data-mo": "panel-error",
1129
+ style: {
1130
+ margin: "8px 0", padding: "6px 10px", borderRadius: 8, fontSize: 12, lineHeight: "18px",
1131
+ whiteSpace: "pre-wrap",
1132
+ border: "1px solid var(--dsw-alias-state-error-primary, #f85149)",
1133
+ color: "var(--dsw-alias-state-error-primary, #f85149)"
1134
+ }
1135
+ }, "dsh-model-organizer 面板渲染失败:" + message);
1136
+ }
1137
+ return this.props.children;
1138
+ }
1139
+ }
1140
+
1141
+ function ProviderOrderPanelGuarded(props) {
1142
+ return h(PanelBoundary, null, h(ProviderOrderPanel, props));
1143
+ }
1144
+
1108
1145
  function ProviderOrderPanel(props) {
1109
1146
  const C = resolveOfficialClasses();
1110
1147
  ensureOwnStyles();
@@ -1164,21 +1201,9 @@ window.__ModuleLoader__.load({
1164
1201
  const idsKey = ids.join("|");
1165
1202
  const dirty = order !== null && order.join("|") !== sourceKey;
1166
1203
 
1167
- /* Mirror the stored order onto the shipped provider cards (CSS `order` only —
1168
- the DOM React rendered is never touched). The cards may be (re)rendered after
1169
- this effect runs, which drops the inline styles, so it is applied again on the
1170
- next frame and once more shortly after. */
1171
- useEffect(function () {
1172
- const list = idsKey.length === 0 ? [] : idsKey.split("|");
1173
- applyCardOrder(display, list);
1174
- const frame = requestAnimationFrame(function () { applyCardOrder(display, list); });
1175
- const timer = setTimeout(function () { applyCardOrder(display, list); }, 400);
1176
- return function () {
1177
- cancelAnimationFrame(frame);
1178
- clearTimeout(timer);
1179
- };
1180
- }, [sourceKey, idsKey]);
1181
-
1204
+ /* The shipped provider cards are left exactly as React renders them. Ordering used
1205
+ to be mirrored onto them with inline `order`; that is gone — this plugin adds a
1206
+ floating panel to the Models page and nothing else. */
1182
1207
  function commit(next) {
1183
1208
  if (next.join("|") === baseIds.join("|")) { setOrder(null); return; }
1184
1209
  writeProviderOrder(next);
@@ -1352,56 +1377,19 @@ window.__ModuleLoader__.load({
1352
1377
  : Object.assign({}, S.floatPanel, { left: panelPos.left, top: panelPos.top, right: "auto", bottom: "auto" });
1353
1378
  /* Original layout (header first, list below) with a TOP-anchored panel: the
1354
1379
  header never moves and collapsing pulls the bottom edge up. */
1355
- return h("div", { ref: panelRef, style: panelStyle },
1356
- head,
1357
- panelOpen ? h("div", { style: S.floatBody }, list) : null
1380
+ /* Portalled to <body>: the footer seat lives deep inside the settings page, so a
1381
+ fixed panel there is trapped in that subtree's stacking context and any other
1382
+ plugin's floating widget paints over it. At body level the z-index is global. */
1383
+ return ReactDOM.createPortal(
1384
+ h("div", { ref: panelRef, style: panelStyle, "data-mo": "panel" },
1385
+ head,
1386
+ panelOpen ? h("div", { style: S.floatBody }, list) : null
1387
+ ),
1388
+ document.body
1358
1389
  );
1359
1390
  }
1360
1391
 
1361
1392
  /* -------------------------------- helpers --------------------------------- */
1362
- /* The Models page renders one <li> per provider inside its own <ul>. The host
1363
- owns that order (the settings document cannot express it — verified), so the
1364
- stored preference is applied as CSS `order` on a flex column: the DOM is left
1365
- exactly as React rendered it, only the visual sequence changes. Cards that do
1366
- not correspond to a settings provider (catalog-only rows) stay pinned first. */
1367
- function applyCardOrder(providers, ids) {
1368
- try {
1369
- const names = [];
1370
- for (const id in providers) {
1371
- const dn = providers[id] ? providers[id].displayName : undefined;
1372
- if (typeof dn === "string" && dn.length > 0) names.push([id, dn]);
1373
- }
1374
- if (names.length === 0) return;
1375
- names.sort(function (a, b) { return b[1].length - a[1].length; });
1376
-
1377
- let list = null;
1378
- let best = 0;
1379
- const lists = document.querySelectorAll("ul");
1380
- for (let i = 0; i < lists.length; i++) {
1381
- const items = lists[i].children;
1382
- let hits = 0;
1383
- for (let j = 0; j < items.length; j++) {
1384
- const text = items[j].textContent || "";
1385
- for (let k = 0; k < names.length; k++) if (text.indexOf(names[k][1]) !== -1) { hits++; break; }
1386
- }
1387
- if (hits >= 2 && hits > best) { best = hits; list = lists[i]; }
1388
- }
1389
- if (list === null) return;
1390
-
1391
- list.style.display = "flex";
1392
- list.style.flexDirection = "column";
1393
- const rank = {};
1394
- for (let i = 0; i < ids.length; i++) rank[ids[i]] = i;
1395
- const items = list.children;
1396
- for (let i = 0; i < items.length; i++) {
1397
- const text = items[i].textContent || "";
1398
- let matched;
1399
- for (let k = 0; k < names.length; k++) if (text.indexOf(names[k][1]) !== -1) { matched = names[k][0]; break; }
1400
- const r = matched === undefined ? undefined : rank[matched];
1401
- items[i].style.order = String(r === undefined ? -1 : r);
1402
- }
1403
- } catch (e) {}
1404
- }
1405
1393
 
1406
1394
  function namespaceView(view, namespace) {
1407
1395
  if (!view || !view.namespaces) return null;
@@ -1450,6 +1438,18 @@ window.__ModuleLoader__.load({
1450
1438
 
1451
1439
  function apply(ctx) {
1452
1440
  try { window.__dshModelOrganizerBuild = BUILD; } catch (e) {}
1441
+ /* A DOM marker proving this bundle actually applied, independent of any seat.
1442
+ Seat registration is silent by design (a throw abdicates the entry), so
1443
+ "is the new code even loaded?" is otherwise unanswerable from the page. */
1444
+ try {
1445
+ if (document.body !== null && document.querySelector('[data-mo="boot"]') === null) {
1446
+ const boot = document.createElement("div");
1447
+ boot.setAttribute("data-mo", "boot");
1448
+ boot.setAttribute("data-mo-build", BUILD);
1449
+ boot.hidden = true;
1450
+ document.body.appendChild(boot);
1451
+ }
1452
+ } catch (e) {}
1453
1453
  ctx.effect(function () { return ctx.locale.register(NS, { zh: zh, en: en }); }, "dsh-model-organizer: dictionaries");
1454
1454
  const t = ctx.locale.bind(NS);
1455
1455
 
@@ -1481,8 +1481,27 @@ window.__ModuleLoader__.load({
1481
1481
  });
1482
1482
  });
1483
1483
 
1484
- ctx.inject(["slots", "settingsScope", "remote", "remote.settings"], function (scope) {
1485
- const mirror = scope.settingsScope.describe();
1484
+ /* The settings mirror moved homes: DSH 0.1.7 exposes it as `configForms`
1485
+ (provided by ui-settings); 0.1.5/0.1.6 exposed the same object as
1486
+ `settingsScope`. A cordis `ctx.inject` whose service never appears simply
1487
+ never runs, so injecting only the old name made this whole seat vanish
1488
+ without a single error once DSH renamed it. Inject both names and mount
1489
+ once — whichever resolves first wins, the other stays dormant. */
1490
+ let orderPanelMounted = false;
1491
+
1492
+ function mountOrderPanel(scope, mirror) {
1493
+ if (orderPanelMounted) return;
1494
+ orderPanelMounted = true;
1495
+ /* Mark that the service resolved and the panel is being mounted — this
1496
+ separates "the seat never rendered" from "the inject never fired". */
1497
+ try {
1498
+ if (document.body !== null && document.querySelector('[data-mo="panel-mounted"]') === null) {
1499
+ const mark = document.createElement("div");
1500
+ mark.setAttribute("data-mo", "panel-mounted");
1501
+ mark.hidden = true;
1502
+ document.body.appendChild(mark);
1503
+ }
1504
+ } catch (e) {}
1486
1505
  mirror.ensure();
1487
1506
  const mutate = function (ops, revision) { return scope.remote.settings.mutate(PI_AI_NS, ops, revision); };
1488
1507
 
@@ -1504,9 +1523,16 @@ window.__ModuleLoader__.load({
1504
1523
  id: "model-organizer-provider-order",
1505
1524
  order: 100,
1506
1525
  inject: face
1507
- }, ProviderOrderPanel);
1526
+ }, ProviderOrderPanelGuarded);
1508
1527
  });
1509
1528
  });
1529
+ }
1530
+
1531
+ ctx.inject(["slots", "configForms", "remote", "remote.settings"], function (scope) {
1532
+ mountOrderPanel(scope, scope.configForms.describe());
1533
+ });
1534
+ ctx.inject(["slots", "settingsScope", "remote", "remote.settings"], function (scope) {
1535
+ mountOrderPanel(scope, scope.settingsScope.describe());
1510
1536
  });
1511
1537
  }
1512
1538
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-model-organizer",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "DSH 模型整理插件:输入框的模型下拉改为「供应商分组 + 模型二级折叠菜单」(含推理等级),并在模型设置页提供一个可拖拽的「供应商与模型顺序」浮窗。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",