@huaweidevtools/aishell-dsh-llm 0.0.2 → 0.0.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 CHANGED
@@ -4,6 +4,10 @@ OpenAI-compatible LLM adapter for DSH. Registers the `aishell` provider route on
4
4
 
5
5
  Besides the live route, the plugin declares `aishell` in the configurable-provider directory (`ctx.llm.registerConfigurableProviders`) — the directory the Models page joins with live routes, so the route shows up as a configurable provider row with its own settings address. The plugin also carries a Config schema; the settings mirror only lists namespaces whose plugin declares one, so without it the Models page would not show the row.
6
6
 
7
+ ## The Models-page row
8
+
9
+ The row itself is read-only: the profile has no fields the host editor knows how to write, and credentials are managed at Settings → AI Shell (by `aishell-credentials`). The browser half therefore does not extend the row with a card — it shadows the row's header at runtime (`src/client/provider-row-shadow.ts`, the same approach `aishell-brand` uses for its digest-protected host residues): a status dot after the row's name (green when AK/SK are stored, warn when they are not, with the readiness copy as its hover title) and the header's Edit button hidden, because there is nothing to edit. A MutationObserver keeps the shadow applied across the page's re-renders; disabling the plugin restores the official row exactly.
10
+
7
11
  ## How it works
8
12
 
