@huaweidevtools/aishell-dsh-llm 0.0.2 → 0.0.4

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 entered through the form the `aishell-credentials` chips open (the account chip beside the chat input, or the chip on a New Session page). 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; the shadow is re-installed on a locale switch so the hover copy follows the active language; disabling the plugin restores the official row exactly.
10
+
7
11
  ## How it works
8
12
 
9
13
  ```
@@ -76,7 +80,7 @@ The three image fields share `dsh-llm-pi-ai`'s defaults: the gateway publishes n
76
80
 
77
81
  ## Storing credentials
78
82
 
79
- **Web UI (recommended):** Install `aishell-credentials` (or the `aishell-base` bundle) — the Settings page gets an "AI Shell" section with AK/SK/SecurityToken input fields. Values are transferred to the credential store.
83
+ **Web UI (recommended):** Install `aishell-credentials` (or the `aishell-base` bundle) — clicking the credentials button beside the chat input (or the one on a New Session page) opens a form with AK/SK/SecurityToken fields. Values are verified against Huawei Cloud IAM and transferred to the credential store; the button flips to the account name when they are stored.
80
84
 
81
85
  **CLI fallback:**
82
86
 
package/lib/client.js CHANGED
@@ -1,221 +1,234 @@
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
-
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
4
 
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
+ /** Every Edit button an aishell row renders (there is ordinarily one; a
50
+ * re-render that leaves a second copy gets both, so none stays visible). */
51
+ function findEditButtons(row) {
52
+ const actions = row.querySelector("[class*=\"rowActions\"]");
53
+ if (actions === null) return [];
54
+ const found = [];
55
+ for (const button of actions.querySelectorAll("button")) {
56
+ const label = button.getAttribute("aria-label") ?? "";
57
+ if (label.includes(PROVIDER_DISPLAY_NAME) || button.textContent === "编辑") found.push(button);
58
+ }
59
+ return found;
60
+ }
44
61
  /**
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.
62
+ * Whether `node` is an Element (`querySelectorAll` roots can be non-element
63
+ * nodes like the Document, where the row check below simply answers false).
64
+ */
65
+ function isElement(node) {
66
+ return node.nodeType === Node.ELEMENT_NODE;
67
+ }
68
+ /**
69
+ * Whether `node` is a provider row whose rendered name is ours.
51
70
  *
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.
71
+ * Duck-typed on the attribute surface rather than `instanceof Element` so
72
+ * the shadow works identically in the browser and under the specs' DOM
73
+ * stub, which provides the attribute methods without the constructor.
74
+ */
75
+ function isProviderRow(node) {
76
+ if (node.nodeType !== Node.ELEMENT_NODE) return false;
77
+ const element = node;
78
+ const name = element.querySelector("[class*=\"rowName\"]");
79
+ return name !== null && name.textContent === PROVIDER_DISPLAY_NAME;
80
+ }
81
+ /**
82
+ * Apply the shadow to one row: add the dot (or repaint an existing one) and
83
+ * hide the Edit button. Idempotent — a second pass rewrites the dot's
84
+ * readiness instead of stacking another one.
54
85
  *
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.
86
+ * `target` is the owning document, passed in because the stub DOM the specs
87
+ * run on keeps no `ownerDocument` link.
57
88
  */
58
- function catalogStatusText(catalog) {
59
- if (catalog === void 0) return "获取中…";
60
- if (catalog.models.length > 0) return "";
61
- return catalog.credentialsPresent ? "网关未返回任何模型" : "需要凭据才能获取(凭据在 设置 → AI Shell 中配置)";
89
+ function shadowRow(row, ready, titles, target) {
90
+ const identity = row.querySelector("[class*=\"rowIdentity\"]");
91
+ if (identity === null) return;
92
+ const state = ready ? "ok" : "warn";
93
+ const existing = identity.querySelector(`[${ROW_DOT_MARKER}]`);
94
+ const dot = existing ?? target.createElement("span");
95
+ dot.setAttribute(ROW_DOT_MARKER, state);
96
+ dot.setAttribute("title", titles[state]);
97
+ dot.setAttribute("aria-label", titles[state]);
98
+ if (existing === null) {
99
+ dot.setAttribute("role", "img");
100
+ identity.appendChild(dot);
101
+ }
102
+ for (const edit of findEditButtons(row)) edit.setAttribute(ROW_EDIT_HIDDEN, "");
62
103
  }
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
104
+ /** Undo one row: remove the dot, restore the Edit button. */
105
+ function unshadowRow(row) {
106
+ row.querySelectorAll(`[${ROW_DOT_MARKER}]`).forEach((dot) => {
107
+ dot.remove();
108
+ });
109
+ row.querySelectorAll(`[${ROW_EDIT_HIDDEN}]`).forEach((button) => {
110
+ button.removeAttribute(ROW_EDIT_HIDDEN);
111
+ });
112
+ }
113
+ /**
114
+ * Install the provider-row shadow on a document.
115
+ *
116
+ * `ready` is the credential fact as the browser half derives it from the
117
+ * settings namespace `aishell-credentials` owns (see `client/index.ts`);
118
+ * `titles` is the dot's hover copy in the active locale (the installer
119
+ * translates; this module holds only the DOM work). Neither is read from any
120
+ * store here, so a later credential save or locale switch is a reinstall with
121
+ * the new values, not a re-derivation buried in here. Sweeps the page once,
122
+ * then observes the body so re-rendered rows are covered too. Returns the
123
+ * disposer that stops the observer and restores every row.
124
+ */
125
+ function installProviderRowShadow(target, ready, titles) {
126
+ if (typeof target.querySelector !== "function" || typeof MutationObserver !== "function") return () => {};
127
+ /** Run the shadow over every provider row at or below `root`. */
128
+ const sweep = (root) => {
129
+ if (isElement(root) && isProviderRow(root)) shadowRow(root, ready, titles, target);
130
+ for (const row of root.querySelectorAll("[class*=\"rowCard\"], [class*=\"setupCard\"]")) if (isProviderRow(row)) shadowRow(row, ready, titles, target);
121
131
  };
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)"
132
+ const sweepIfRow = (node) => {
133
+ if (isProviderRow(node)) {
134
+ shadowRow(node, ready, titles, target);
135
+ return;
136
+ }
137
+ for (let parent = node.parentElement; parent !== null; parent = parent.parentElement) sweep(parent);
129
138
  };
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
- })]
139
+ const style = target.createElement("style");
140
+ style.setAttribute(ROW_DOT_MARKER + "-css", "");
141
+ style.textContent = ROW_DOT_CSS;
142
+ target.head.appendChild(style);
143
+ sweep(target.body);
144
+ const observer = new MutationObserver((mutations) => {
145
+ for (const mutation of mutations) for (const node of mutation.addedNodes) if (node.nodeType === Node.ELEMENT_NODE) sweepIfRow(node);
186
146
  });
147
+ observer.observe(target.body, {
148
+ childList: true,
149
+ subtree: true
150
+ });
151
+ return () => {
152
+ observer.disconnect();
153
+ style.remove();
154
+ for (const row of target.body.querySelectorAll("[class*=\"rowCard\"], [class*=\"setupCard\"]")) if (isProviderRow(row)) unshadowRow(row);
155
+ };
187
156
  }
157
+
158
+ //#endregion
159
+ //#region src/client/locales.ts
188
160
  /**
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.
161
+ * aishell-llm locale dictionaries — the Models-page provider row's copy.
162
+ *
163
+ * The row shadow is a DOM patch, not a React render, so its strings do not
164
+ * ride the framework's locale seat: the install reads them through `t` at
165
+ * install time and repaints on a locale switch (the client half re-installs
166
+ * on `locale/change`). zh is the key-set source of truth; en mirrors it.
167
+ *
168
+ * @module @huaweidevtools/aishell-dsh-llm/client/locales
193
169
  */
194
- const MODELS_ROUTE = "api/aishell/models";
170
+ /** Dictionary namespace owned by this plugin. */
171
+ const NS = "aishell-llm";
172
+ /** Simplified Chinese dictionary (the key-set source of truth). */
173
+ const zh = {
174
+ "provider.ready": "已就绪",
175
+ "provider.warn": "未就绪 — 凭据未配置(点击对话输入框上方的凭据按钮填写 AK/SK)"
176
+ };
177
+ /** English dictionary. */
178
+ const en = {
179
+ "provider.ready": "Ready",
180
+ "provider.warn": "Not ready — credentials missing (click the credentials button above the chat input to enter AK/SK)"
181
+ };
195
182
 
196
183
  //#endregion
197
184
  //#region src/client/index.ts
198
- /** The settings namespace aishell-credentials owns (the card reads its status). */
185
+ /** The settings namespace aishell-credentials owns (the shadow reads its status). */
199
186
  const AISHELL_CREDENTIALS_NS = "aishell-client-credentials";
200
187
  /** Required services. */
201
- const inject = ["slots", "configForms"];
188
+ const inject = ["configForms", "locale"];
202
189
  function apply(ctx) {
190
+ if (typeof document === "undefined") return;
191
+ ctx.effect(() => ctx.locale.register(NS, {
192
+ zh,
193
+ en
194
+ }), "aishell-llm: dictionaries");
195
+ const t = ctx.locale.bind(NS);
203
196
  const form = ctx.configForms.get(AISHELL_CREDENTIALS_NS);
204
- const status = (0, __deepseek_ai_dsh_client_store.createSnapshotStore)({ credentialsStored: false });
197
+ let readiness = form.getSnapshot().value?.credentialsStored === true;
198
+ let disposeShadow;
199
+ const titles = () => ({
200
+ ok: t("provider.ready"),
201
+ warn: t("provider.warn")
202
+ });
203
+ const sync = () => {
204
+ disposeShadow?.();
205
+ disposeShadow = installProviderRowShadow(document, readiness, titles());
206
+ };
205
207
  const derive = () => {
206
- status.set({ credentialsStored: form.getSnapshot().value?.credentialsStored === true });
208
+ const next = form.getSnapshot().value?.credentialsStored === true;
209
+ if (next !== readiness) {
210
+ readiness = next;
211
+ sync();
212
+ }
207
213
  };
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");
214
+ ctx.effect(() => ctx.configForms.whileServed([AISHELL_CREDENTIALS_NS], () => {
215
+ sync();
216
+ return () => {
217
+ disposeShadow?.();
218
+ disposeShadow = void 0;
219
+ };
220
+ }), "aishell-llm: models page provider row shadow");
221
+ ctx.effect(() => form.subscribe(derive), "aishell-llm: credential presence for the models row");
222
+ ctx.effect(() => ctx.on("locale/change", () => {
223
+ sync();
224
+ }), "aishell-llm: shadow locale repaint");
215
225
  }
216
226
 
217
227
  //#endregion
228
+ exports.ROW_DOT_MARKER = ROW_DOT_MARKER
229
+ exports.ROW_EDIT_HIDDEN = ROW_EDIT_HIDDEN
218
230
  exports.apply = apply
219
231
  exports.inject = inject
232
+ exports.installProviderRowShadow = installProviderRowShadow
220
233
  return module.exports; } });
221
234
  //# 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,31 @@
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 are entered
6
+ * through the credential form the `aishell-credentials` chips open. What the
7
+ * row should tell the user is the one fact the old card extension distilled —
8
+ * ready or not — so the browser half paints it as a status dot beside the
9
+ * row's name and hides the header's Edit button (see
10
+ * `client/provider-row-shadow.ts`).
11
+ *
12
+ * The readiness fact is read from the `aishell-client-credentials` settings
13
+ * namespace (`aishell-credentials` owns it; the shadow never names accounts
14
+ * or manages anything). The shadow follows that namespace through
15
+ * `whileServed`, so a deployment without that plugin (this LLM package
16
+ * installed standalone) shows no trace of the dot either — the fact is
17
+ * unknown, not false.
18
+ *
19
+ * The dot's hover copy rides this package's own locale namespace. The shadow
20
+ * is a DOM patch that does not re-render on a locale switch, so the install
21
+ * is redone on `locale/change` — the repaint is attribute writes over rows
22
+ * the sweep re-finds, not a page mutation.
13
23
  *
14
24
  * @module @huaweidevtools/aishell-dsh-llm/client
15
25
  */
16
26
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
17
- export type { AishellProviderCard, AishellProviderCardProps } from './ProviderCard.tsx';
27
+ export type { ProviderRowShadowDisposer, ProviderRowTitles, } from './provider-row-shadow.ts';
28
+ export { installProviderRowShadow, ROW_DOT_MARKER, ROW_EDIT_HIDDEN, } from './provider-row-shadow.ts';
18
29
  /** Required services. */
19
30
  export declare const inject: string[];
20
31
  export declare function apply(ctx: ClientContext): void;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * aishell-llm locale dictionaries — the Models-page provider row's copy.
3
+ *
4
+ * The row shadow is a DOM patch, not a React render, so its strings do not
5
+ * ride the framework's locale seat: the install reads them through `t` at
6
+ * install time and repaints on a locale switch (the client half re-installs
7
+ * on `locale/change`). zh is the key-set source of truth; en mirrors it.
8
+ *
9
+ * @module @huaweidevtools/aishell-dsh-llm/client/locales
10
+ */
11
+ /** Dictionary namespace owned by this plugin. */
12
+ export declare const NS: "aishell-llm";
13
+ /** Simplified Chinese dictionary (the key-set source of truth). */
14
+ export declare const zh: {
15
+ readonly 'provider.ready': "已就绪";
16
+ readonly 'provider.warn': "未就绪 — 凭据未配置(点击对话输入框上方的凭据按钮填写 AK/SK)";
17
+ };
18
+ /** English dictionary. */
19
+ export declare const en: {
20
+ readonly 'provider.ready': "Ready";
21
+ readonly 'provider.warn': "Not ready — credentials missing (click the credentials button above the chat input to enter AK/SK)";
22
+ };
23
+ /** All keys (derived from the zh dictionary, the source of truth). */
24
+ export type AishellLlmLocaleKey = keyof typeof zh;
25
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
26
+ interface LocaleNamespaceMap {
27
+ 'aishell-llm': AishellLlmLocaleKey;
28
+ }
29
+ }
30
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1,80 @@
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 (installer-supplied, so it
28
+ * follows the active locale) as its title;
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
+ /** Title copy for the dot, by readiness — supplied by the installer so it
57
+ * follows the active locale (this module is a DOM patch with no locale seat
58
+ * of its own). The warn state keeps the actionable half — where to enter
59
+ * AK/SK — because that is the one thing a fresh user cannot guess; the ready
60
+ * state needs nothing beyond itself. */
61
+ export interface ProviderRowTitles {
62
+ /** Hover copy when AK/SK are stored. */
63
+ readonly ok: string;
64
+ /** Hover copy when they are not. */
65
+ readonly warn: string;
66
+ }
67
+ /**
68
+ * Install the provider-row shadow on a document.
69
+ *
70
+ * `ready` is the credential fact as the browser half derives it from the
71
+ * settings namespace `aishell-credentials` owns (see `client/index.ts`);
72
+ * `titles` is the dot's hover copy in the active locale (the installer
73
+ * translates; this module holds only the DOM work). Neither is read from any
74
+ * store here, so a later credential save or locale switch is a reinstall with
75
+ * the new values, not a re-derivation buried in here. Sweeps the page once,
76
+ * then observes the body so re-rendered rows are covered too. Returns the
77
+ * disposer that stops the observer and restores every row.
78
+ */
79
+ export declare function installProviderRowShadow(target: Document, ready: boolean, titles: ProviderRowTitles): ProviderRowShadowDisposer;
80
+ //# 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.4",
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.4"
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,18 @@
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",
50
+ "@deepseek-ai/dsh-client-locale": ">=0.2.0-0 <0.3.0-0",
54
51
  "@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",
52
+ "@deepseek-ai/dsh-client-ui-slots": ">=0.2.0-0 <0.3.0-0",
57
53
  "@deepseek-ai/dsh-fs": "0.2.0-rc.1",
58
54
  "@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"
55
+ "@deepseek-ai/schemastery": "3.18.4"
62
56
  },
63
57
  "dsh": {
64
58
  "client": {
65
59
  "inject": [
66
60
  "@deepseek-ai/dsh-client-ui-settings",
67
- "@deepseek-ai/dsh-client-ui-settings-models",
68
- "@deepseek-ai/dsh-api-remotes"
61
+ "@deepseek-ai/dsh-client-locale"
69
62
  ],
70
63
  "platform": "web"
71
64
  },
@@ -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