9
13
  ```
package/lib/client.js CHANGED
@@ -1,221 +1,202 @@
1
1
  window.__ModuleLoader__.load({ id: "@huaweidevtools/aishell-dsh-llm", factory: (require) => {
2
2
  "use strict";
3
3
  var module = { exports: {} }; var exports = module.exports;
4
- //#region rolldown:runtime
5
- var __create = Object.create;
6
- var __defProp = Object.defineProperty;
7
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
- var __getOwnPropNames = Object.getOwnPropertyNames;
9
- var __getProtoOf = Object.getPrototypeOf;
10
- var __hasOwnProp = Object.prototype.hasOwnProperty;
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
13
- key = keys[i];
14
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
15
- get: ((k) => from[k]).bind(null, key),
16
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
- });
18
- }
19
- return to;
20
- };
21
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
22
- value: mod,
23
- enumerable: true
24
- }) : target, mod));
25
4
 
26
- //#endregion
27
- const __deepseek_ai_dsh_client_store = __toESM(require("@deepseek-ai/dsh-client-store"));
28
- const react = __toESM(require("react"));
29
- const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
30
-
31
- //#region src/client/ProviderCard.tsx
5
+ //#region src/provider-name.ts
32
6
  /**
33
- * Render the aishell family's card extension.
7
+ * The aishell provider's display name, shared by both plugin halves.
8
+ *
9
+ * The Host half registers it on the configurable-provider directory (the
10
+ * Models page renders it as the provider row's name), and the browser half's
11
+ * row shadow finds that row by matching the rendered name against this
12
+ * literal (see `client/provider-row-shadow.ts`). It must stay a single
13
+ * literal string — the shadow compares `textContent` with it.
34
14
  *
35
- * The parent renders this inside the host editor's card (below its fallback
36
- * hint), so the visual style follows the surrounding card rather than opening
37
- * a panel of its own: one readiness line and one catalog list, no buttons —
38
- * the host card's Cancel stays the only control. Readiness folds the
39
- * credential fact into a single conclusion (the question this page asks is
40
- * "can I use this provider", not "what are my credentials"); details and
41
- * management of credentials belong to the AI Shell settings section, which
42
- * the unconfigured hint names.
15
+ * It is also the name `providerInfo()` reports, so the picker and the Models
16
+ * row call the route the same thing: this package's whole surface is one
17
+ * product, and qualifying it by cloud ("AI Shell (Huawei Cloud)") added a
18
+ * distinction no reader needed — there is no second AI Shell to tell it
19
+ * apart from.
43
20
  */
21
+ const PROVIDER_DISPLAY_NAME = "AI Shell";
22
+
23
+ //#endregion
24
+ //#region src/client/provider-row-shadow.ts
25
+ /** Marker attribute the dot carries (exposed for the spec). */
26
+ const ROW_DOT_MARKER = "data-aishell-provider-dot";
27
+ /** Marker attribute a hidden Edit button carries (exposed for the spec). */
28
+ const ROW_EDIT_HIDDEN = "data-aishell-edit-hidden";
29
+ /** The dot's own CSS: the host credential dot's geometry and palette. */
30
+ const ROW_DOT_CSS = [
31
+ `[class*="rowIdentity"] [${ROW_DOT_MARKER}] {`,
32
+ " box-sizing: border-box;",
33
+ " border-radius: 50%;",
34
+ " flex: none;",
35
+ " width: 8px;",
36
+ " height: 8px;",
37
+ " display: inline-block;",
38
+ "}",
39
+ `[class*="rowIdentity"] [${ROW_DOT_MARKER}="ok"] {`,
40
+ " background: var(--dsw-alias-state-success-primary);",
41
+ "}",
42
+ `[class*="rowIdentity"] [${ROW_DOT_MARKER}="warn"] {`,
43
+ " background: var(--dsw-alias-state-warn-label);",
44
+ "}",
45
+ `[${ROW_EDIT_HIDDEN}] {`,
46
+ " display: none !important;",
47
+ "}"
48
+ ].join("\n");
49
+ /** Title copy for the dot, by readiness. The warn state keeps the
50
+ * actionable half — where to save AK/SK — because that is the one thing a
51
+ * fresh user cannot guess; the ready state needs nothing beyond itself. */
52
+ const DOT_TITLE = {
53
+ ok: "已就绪",
54
+ warn: "未就绪 — 凭据未配置(在 设置 → AI Shell 中保存 AK/SK)"
55
+ };
56
+ /** Every Edit button an aishell row renders (there is ordinarily one; a
57
+ * re-render that leaves a second copy gets both, so none stays visible). */
58
+ function findEditButtons(row) {
59
+ const actions = row.querySelector("[class*=\"rowActions\"]");
60
+ if (actions === null) return [];
61
+ const found = [];
62
+ for (const button of actions.querySelectorAll("button")) {
63
+ const label = button.getAttribute("aria-label") ?? "";
64
+ if (label.includes(PROVIDER_DISPLAY_NAME) || button.textContent === "编辑") found.push(button);
65
+ }
66
+ return found;
67
+ }
44
68
  /**
45
- * What an empty catalog means, and therefore what the card says about it.
46
- *
47
- * An empty model list has two unrelated causes that look identical on the
48
- * wire: the gateway returned nothing, or the request was never signed because
49
- * no credentials are stored. Only the second one is actionable, and it is the
50
- * state every new user starts in — so it has to name the place to fix it.
69
+ * Whether `node` is an Element (`querySelectorAll` roots can be non-element
70
+ * nodes like the Document, where the row check below simply answers false).
71
+ */
72
+ function isElement(node) {
73
+ return node.nodeType === Node.ELEMENT_NODE;
74
+ }
75
+ /**
76
+ * Whether `node` is a provider row whose rendered name is ours.
51
77
  *
52
- * Extracted from the render so the branch is testable without a DOM: the
53
- * repo's client-side tests cover pure logic, not components.
78
+ * Duck-typed on the attribute surface rather than `instanceof Element` so
79
+ * the shadow works identically in the browser and under the specs' DOM
80
+ * stub, which provides the attribute methods without the constructor.
81
+ */
82
+ function isProviderRow(node) {
83
+ if (node.nodeType !== Node.ELEMENT_NODE) return false;
84
+ const element = node;
85
+ const name = element.querySelector("[class*=\"rowName\"]");
86
+ return name !== null && name.textContent === PROVIDER_DISPLAY_NAME;
87
+ }
88
+ /**
89
+ * Apply the shadow to one row: add the dot (or repaint an existing one) and
90
+ * hide the Edit button. Idempotent — a second pass rewrites the dot's
91
+ * readiness instead of stacking another one.
54
92
  *
55
- * @param catalog - the route body, or `undefined` while it is still loading.
56
- * @returns the text to show, which the caller renders as-is.
93
+ * `target` is the owning document, passed in because the stub DOM the specs
94
+ * run on keeps no `ownerDocument` link.
57
95
  */
58
- function catalogStatusText(catalog) {
59
- if (catalog === void 0) return "获取中…";
60
- if (catalog.models.length > 0) return "";
61
- return catalog.credentialsPresent ? "网关未返回任何模型" : "需要凭据才能获取(凭据在 设置 → AI Shell 中配置)";
96
+ function shadowRow(row, ready, target) {
97
+ const identity = row.querySelector("[class*=\"rowIdentity\"]");
98
+ if (identity === null) return;
99
+ const state = ready ? "ok" : "warn";
100
+ const existing = identity.querySelector(`[${ROW_DOT_MARKER}]`);
101
+ const dot = existing ?? target.createElement("span");
102
+ dot.setAttribute(ROW_DOT_MARKER, state);
103
+ dot.setAttribute("title", DOT_TITLE[state]);
104
+ dot.setAttribute("aria-label", DOT_TITLE[state]);
105
+ if (existing === null) {
106
+ dot.setAttribute("role", "img");
107
+ identity.appendChild(dot);
108
+ }
109
+ for (const edit of findEditButtons(row)) edit.setAttribute(ROW_EDIT_HIDDEN, "");
62
110
  }
63
- function AishellProviderCard(props) {
64
- const status = props.useCredentials((snapshot) => snapshot);
65
- const [catalog, setCatalog] = (0, react.useState)(void 0);
66
- const [catalogError, setCatalogError] = (0, react.useState)(void 0);
67
- (0, react.useEffect)(() => {
68
- let cancelled = false;
69
- fetch(MODELS_ROUTE).then(async (response) => {
70
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
71
- return response.json();
72
- }).then((body) => {
73
- if (!cancelled) {
74
- setCatalog(body);
75
- setCatalogError(void 0);
76
- }
77
- }).catch((error) => {
78
- if (!cancelled) setCatalogError(error instanceof Error ? error.message : String(error));
79
- });
80
- return () => {
81
- cancelled = true;
82
- };
83
- }, []);
84
- const configured = status.credentialsStored;
85
- const readinessStyle = {
86
- display: "flex",
87
- alignItems: "center",
88
- gap: 8,
89
- fontSize: 12,
90
- lineHeight: "18px",
91
- fontWeight: 500,
92
- color: "var(--dsw-alias-label-secondary)"
93
- };
94
- const readinessTextStyle = {
95
- fontWeight: 400,
96
- color: configured ? "var(--dsw-alias-label-primary)" : "var(--dsw-alias-label-tertiary)"
97
- };
98
- const rowStyle = {
99
- display: "flex",
100
- flexDirection: "column",
101
- gap: 2
102
- };
103
- const catalogLabelStyle = {
104
- fontSize: 12,
105
- lineHeight: "18px",
106
- fontWeight: 500,
107
- color: "var(--dsw-alias-label-secondary)"
108
- };
109
- const valueStyle = {
110
- fontSize: 12,
111
- lineHeight: "18px",
112
- color: "var(--dsw-alias-label-tertiary)"
113
- };
114
- const sectionStyle = {
115
- display: "flex",
116
- flexDirection: "column",
117
- gap: 10,
118
- borderTop: "0.5px solid var(--dsw-alias-border-l4)",
119
- marginTop: 12,
120
- paddingTop: 12
121
- };
122
- const modelStyle = {
123
- display: "flex",
124
- alignItems: "center",
125
- gap: 8,
126
- fontSize: 13,
127
- lineHeight: "20px",
128
- color: "var(--dsw-alias-label-primary)"
129
- };
130
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
131
- style: sectionStyle,
132
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
133
- style: readinessStyle,
134
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
135
- style: {
136
- width: 6,
137
- height: 6,
138
- borderRadius: "50%",
139
- flex: "none",
140
- background: configured ? "var(--dsw-alias-state-success-primary)" : "var(--dsw-alias-state-warn-label)"
141
- },
142
- "aria-hidden": "true"
143
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
144
- style: readinessTextStyle,
145
- children: configured ? "已就绪 — 凭据已验证,可直接对话" : "未就绪 — 凭据未配置(在 设置 → AI Shell 中保存 AK/SK)"
146
- })]
147
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
148
- style: rowStyle,
149
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
150
- style: catalogLabelStyle,
151
- children: "模型目录(来自 aishell 网关,只读)"
152
- }), catalogError !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
153
- style: valueStyle,
154
- children: ["目录获取失败:", catalogError]
155
- }) : catalog === void 0 || catalog.models.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
156
- style: valueStyle,
157
- children: catalogStatusText(catalog)
158
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
159
- style: {
160
- margin: 0,
161
- padding: 0,
162
- listStyle: "none",
163
- display: "flex",
164
- flexDirection: "column",
165
- gap: 4
166
- },
167
- children: catalog.models.map((model) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
168
- style: modelStyle,
169
- children: [
170
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
171
- "aria-hidden": "true",
172
- children: "·"
173
- }),
174
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.id }),
175
- model.inputModalities.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
176
- style: {
177
- fontSize: 11,
178
- color: "var(--dsw-alias-label-quaternary)"
179
- },
180
- children: [model.inputModalities.join(" / "), " 输入"]
181
- }) : null
182
- ]
183
- }, model.id))
184
- })]
185
- })]
111
+ /** Undo one row: remove the dot, restore the Edit button. */
112
+ function unshadowRow(row) {
113
+ row.querySelectorAll(`[${ROW_DOT_MARKER}]`).forEach((dot) => {
114
+ dot.remove();
115
+ });
116
+ row.querySelectorAll(`[${ROW_EDIT_HIDDEN}]`).forEach((button) => {
117
+ button.removeAttribute(ROW_EDIT_HIDDEN);
186
118
  });
187
119
  }
188
120
  /**
189
- * Host route, written document-relative like the credential routes the
190
- * sibling plugin uses: the served index carries `<base href="./">`, so the
191
- * request lands on the same origin that serves the page — which carries the
192
- * browser-session cookie the route's authentication fence requires.
121
+ * Install the provider-row shadow on a document.
122
+ *
123
+ * `ready` is the credential fact as the browser half derives it from the
124
+ * settings namespace `aishell-credentials` owns (see `client/index.ts`);
125
+ * the install does not read the store itself, so a later credential save is
126
+ * a reinstall with the new fact, not a re-derivation buried in here. Sweeps
127
+ * the page once, then observes the body so re-rendered rows are covered too.
128
+ * Returns the disposer that stops the observer and restores every row.
193
129
  */
194
- const MODELS_ROUTE = "api/aishell/models";
130
+ function installProviderRowShadow(target, ready) {
131
+ if (typeof target.querySelector !== "function" || typeof MutationObserver !== "function") return () => {};
132
+ /** Run the shadow over every provider row at or below `root`. */
133
+ const sweep = (root) => {
134
+ if (isElement(root) && isProviderRow(root)) shadowRow(root, ready, target);
135
+ for (const row of root.querySelectorAll("[class*=\"rowCard\"], [class*=\"setupCard\"]")) if (isProviderRow(row)) shadowRow(row, ready, target);
136
+ };
137
+ const sweepIfRow = (node) => {
138
+ if (isProviderRow(node)) {
139
+ shadowRow(node, ready, target);
140
+ return;
141
+ }
142
+ for (let parent = node.parentElement; parent !== null; parent = parent.parentElement) sweep(parent);
143
+ };
144
+ const style = target.createElement("style");
145
+ style.setAttribute(ROW_DOT_MARKER + "-css", "");
146
+ style.textContent = ROW_DOT_CSS;
147
+ target.head.appendChild(style);
148
+ sweep(target.body);
149
+ const observer = new MutationObserver((mutations) => {
150
+ for (const mutation of mutations) for (const node of mutation.addedNodes) if (node.nodeType === Node.ELEMENT_NODE) sweepIfRow(node);
151
+ });
152
+ observer.observe(target.body, {
153
+ childList: true,
154
+ subtree: true
155
+ });
156
+ return () => {
157
+ observer.disconnect();
158
+ style.remove();
159
+ for (const row of target.body.querySelectorAll("[class*=\"rowCard\"], [class*=\"setupCard\"]")) if (isProviderRow(row)) unshadowRow(row);
160
+ };
161
+ }
195
162
 
196
163
  //#endregion
197
164
  //#region src/client/index.ts
198
- /** The settings namespace aishell-credentials owns (the card reads its status). */
165
+ /** The settings namespace aishell-credentials owns (the shadow reads its status). */
199
166
  const AISHELL_CREDENTIALS_NS = "aishell-client-credentials";
200
167
  /** Required services. */
201
- const inject = ["slots", "configForms"];
168
+ const inject = ["configForms"];
202
169
  function apply(ctx) {
170
+ if (typeof document === "undefined") return;
203
171
  const form = ctx.configForms.get(AISHELL_CREDENTIALS_NS);
204
- const status = (0, __deepseek_ai_dsh_client_store.createSnapshotStore)({ credentialsStored: false });
172
+ let readiness = form.getSnapshot().value?.credentialsStored === true;
173
+ let disposeShadow;
174
+ const sync = () => {
175
+ disposeShadow?.();
176
+ disposeShadow = installProviderRowShadow(document, readiness);
177
+ };
205
178
  const derive = () => {
206
- status.set({ credentialsStored: form.getSnapshot().value?.credentialsStored === true });
179
+ const next = form.getSnapshot().value?.credentialsStored === true;
180
+ if (next !== readiness) {
181
+ readiness = next;
182
+ sync();
183
+ }
207
184
  };
208
- ctx.effect(() => form.subscribe(derive), "aishell-llm: credential presence for the models card");
209
- derive();
210
- ctx.effect(() => ctx.configForms.whileServed([AISHELL_CREDENTIALS_NS], () => ctx.slots.inject("settings.models.provider-card", () => ctx.slots.register({
211
- name: "settings.models.provider-card",
212
- key: "aishell-llm",
213
- inject: () => ({ hooks: { credentials: status } })
214
- }, AishellProviderCard))), "aishell-llm: models page provider card");
185
+ ctx.effect(() => ctx.configForms.whileServed([AISHELL_CREDENTIALS_NS], () => {
186
+ sync();
187
+ return () => {
188
+ disposeShadow?.();
189
+ disposeShadow = void 0;
190
+ };
191
+ }), "aishell-llm: models page provider row shadow");
192
+ ctx.effect(() => form.subscribe(derive), "aishell-llm: credential presence for the models row");
215
193
  }
216
194
 
217
195
  //#endregion
196
+ exports.ROW_DOT_MARKER = ROW_DOT_MARKER
197
+ exports.ROW_EDIT_HIDDEN = ROW_EDIT_HIDDEN
218
198
  exports.apply = apply
219
199
  exports.inject = inject
200
+ exports.installProviderRowShadow = installProviderRowShadow
220
201
  return module.exports; } });
221
202
  //# sourceMappingURL=client.js.map
package/lib/index.js CHANGED
@@ -1404,6 +1404,25 @@ async function* translate(payloads) {
1404
1404
  };
1405
1405
  }
1406
1406
 
1407
+ //#endregion
1408
+ //#region src/provider-name.ts
1409
+ /**
1410
+ * The aishell provider's display name, shared by both plugin halves.
1411
+ *
1412
+ * The Host half registers it on the configurable-provider directory (the
1413
+ * Models page renders it as the provider row's name), and the browser half's
1414
+ * row shadow finds that row by matching the rendered name against this
1415
+ * literal (see `client/provider-row-shadow.ts`). It must stay a single
1416
+ * literal string — the shadow compares `textContent` with it.
1417
+ *
1418
+ * It is also the name `providerInfo()` reports, so the picker and the Models
1419
+ * row call the route the same thing: this package's whole surface is one
1420
+ * product, and qualifying it by cloud ("AI Shell (Huawei Cloud)") added a
1421
+ * distinction no reader needed — there is no second AI Shell to tell it
1422
+ * apart from.
1423
+ */
1424
+ const PROVIDER_DISPLAY_NAME = "AI Shell";
1425
+
1407
1426
  //#endregion
1408
1427
  //#region src/adapter.ts
1409
1428
  /** Credential store account names for Huawei Cloud credentials. */
@@ -1457,7 +1476,7 @@ var GatewayAdapter = class GatewayAdapter extends LlmAdapter {
1457
1476
  providerInfo(provider) {
1458
1477
  return {
1459
1478
  id: provider,
1460
- name: "AI Shell"
1479
+ name: PROVIDER_DISPLAY_NAME
1461
1480
  };
1462
1481
  }
1463
1482
  /**
@@ -1487,7 +1506,7 @@ var GatewayAdapter = class GatewayAdapter extends LlmAdapter {
1487
1506
  return wireModels.map((m) => ({
1488
1507
  provider,
1489
1508
  id: m.id,
1490
- name: m.id,
1509
+ name: m.name ?? m.id,
1491
1510
  ...mapModalities(m.modalities)
1492
1511
  }));
1493
1512
  }
@@ -1497,7 +1516,7 @@ var GatewayAdapter = class GatewayAdapter extends LlmAdapter {
1497
1516
  const base = {
1498
1517
  provider,
1499
1518
  id: model,
1500
- name: model,
1519
+ name: wire?.name ?? model,
1501
1520
  ...mapModalities(wire?.modalities)
1502
1521
  };
1503
1522
  if (wire === void 0) return base;
@@ -1669,35 +1688,10 @@ const Config = Schema.object({
1669
1688
  requestImageMaxBytes: Schema.number().step(1).min(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES).volatile(),
1670
1689
  maxRequestImageBytes: Schema.number().step(1).min(1).default(DEFAULT_MAX_REQUEST_IMAGE_BYTES).volatile()
1671
1690
  });
1672
- /** Route the Models-page card reads the live gateway catalog from. */
1673
- const MODELS_PATH = "/api/aishell/models";
1674
- /**
1675
- * Build the route body from the adapter's own catalog cache, so the card and
1676
- * the pickers cannot disagree about what the gateway carries.
1677
- *
1678
- * `credentialsPresent` is read from the adapter rather than assumed: without
1679
- * credentials `listModels` answers `[]` for a reason that is not "the gateway
1680
- * has no models", and the card's empty state is the one place a fresh user
1681
- * learns which of the two it is.
1682
- *
1683
- * Returned as a plain function of the adapter so a test can pin the mapping
1684
- * without a live context; `apply` wires it to the fetch route.
1685
- */
1686
- function createModelsRoute(adapter) {
1687
- return () => adapter.listModels(PROVIDER).then((models) => Response.json({
1688
- credentialsPresent: adapter.hasCredentials(),
1689
- models: models.map((m) => ({
1690
- id: m.id,
1691
- inputModalities: [...m.inputModalities ?? []]
1692
- }))
1693
- }, { headers: { "cache-control": "no-store" } }));
1694
- }
1695
1691
  const name = "aishell-llm";
1696
- const inject = ["llm", "connection"];
1692
+ const inject = ["llm"];
1697
1693
  /** The provider route this plugin owns. */
1698
1694
  const PROVIDER = "aishell";
1699
- /** Human-readable provider name the Models page and pickers show. */
1700
- const PROVIDER_DISPLAY_NAME = "AI Shell (Huawei Cloud)";
1701
1695
  function apply(ctx, config) {
1702
1696
  const keyringService = config.keyringService.get();
1703
1697
  const store = createCredentialsStore(keyringService === void 0 || keyringService === "" ? {} : { service: keyringService });
@@ -1719,14 +1713,8 @@ function apply(ctx, config) {
1719
1713
  settingsNs: ctx.fiber.entry?.options.id ?? name,
1720
1714
  settingsPath: []
1721
1715
  }]);
1722
- ctx.effect(() => ctx.connection.fetch.register({
1723
- path: MODELS_PATH,
1724
- methods: ["GET"],
1725
- requestBody: "buffered",
1726
- fetch: createModelsRoute(adapter)
1727
- }), "aishell-llm: models catalog route");
1728
1716
  }
1729
1717
 
1730
1718
  //#endregion
1731
- export { Config, PROVIDER, PROVIDER_DISPLAY_NAME, apply, createModelsRoute, inject, name };
1719
+ export { Config, PROVIDER, PROVIDER_DISPLAY_NAME, apply, inject, name };
1732
1720
  //# sourceMappingURL=index.js.map
@@ -1,20 +1,25 @@
1
1
  /**
2
- * aishell LLM browser half — fills the Models page's provider-card seat.
2
+ * aishell LLM browser half — shadows the Models page's provider row.
3
3
  *
4
- * The Models page's Edit card for an adapter family it does not know renders
5
- * the host's fallback hint and nothing editable. This plugin owns the family,
6
- * so it fills the card itself through the `settings.models.provider-card`
7
- * seat (keyed by the `aishell-llm` settings namespace, which the Host half's
8
- * directory entry names): a read-only credential line (managed by
9
- * `aishell-credentials`, shown with the verified account name) and the live
10
- * gateway model catalog, read from the Host half's `api/aishell/models` route
11
- * — the same cache `listModels` serves, so the card and the pickers cannot
12
- * disagree about what the gateway carries.
4
+ * The page's row for the aishell family is read-only: its profile has no
5
+ * fields the host editor knows how to write, and its credentials live in
6
+ * `aishell-credentials`' own settings section. What the row should tell the
7
+ * user is the one fact the old card extension distilled — ready or not — so
8
+ * the browser half paints it as a status dot beside the row's name and hides
9
+ * the header's Edit button (see `client/provider-row-shadow.ts`).
10
+ *
11
+ * The readiness fact is read from the `aishell-client-credentials` settings
12
+ * namespace (`aishell-credentials` owns it; the shadow never names accounts
13
+ * or manages anything). The shadow follows that namespace through
14
+ * `whileServed`, so a deployment without that plugin (this LLM package
15
+ * installed standalone) shows no trace of the dot either — the fact is
16
+ * unknown, not false.
13
17
  *
14
18
  * @module @huaweidevtools/aishell-dsh-llm/client
15
19
  */
16
20
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
17
- export type { AishellProviderCard, AishellProviderCardProps } from './ProviderCard.tsx';
21
+ export type { ProviderRowShadowDisposer, } from './provider-row-shadow.ts';
22
+ export { installProviderRowShadow, ROW_DOT_MARKER, ROW_EDIT_HIDDEN, } from './provider-row-shadow.ts';
18
23
  /** Required services. */
19
24
  export declare const inject: string[];
20
25
  export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Runtime shadow of the aishell provider's Models-page row.
3
+ *
4
+ * The Models page renders each configured provider as a row whose header
5
+ * (`rowHead`) shows the display name followed by an Edit button the aishell
6
+ * family has no use for: the profile has no fields the host editor knows how
7
+ * to write (the old card extension said so itself), and the row's directory
8
+ * entry names no api-key credential, so the host header also paints no status
9
+ * dot. What the user actually needs from this row is the one fact the old
10
+ * card distilled — ready or not — and that reads better beside the name than
11
+ * in a card below the header.
12
+ *
13
+ * The header lives in ui-settings-models, digest-protected with no slot for
14
+ * the identity half (the `settings.models.provider-card` seat extends the
15
+ * area BELOW the header, which is exactly the part this family no longer
16
+ * wants). The shadow therefore works the way aishell-brand's host residues
17
+ * do: find the row, patch the DOM. The row is located by its rendered name
18
+ * (the directory entry's displayName, `PROVIDER_DISPLAY_NAME` — a literal
19
+ * both halves share); the class names beside it are CSS-module hashes, so
20
+ * they are matched by the same `[class*="…"]` substring selectors the hero
21
+ * patch already relies on.
22
+ *
23
+ * What the shadow does to a found row:
24
+ *
25
+ * 1. a status dot after the name — the host credential dot's own 8px
26
+ * geometry and success/warn colors, green when AK/SK are stored, warn
27
+ * when they are not, with the readiness copy as its title (the row
28
+ * keeps saying what the old card said, just in one word of hover);
29
+ * 2. the row's Edit button is hidden — the row is read-only, and the only
30
+ * control the header keeps is whatever the host itself decides (Remove,
31
+ * for user-added rows; this family's row is not removable). The hide is
32
+ * a marker-keyed `display: none !important` rule, not the `hidden`
33
+ * attribute: the button's own class sets `display: inline-flex`, and an
34
+ * author declaration beats the UA sheet's `[hidden] { display: none }`,
35
+ * so the attribute alone leaves the button visible.
36
+ *
37
+ * The Models page re-renders the rows (credential saves, pushed
38
+ * invalidations), so a MutationObserver keeps the shadow applied for as long
39
+ * as the page exists. Shadowing is a default, not a force: install() and the
40
+ * disposer it returns are symmetric, so disabling the plugin restores the
41
+ * official row exactly — dot removed, Edit button back.
42
+ *
43
+ * @module @huaweidevtools/aishell-dsh-llm/client/provider-row-shadow
44
+ */
45
+ /**
46
+ * The disposer stops the observer, removes every dot the install added, and
47
+ * restores every hidden Edit button; a no-op install returns this.
48
+ */
49
+ export type ProviderRowShadowDisposer = () => void;
50
+ /** Marker attribute the dot carries (exposed for the spec). */
51
+ export declare const ROW_DOT_MARKER = "data-aishell-provider-dot";
52
+ /** Marker attribute a hidden Edit button carries (exposed for the spec). */
53
+ export declare const ROW_EDIT_HIDDEN = "data-aishell-edit-hidden";
54
+ /** The dot's own CSS: the host credential dot's geometry and palette. */
55
+ export declare const ROW_DOT_CSS: string;
56
+ /**
57
+ * Install the provider-row shadow on a document.
58
+ *
59
+ * `ready` is the credential fact as the browser half derives it from the
60
+ * settings namespace `aishell-credentials` owns (see `client/index.ts`);
61
+ * the install does not read the store itself, so a later credential save is
62
+ * a reinstall with the new fact, not a re-derivation buried in here. Sweeps
63
+ * the page once, then observes the body so re-rendered rows are covered too.
64
+ * Returns the disposer that stops the observer and restores every row.
65
+ */
66
+ export declare function installProviderRowShadow(target: Document, ready: boolean): ProviderRowShadowDisposer;
67
+ //# sourceMappingURL=provider-row-shadow.d.ts.map
@@ -14,12 +14,14 @@
14
14
  * directory (`registerConfigurableProviders`), which is what the Models page
15
15
  * joins with live routes — and it carries a Config schema, without which the
16
16
  * settings mirror (and therefore the Models row) never lists this namespace.
17
+ * The row itself carries no card: the browser half replaces the page's
18
+ * Edit-card extension with a status dot beside the row's name (see
19
+ * `client/provider-row-shadow.ts`).
17
20
  *
18
21
  * @module @huaweidevtools/aishell-dsh-llm
19
22
  */
20
23
  import type { Context, Volatile } from '@deepseek-ai/cordis';
21
24
  import z from '@deepseek-ai/schemastery';
22
- import { GatewayAdapter } from './adapter.ts';
23
25
  export type { GatewayAdapter, GatewayAdapterOptions } from './adapter.ts';
24
26
  /** Plugin config. */
25
27
  export interface Config {
@@ -56,38 +58,10 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
56
58
  requestImageMaxBytes: z<number, number, "volatile-defined">;
57
59
  maxRequestImageBytes: z<number, number, "volatile-defined">;
58
60
  }>>, "plain">;
59
- /**
60
- * The catalog the route answers with: the gateway's `/models` entries, mapped
61
- * to the display shape the browser card renders, plus the credential state
62
- * that decides which of the two postures the card shows. Detached plain data —
63
- * no secret rides on it, ever (presence flags only).
64
- */
65
- export interface ModelsRouteBody {
66
- /** Whether a usable AK/SK pair is stored — the fact that separates "empty catalog" from "no credentials". */
67
- readonly credentialsPresent: boolean;
68
- readonly models: ReadonlyArray<{
69
- readonly id: string;
70
- readonly inputModalities: readonly string[];
71
- }>;
72
- }
73
- /**
74
- * Build the route body from the adapter's own catalog cache, so the card and
75
- * the pickers cannot disagree about what the gateway carries.
76
- *
77
- * `credentialsPresent` is read from the adapter rather than assumed: without
78
- * credentials `listModels` answers `[]` for a reason that is not "the gateway
79
- * has no models", and the card's empty state is the one place a fresh user
80
- * learns which of the two it is.
81
- *
82
- * Returned as a plain function of the adapter so a test can pin the mapping
83
- * without a live context; `apply` wires it to the fetch route.
84
- */
85
- export declare function createModelsRoute(adapter: GatewayAdapter): () => Promise<Response>;
86
61
  export declare const name = "aishell-llm";
87
62
  export declare const inject: string[];
88
63
  /** The provider route this plugin owns. */
89
64
  export declare const PROVIDER = "aishell";
90
- /** Human-readable provider name the Models page and pickers show. */
91
- export declare const PROVIDER_DISPLAY_NAME = "AI Shell (Huawei Cloud)";
65
+ export { PROVIDER_DISPLAY_NAME } from './provider-name.ts';
92
66
  export declare function apply(ctx: Context, config: Config): void;
93
67
  //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The aishell provider's display name, shared by both plugin halves.
3
+ *
4
+ * The Host half registers it on the configurable-provider directory (the
5
+ * Models page renders it as the provider row's name), and the browser half's
6
+ * row shadow finds that row by matching the rendered name against this
7
+ * literal (see `client/provider-row-shadow.ts`). It must stay a single
8
+ * literal string — the shadow compares `textContent` with it.
9
+ *
10
+ * It is also the name `providerInfo()` reports, so the picker and the Models
11
+ * row call the route the same thing: this package's whole surface is one
12
+ * product, and qualifying it by cloud ("AI Shell (Huawei Cloud)") added a
13
+ * distinction no reader needed — there is no second AI Shell to tell it
14
+ * apart from.
15
+ */
16
+ export declare const PROVIDER_DISPLAY_NAME = "AI Shell";
17
+ //# sourceMappingURL=provider-name.d.ts.map
@@ -86,6 +86,17 @@ export interface WireChunk {
86
86
  /** One model entry from the GET /models response (OpenAI-compatible). */
87
87
  export interface WireModel {
88
88
  id: string;
89
+ /**
90
+ * Display name, when the gateway sends one. Outside OpenAI's own model
91
+ * object (which carries `id` alone), so it is an extension like the
92
+ * metadata below, and the gateway omits it for a model that declares none.
93
+ *
94
+ * This is the field that lets an id stay a durable routing key while the
95
+ * label a person reads stays free to change — the split every official
96
+ * adapter's `name ?? id` expresses. Without it the two roles collapse onto
97
+ * the id, and a reserved name like `trial` has to read well in a picker.
98
+ */
99
+ name?: string;
89
100
  object?: string;
90
101
  owned_by?: string;
91
102
  created?: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@huaweidevtools/aishell-dsh-llm",
3
3
  "description": "aishell LLM gateway adapter for DSH — OpenAI-compatible, Huawei Cloud AK/SK auth via credential store",
4
- "version": "0.0.2",
4
+ "version": "0.0.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -33,13 +33,12 @@
33
33
  "license": "Apache-2.0",
34
34
  "dependencies": {
35
35
  "eventsource-parser": "^3.0.0",
36
- "@huaweidevtools/aishell-dsh-credentials-store": "0.0.2"
36
+ "@huaweidevtools/aishell-dsh-credentials-store": "0.0.3"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.4",
40
40
  "@deepseek-ai/dsh-attachment": ">=0.2.0-0 <0.3.0-0",
41
41
  "@deepseek-ai/dsh-brand": ">=0.2.0-0 <0.3.0-0",
42
- "@deepseek-ai/dsh-client-connection": ">=0.2.0-0 <0.3.0-0",
43
42
  "@deepseek-ai/dsh-fs": ">=0.2.0-0 <0.3.0-0",
44
43
  "@deepseek-ai/dsh-llm": ">=0.2.0-0 <0.3.0-0"
45
44
  },
@@ -48,24 +47,15 @@
48
47
  "@deepseek-ai/cordis-plugin-loader": "^1.0.5",
49
48
  "@deepseek-ai/dsh-attachment": "0.2.0-rc.1",
50
49
  "@deepseek-ai/dsh-brand": ">=0.2.0-0 <0.3.0-0",
51
- "@deepseek-ai/dsh-client-connection": "0.2.0-rc.1",
52
- "@deepseek-ai/dsh-client-store": "0.2.0-rc.1",
53
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.0-rc.1",
54
50
  "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.1",
55
- "@deepseek-ai/dsh-client-ui-settings-models": "0.2.0-rc.1",
56
- "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.1",
57
51
  "@deepseek-ai/dsh-fs": "0.2.0-rc.1",
58
52
  "@deepseek-ai/dsh-llm": ">=0.2.0-0 <0.3.0-0",
59
- "@deepseek-ai/schemastery": "3.18.4",
60
- "@types/react": "~18.3.1",
61
- "react": "^18.2.0"
53
+ "@deepseek-ai/schemastery": "3.18.4"
62
54
  },
63
55
  "dsh": {
64
56
  "client": {
65
57
  "inject": [
66
- "@deepseek-ai/dsh-client-ui-settings",
67
- "@deepseek-ai/dsh-client-ui-settings-models",
68
- "@deepseek-ai/dsh-api-remotes"
58
+ "@deepseek-ai/dsh-client-ui-settings"
69
59
  ],
70
60
  "platform": "web"
71
61
  },
@@ -1,77 +0,0 @@
1
- /**
2
- * The aishell row's Edit-card extension: a one-line readiness status + the
3
- * live gateway model catalog.
4
- *
5
- * The Models page's own editor for an adapter family it does not know renders
6
- * the host's fallback hint ("other fields live in cordis.patch.yml") with a
7
- * disabled Apply — nothing to edit, because the aishell profile has no fields
8
- * the host editor knows how to write. What the user actually needs from this
9
- * card is the other direction: to READ the family's state.
10
- *
11
- * Scope discipline: this page answers "can I use this provider, and what does
12
- * it carry" — one readiness line whose credential fact comes from
13
- * `aishell-credentials`' settings namespace (managed at Settings → AI Shell;
14
- * the card never names accounts or manages anything). The model catalog comes
15
- * from the gateway through the Host half's `api/aishell/models` route and is
16
- * likewise read-only — the gateway owns it.
17
- *
18
- * @module @huaweidevtools/aishell-dsh-llm/client/ProviderCard
19
- */
20
- import { type ReactNode } from 'react';
21
- import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
22
- import type { InjectFace, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
23
- /** The route body the Host half answers (see its ModelsRouteBody). */
24
- interface ModelsRouteBody {
25
- readonly credentialsPresent: boolean;
26
- readonly models: ReadonlyArray<{
27
- readonly id: string;
28
- readonly inputModalities: readonly string[];
29
- }>;
30
- }
31
- /**
32
- * Credential presence, read from the settings namespace `aishell-credentials`
33
- * owns. The card only needs the presence flag for its readiness line; the
34
- * account name stays in the credentials section that owns it.
35
- */
36
- export interface CredentialSnapshot {
37
- readonly credentialsStored: boolean;
38
- }
39
- /** Full component props: the seat's owner share + the inject face. */
40
- export type AishellProviderCardProps = PropsRuntime<'settings.models.provider-card'> & InjectFace<AishellProviderCardInjected>;
41
- /** Injected hooks bound in the client half's apply(). */
42
- export interface AishellProviderCardInjected {
43
- hooks: {
44
- /** Credential presence from the aishell-client-credentials namespace. */
45
- credentials: SnapshotStore<CredentialSnapshot>;
46
- };
47
- }
48
- /**
49
- * Render the aishell family's card extension.
50
- *
51
- * The parent renders this inside the host editor's card (below its fallback
52
- * hint), so the visual style follows the surrounding card rather than opening
53
- * a panel of its own: one readiness line and one catalog list, no buttons —
54
- * the host card's Cancel stays the only control. Readiness folds the
55
- * credential fact into a single conclusion (the question this page asks is
56
- * "can I use this provider", not "what are my credentials"); details and
57
- * management of credentials belong to the AI Shell settings section, which
58
- * the unconfigured hint names.
59
- */
60
- /**
61
- * What an empty catalog means, and therefore what the card says about it.
62
- *
63
- * An empty model list has two unrelated causes that look identical on the
64
- * wire: the gateway returned nothing, or the request was never signed because
65
- * no credentials are stored. Only the second one is actionable, and it is the
66
- * state every new user starts in — so it has to name the place to fix it.
67
- *
68
- * Extracted from the render so the branch is testable without a DOM: the
69
- * repo's client-side tests cover pure logic, not components.
70
- *
71
- * @param catalog - the route body, or `undefined` while it is still loading.
72
- * @returns the text to show, which the caller renders as-is.
73
- */
74
- export declare function catalogStatusText(catalog: ModelsRouteBody | undefined): string;
75
- export declare function AishellProviderCard(props: AishellProviderCardProps): ReactNode;
76
- export {};
77
- //# sourceMappingURL=ProviderCard.d.ts.map