@edge-sky/dsh-oauth-adapter 0.1.2-dev.1 → 0.1.2-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -1,4 +1,4 @@
1
1
  # Bilingual-pair consistency record: the git blob hash of each side as of the
2
2
  # last confirmed-consistent state. Both languages carry equal authority.
3
- README.md: f3e3243d02440749e53038872e3fdca91f516468
4
- README.zh.md: 31a969685a57a6919b8544e24a7e09c2a4b77303
3
+ README.md: fd0eef65a18d89c5e1dd49d9bc0fc89f9c2a770a
4
+ README.zh.md: aeb4cf30a05c98c1b885c4c7eece00f3bb138c39
package/README.md CHANGED
@@ -56,3 +56,30 @@ pnpm test
56
56
  ```
57
57
 
58
58
  Tests cover provider responses, matching, retained results, revisions, migration recovery, cancellation, actual rc adapter requests with network interception, and published rc model-page bundles in a DOM environment with replayable snapshots. WebSocket tests bind a loopback port. All test credentials are synthetic; these checks do not establish live-account discovery or inference success for any provider. DSH source changes are not required.
59
+
60
+ ## Local package and installation verification
61
+
62
+ `npm pack` and `npm publish` from the package directory run type checking, a fresh build, and tests through `prepack`; a failure stops packaging. Do not bypass this check with `--ignore-scripts`. Published versions cannot be overwritten, so a fixed release needs a new version number.
63
+
64
+ A temporary `DSH_HOME` lets you test a local tarball without affecting existing accounts or profiles or changing DSH code:
65
+
66
+ ```sh
67
+ pnpm install --frozen-lockfile
68
+ artifact_dir=$(mktemp -d)
69
+ npm pack --pack-destination "$artifact_dir"
70
+ export DSH_HOME=$(mktemp -d)
71
+ mkdir -p "$DSH_HOME/profiles/web"
72
+ cat > "$DSH_HOME/profiles/web/pnpm-workspace.yaml" <<'YAML'
73
+ packages:
74
+ - .
75
+ nodeLinker: hoisted
76
+ autoInstallPeers: false
77
+ allowBuilds:
78
+ '@google/genai': false
79
+ protobufjs: false
80
+ YAML
81
+ npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add "$artifact_dir"/*.tgz
82
+ npx @deepseek-ai/dsh@0.1.2-rc.1 web --no-open
83
+ ```
84
+
85
+ After stopping the server, run `unset DSH_HOME` to restore the default directory. The test profile explicitly skips installation scripts for these two dependencies; the plugin repository's policy does not propagate to consumers. New users still need to configure this policy in their own profiles. Successful startup verifies plugin loading; real-account login, model discovery, and inference need separate verification.
package/README.zh.md CHANGED
@@ -56,3 +56,30 @@ pnpm test
56
56
  ```
57
57
 
58
58
  测试覆盖供应商响应、匹配、失败保留、revision 冲突、迁移恢复、取消、经过网络拦截的真实 rc 适配器请求,以及 DOM 环境中加载已发布 rc 模型页产物的可重放快照。WebSocket 测试需要绑定回环端口。测试凭据全部为合成数据,这些验证不代表任何供应商的真实账号发现或推理调用已经通过。无需修改 DSH 源码。
59
+
60
+ ## 本地打包与安装验证
61
+
62
+ `npm pack` 和从目录执行的 `npm publish` 通过 `prepack` 运行类型检查、重新构建和测试;任何一步失败都会终止打包。不要使用 `--ignore-scripts` 跳过该检查。已发布版本不能覆盖,修复后发布必须使用新版本号。
63
+
64
+ 使用临时 `DSH_HOME` 可以测试本地 tarball,不影响现有账号与 profile,也不需要修改 DSH 代码:
65
+
66
+ ```sh
67
+ pnpm install --frozen-lockfile
68
+ artifact_dir=$(mktemp -d)
69
+ npm pack --pack-destination "$artifact_dir"
70
+ export DSH_HOME=$(mktemp -d)
71
+ mkdir -p "$DSH_HOME/profiles/web"
72
+ cat > "$DSH_HOME/profiles/web/pnpm-workspace.yaml" <<'YAML'
73
+ packages:
74
+ - .
75
+ nodeLinker: hoisted
76
+ autoInstallPeers: false
77
+ allowBuilds:
78
+ '@google/genai': false
79
+ protobufjs: false
80
+ YAML
81
+ npx @deepseek-ai/dsh@0.1.2-rc.1 plugin --profile web add "$artifact_dir"/*.tgz
82
+ npx @deepseek-ai/dsh@0.1.2-rc.1 web --no-open
83
+ ```
84
+
85
+ 停止服务器后执行 `unset DSH_HOME` 恢复默认目录。测试 profile 的脚本策略显式跳过这两个依赖的安装脚本;插件仓库的策略不会传播到消费端。新用户仍需在自己的 profile 中配置该策略。启动成功只验证插件加载;真实账号登录、模型发现和推理需要单独验证。
package/lib/client.js CHANGED
@@ -6,8 +6,37 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
+ //#region src/model-types.ts
10
+ const MODEL_PROTOCOLS = {
11
+ "github-copilot": [
12
+ "openai-completions",
13
+ "openai-responses",
14
+ "anthropic-messages"
15
+ ],
16
+ openrouter: ["openai-completions", "anthropic-messages"],
17
+ "openai-codex": ["openai-codex-responses"],
18
+ xai: ["openai-responses"],
19
+ anthropic: ["anthropic-messages"],
20
+ "kimi-coding": ["anthropic-messages"]
21
+ };
22
+ function modelText(value) {
23
+ return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\u0000-\u001f\u007f]/u.test(value);
24
+ }
25
+ /** Validate at the wire and durable-data boundaries; fixed-protocol routes accept no override. */
26
+ function validManual(provider, value) {
27
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
28
+ const m = value;
29
+ if (Object.keys(m).some((k) => ![
30
+ "id",
31
+ "name",
32
+ "api"
33
+ ].includes(k)) || !modelText(m.id)) return false;
34
+ if (m.name !== void 0 && !modelText(m.name)) return false;
35
+ const choices = MODEL_PROTOCOLS[provider];
36
+ return choices.length === 1 ? m.api === void 0 : typeof m.api === "string" && choices.includes(m.api);
37
+ }
38
+ //#endregion
9
39
  //#region src/protocol.ts
10
- /** Shared browser/Host wire vocabulary for the OAuth account surface. */
11
40
  const OAUTH_SOCKET_PATH = "/_edge-sky/dsh-oauth";
12
41
  const OAUTH_SOCKET_PROTOCOL = "dsh-oauth-v1";
13
42
  const PROVIDERS = [
@@ -49,6 +78,262 @@ window.__ModuleLoader__.load({
49
78
  }
50
79
  ];
51
80
  //#endregion
81
+ //#region src/client/OAuthModelsSection.tsx
82
+ /** OAuth-specific model management mounted inside the existing Models settings page. */
83
+ function ModelForm({ provider, initial, busy, t, save, close }) {
84
+ const [id, setId] = (0, react.useState)(initial?.id ?? "");
85
+ const [name, setName] = (0, react.useState)(initial?.name ?? "");
86
+ const choices = MODEL_PROTOCOLS[provider];
87
+ const [api, setApi] = (0, react.useState)(initial?.api ?? choices[0]);
88
+ const model = {
89
+ id: id.trim(),
90
+ ...name.trim() ? { name: name.trim() } : {},
91
+ ...choices.length > 1 ? { api } : {}
92
+ };
93
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
94
+ className: "dsh-oauth-model-form",
95
+ onSubmit: (event) => {
96
+ event.preventDefault();
97
+ if (validManual(provider, model)) save(model);
98
+ },
99
+ children: [
100
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("modelId"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
101
+ required: true,
102
+ maxLength: 256,
103
+ value: id,
104
+ readOnly: initial !== void 0,
105
+ onChange: (event) => setId(event.target.value)
106
+ })] }),
107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("modelName"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
108
+ maxLength: 256,
109
+ value: name,
110
+ onChange: (event) => setName(event.target.value),
111
+ placeholder: id
112
+ })] }),
113
+ choices.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [t("modelProtocol"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
114
+ value: api,
115
+ onChange: (event) => setApi(event.target.value),
116
+ children: choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
117
+ value: choice,
118
+ children: choice === "openai-completions" ? "Chat Completions" : choice === "openai-responses" ? "Responses" : choice === "openai-codex-responses" ? "Codex Responses" : "Anthropic Messages"
119
+ }, choice))
120
+ })] }),
121
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
122
+ className: "dsh-oauth-muted",
123
+ children: t("manualModelHint")
124
+ }),
125
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
126
+ type: "submit",
127
+ disabled: busy || !validManual(provider, model),
128
+ children: t("saveModel")
129
+ }),
130
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
131
+ type: "button",
132
+ disabled: busy,
133
+ onClick: close,
134
+ children: t("cancel")
135
+ })
136
+ ]
137
+ });
138
+ }
139
+ function ProviderModels({ page, controller, busy, error, t }) {
140
+ const [editing, setEditing] = (0, react.useState)();
141
+ const [submitted, setSubmitted] = (0, react.useState)(false);
142
+ const [confirmMigration, setConfirmMigration] = (0, react.useState)(false);
143
+ (0, react.useEffect)(() => {
144
+ if (submitted && !busy) {
145
+ if (!error) setEditing(void 0);
146
+ setSubmitted(false);
147
+ }
148
+ }, [
149
+ busy,
150
+ error,
151
+ submitted
152
+ ]);
153
+ const errorCode = error?.code ?? page.errorCode;
154
+ const errorLabel = errorCode === "settings-conflict" ? "modelConflict" : errorCode === "discovery-unsupported" ? "modelUnsupported" : errorCode === "discovery-auth" ? "modelAuthFailed" : errorCode === "discovery-timeout" ? "modelTimedOut" : errorCode === "route-conflict" ? "modelRouteConflict" : "modelOperationFailed";
155
+ const disabled = busy || !page.connected || !page.writable;
156
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
157
+ className: "dsh-oauth-model-provider",
158
+ children: [
159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: PROVIDERS.find((p) => p.id === page.provider)?.modelGroupLabel }),
160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
161
+ page.connected ? t("modelConnected") : t("disconnected"),
162
+ " · ",
163
+ page.source === "account" ? t("accountCatalog") : t("builtinCatalog")
164
+ ] }),
165
+ page.syncedAt && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
166
+ t("lastModelSync"),
167
+ ": ",
168
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
169
+ dateTime: page.syncedAt,
170
+ children: new Date(page.syncedAt).toLocaleString()
171
+ })
172
+ ] }),
173
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
174
+ t("matchedModels"),
175
+ ": ",
176
+ page.counts.matched,
177
+ " · ",
178
+ t("pendingModels"),
179
+ ": ",
180
+ page.counts.pending,
181
+ " · ",
182
+ t("manualModels"),
183
+ ": ",
184
+ page.counts.manual
185
+ ] }),
186
+ (error || page.error) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
187
+ role: "alert",
188
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t(errorLabel) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("technicalDetails") }), error?.message ?? page.error] })]
189
+ }),
190
+ page.migration ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
191
+ disabled,
192
+ onClick: () => setConfirmMigration(!confirmMigration),
193
+ children: t("reviewMigration")
194
+ }), confirmMigration && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
195
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("migrationDescription") }),
196
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [
197
+ page.migration.fields.join(", "),
198
+ " · ",
199
+ page.migration.models,
200
+ " ",
201
+ t("modelsTitle")
202
+ ] }),
203
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
204
+ disabled,
205
+ onClick: () => controller.migrateModels(page.provider),
206
+ children: t("confirmMigration")
207
+ })
208
+ ] })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
209
+ disabled: disabled || !page.managed,
210
+ onClick: () => controller.syncModels(page.provider),
211
+ children: busy ? t("modelWorking") : t("syncModels")
212
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
213
+ disabled: disabled || !page.managed,
214
+ onClick: () => setEditing(null),
215
+ children: t("addModel")
216
+ })] }),
217
+ editing !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelForm, {
218
+ provider: page.provider,
219
+ ...editing === null ? {} : { initial: editing },
220
+ busy,
221
+ t,
222
+ save: (model) => {
223
+ setSubmitted(true);
224
+ controller.saveModel(page.provider, model);
225
+ },
226
+ close: () => setEditing(void 0)
227
+ }, editing?.id ?? "new"),
228
+ [
229
+ "matched",
230
+ "pending",
231
+ "manual"
232
+ ].map((source) => {
233
+ const rows = page.rows.filter((row) => (row.source === "catalog" ? "matched" : row.source) === source);
234
+ if (!rows.length) return null;
235
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
236
+ "aria-label": t(source === "pending" ? "pendingModels" : source === "manual" ? "manualModels" : "matchedModels"),
237
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", { children: t(source === "pending" ? "pendingModels" : source === "manual" ? "manualModels" : "matchedModels") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
238
+ className: "dsh-oauth-model-list",
239
+ children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
240
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
241
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: row.name }),
242
+ " ",
243
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: row.id }),
244
+ " ",
245
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(row.source === "pending" ? "pendingModels" : row.source === "manual" ? "manualModels" : "matchedModels") })
246
+ ] }),
247
+ row.source === "pending" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
248
+ disabled: disabled || !page.managed,
249
+ onClick: () => setEditing({
250
+ id: row.id,
251
+ name: row.name
252
+ }),
253
+ children: t("configureModel")
254
+ }),
255
+ row.source === "manual" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
256
+ disabled,
257
+ onClick: () => setEditing({
258
+ id: row.id,
259
+ name: row.name,
260
+ ...MODEL_PROTOCOLS[page.provider].length > 1 ? { api: row.api } : {}
261
+ }),
262
+ children: t("editModel")
263
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
264
+ disabled,
265
+ onClick: () => controller.deleteModel(page.provider, row.id),
266
+ children: t("deleteModel")
267
+ })] })
268
+ ] }, row.id))
269
+ })]
270
+ }, source);
271
+ }),
272
+ page.total === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("noModels") }),
273
+ page.total > 10 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
274
+ "aria-label": t("modelsPagination"),
275
+ children: [
276
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
277
+ disabled: page.offset === 0 || busy,
278
+ onClick: () => controller.readModels(page.provider, Math.max(0, page.offset - 10)),
279
+ children: t("previousModels")
280
+ }),
281
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
282
+ page.offset + 1,
283
+ "–",
284
+ Math.min(page.offset + 10, page.total),
285
+ " / ",
286
+ page.total
287
+ ] }),
288
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
289
+ disabled: page.offset + 10 >= page.total || busy,
290
+ onClick: () => controller.readModels(page.provider, page.offset + 10),
291
+ children: t("nextModels")
292
+ })
293
+ ]
294
+ })
295
+ ]
296
+ });
297
+ }
298
+ function OAuthModelsSection(props) {
299
+ const { controller, t } = props;
300
+ if (!controller || !t) return null;
301
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OAuthModels, {
302
+ controller,
303
+ t
304
+ });
305
+ }
306
+ function OAuthModels({ controller, t }) {
307
+ const snapshot = (0, react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
308
+ (0, react.useEffect)(() => {
309
+ controller.loadModels();
310
+ }, [controller]);
311
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
312
+ className: "dsh-oauth-section",
313
+ children: [
314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: t("modelsTitle") }),
315
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("modelsIntro") }),
316
+ snapshot.connection !== "open" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
317
+ role: "status",
318
+ children: t("hostDisconnected")
319
+ }),
320
+ PROVIDERS.map((provider) => {
321
+ const page = snapshot.modelPages?.[provider.id];
322
+ if (!page) return null;
323
+ if (!page.connected && !page.managed && page.counts.manual === 0) return null;
324
+ const error = snapshot.modelErrors?.[provider.id];
325
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderModels, {
326
+ page,
327
+ controller,
328
+ t,
329
+ busy: snapshot.connection !== "open" || snapshot.modelBusy?.[provider.id] === true || page.busy,
330
+ ...error ? { error } : {}
331
+ }, provider.id);
332
+ })
333
+ ]
334
+ });
335
+ }
336
+ //#endregion
52
337
  //#region src/client/OAuthAccountsSection.tsx
53
338
  /** OAuth account settings page. */
54
339
  function safeHttpUrl(value) {
@@ -179,7 +464,7 @@ window.__ModuleLoader__.load({
179
464
  t: props.t
180
465
  }, props.attempt.prompt.id),
181
466
  props.attempt.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorPanel, {
182
- message: props.attempt.error,
467
+ message: props.attempt.phase === "authorized" ? props.t("loginSyncFailed") : props.attempt.error,
183
468
  ...props.attempt.errorDetail === void 0 ? {} : { detail: props.attempt.errorDetail },
184
469
  t: props.t
185
470
  }),
@@ -297,6 +582,40 @@ window.__ModuleLoader__.load({
297
582
  //#region src/client/locales.ts
298
583
  /** English copy for the OAuth accounts page. */
299
584
  const en = {
585
+ modelUnsupported: "This provider does not support account discovery with the current credential. You can still add models manually.",
586
+ modelAuthFailed: "Model discovery was denied. Reconnect or check this account’s permissions. The previous list was retained.",
587
+ modelTimedOut: "Model synchronization timed out. The previous list was retained.",
588
+ modelRouteConflict: "This provider is owned by an existing configuration. Review the details before migration.",
589
+ loginSyncFailed: "Sign-in succeeded, but model synchronization failed. Open Models to retry.",
590
+ modelsTitle: "OAuth models",
591
+ modelsIntro: "Synchronize account-visible models or add a model manually. Manual models survive synchronization.",
592
+ modelId: "Model ID",
593
+ modelName: "Display name",
594
+ modelProtocol: "Request protocol",
595
+ manualModelHint: "Saving configures a request; it does not verify account access or model capabilities.",
596
+ saveModel: "Save model",
597
+ modelConnected: "Connected",
598
+ accountCatalog: "Account-synchronized list",
599
+ builtinCatalog: "Built-in list · account access unverified",
600
+ lastModelSync: "Last successful synchronization",
601
+ matchedModels: "Matched",
602
+ pendingModels: "Needs configuration",
603
+ manualModels: "Manual",
604
+ modelConflict: "Models changed elsewhere. The list was refreshed; review it and retry.",
605
+ modelOperationFailed: "The model operation failed. Review the details before retrying.",
606
+ reviewMigration: "Review migration",
607
+ migrationDescription: "Move this OAuth provider configuration to plugin management while preserving its ID and credentials. The following configuration fields will be retained:",
608
+ confirmMigration: "Migrate this provider",
609
+ modelWorking: "Working…",
610
+ syncModels: "Synchronize models",
611
+ addModel: "Add model",
612
+ configureModel: "Configure",
613
+ editModel: "Edit",
614
+ deleteModel: "Delete manual entry",
615
+ noModels: "No selectable models. Configure an unmatched model or add one manually.",
616
+ modelsPagination: "OAuth model pages",
617
+ previousModels: "Previous",
618
+ nextModels: "Next",
300
619
  nav: "OAuth Accounts",
301
620
  title: "OAuth Accounts",
302
621
  intro: "Connect subscription-backed providers. Their models appear in separate OAuth groups, apart from API-key providers.",
@@ -329,6 +648,40 @@ window.__ModuleLoader__.load({
329
648
  };
330
649
  /** Simplified Chinese copy for the OAuth accounts page. */
331
650
  const zh = {
651
+ modelUnsupported: "当前凭据不支持查询该供应商的账号模型列表,仍可手动添加模型。",
652
+ modelAuthFailed: "模型查询被拒绝,请重新连接或检查账号权限。已保留之前的列表。",
653
+ modelTimedOut: "模型同步超时,已保留之前的列表。",
654
+ modelRouteConflict: "该供应商由现有配置管理,请查看详情后再迁移。",
655
+ loginSyncFailed: "登录成功,但模型同步失败。请前往模型页面重试。",
656
+ modelsTitle: "OAuth 模型",
657
+ modelsIntro: "同步账号可见模型,或手动添加模型。同步时会保留手动模型。",
658
+ modelId: "模型 ID",
659
+ modelName: "显示名称",
660
+ modelProtocol: "调用协议",
661
+ manualModelHint: "保存后可以尝试调用,但不代表已经验证账号权限或模型能力。",
662
+ saveModel: "保存模型",
663
+ modelConnected: "已连接",
664
+ accountCatalog: "已同步账号列表",
665
+ builtinCatalog: "内置列表 · 尚未验证账号权限",
666
+ lastModelSync: "最近成功同步",
667
+ matchedModels: "已匹配",
668
+ pendingModels: "待配置",
669
+ manualModels: "手动模型",
670
+ modelConflict: "模型已被其他操作修改。列表已刷新,请确认后重试。",
671
+ modelOperationFailed: "模型操作失败,请查看详情后重试。",
672
+ reviewMigration: "预览迁移",
673
+ migrationDescription: "将此 OAuth 供应商配置迁移到插件管理,保留供应商 ID 和凭据。以下配置字段将被保留:",
674
+ confirmMigration: "迁移此供应商",
675
+ modelWorking: "正在处理…",
676
+ syncModels: "同步模型",
677
+ addModel: "手动添加",
678
+ configureModel: "补充配置",
679
+ editModel: "编辑",
680
+ deleteModel: "删除手动项",
681
+ noModels: "暂无可选模型,请补充待配置项或手动添加。",
682
+ modelsPagination: "OAuth 模型分页",
683
+ previousModels: "上一页",
684
+ nextModels: "下一页",
332
685
  nav: "OAuth 账户",
333
686
  title: "OAuth 账户",
334
687
  intro: "连接订阅型模型服务。OAuth 模型会以独立分组显示,与 API 密钥提供商分开。",
@@ -395,6 +748,9 @@ window.__ModuleLoader__.load({
395
748
  reconnectTimer;
396
749
  reconnectDelay = 500;
397
750
  disposed = false;
751
+ modelsVisible = false;
752
+ modelRequests = /* @__PURE__ */ new Map();
753
+ latestModelReads = /* @__PURE__ */ new Map();
398
754
  constructor() {
399
755
  this.connect();
400
756
  }
@@ -453,6 +809,69 @@ window.__ModuleLoader__.load({
453
809
  provider
454
810
  });
455
811
  }
812
+ loadModels() {
813
+ this.modelsVisible = true;
814
+ for (const provider of this.snapshot.accounts) this.readModels(provider.provider);
815
+ }
816
+ readModels(provider, offset = 0) {
817
+ if (this.snapshot.connection !== "open") return;
818
+ const requestId = crypto.randomUUID();
819
+ this.latestModelReads.set(provider, requestId);
820
+ this.modelRequests.set(requestId, {
821
+ provider,
822
+ write: false
823
+ });
824
+ this.send({
825
+ type: "models-list",
826
+ requestId,
827
+ provider,
828
+ offset
829
+ });
830
+ }
831
+ syncModels(provider) {
832
+ this.writeModels(provider, { type: "models-sync" });
833
+ }
834
+ migrateModels(provider) {
835
+ this.writeModels(provider, { type: "models-migrate" });
836
+ }
837
+ saveModel(provider, model) {
838
+ this.writeModels(provider, {
839
+ type: "models-save",
840
+ model
841
+ });
842
+ }
843
+ deleteModel(provider, id) {
844
+ this.writeModels(provider, {
845
+ type: "models-delete",
846
+ id
847
+ });
848
+ }
849
+ writeModels(provider, operation) {
850
+ const page = this.snapshot.modelPages?.[provider];
851
+ if (!page || this.snapshot.modelBusy?.[provider] || this.snapshot.connection !== "open") return;
852
+ const requestId = crypto.randomUUID();
853
+ this.modelRequests.set(requestId, {
854
+ provider,
855
+ write: true
856
+ });
857
+ this.update({
858
+ ...this.snapshot,
859
+ modelBusy: {
860
+ ...this.snapshot.modelBusy,
861
+ [provider]: true
862
+ },
863
+ modelErrors: {
864
+ ...this.snapshot.modelErrors,
865
+ [provider]: void 0
866
+ }
867
+ });
868
+ this.send({
869
+ ...operation,
870
+ requestId,
871
+ provider,
872
+ revision: page.revision
873
+ });
874
+ }
456
875
  retry() {
457
876
  if (this.socket?.readyState === WebSocket.OPEN || this.socket?.readyState === WebSocket.CONNECTING) return;
458
877
  if (this.reconnectTimer !== void 0) window.clearTimeout(this.reconnectTimer);
@@ -483,6 +902,12 @@ window.__ModuleLoader__.load({
483
902
  ...current,
484
903
  connection: "open"
485
904
  });
905
+ this.modelRequests.clear();
906
+ this.latestModelReads.clear();
907
+ this.update({
908
+ ...this.snapshot,
909
+ modelBusy: {}
910
+ });
486
911
  this.send({
487
912
  type: "refresh",
488
913
  requestId: crypto.randomUUID()
@@ -551,6 +976,60 @@ window.__ModuleLoader__.load({
551
976
  }
552
977
  receive(message) {
553
978
  switch (message.type) {
979
+ case "models-changed":
980
+ if (this.modelsVisible) this.readModels(message.provider, this.snapshot.modelPages?.[message.provider]?.offset ?? 0);
981
+ return;
982
+ case "models-page": {
983
+ const request = this.modelRequests.get(message.requestId);
984
+ this.modelRequests.delete(message.requestId);
985
+ if (!request) return;
986
+ if (!request.write && this.latestModelReads.get(request.provider) !== message.requestId) return;
987
+ const current = this.snapshot.modelPages?.[request.provider];
988
+ if (current && message.page.revision < current.revision) {
989
+ if (request.write) this.update({
990
+ ...this.snapshot,
991
+ modelBusy: {
992
+ ...this.snapshot.modelBusy,
993
+ [request.provider]: false
994
+ }
995
+ });
996
+ return;
997
+ }
998
+ this.update({
999
+ ...this.snapshot,
1000
+ modelPages: {
1001
+ ...this.snapshot.modelPages,
1002
+ [request.provider]: message.page
1003
+ },
1004
+ modelBusy: {
1005
+ ...this.snapshot.modelBusy,
1006
+ ...request.write ? { [request.provider]: false } : {}
1007
+ }
1008
+ });
1009
+ return;
1010
+ }
1011
+ case "models-error": {
1012
+ const request = this.modelRequests.get(message.requestId);
1013
+ this.modelRequests.delete(message.requestId);
1014
+ if (!request) return;
1015
+ if (!request.write && this.latestModelReads.get(request.provider) !== message.requestId) return;
1016
+ this.update({
1017
+ ...this.snapshot,
1018
+ modelBusy: {
1019
+ ...this.snapshot.modelBusy,
1020
+ ...request.write ? { [request.provider]: false } : {}
1021
+ },
1022
+ modelErrors: {
1023
+ ...this.snapshot.modelErrors,
1024
+ [request.provider]: {
1025
+ code: message.code,
1026
+ message: message.message
1027
+ }
1028
+ }
1029
+ });
1030
+ if (request.write) this.readModels(request.provider);
1031
+ return;
1032
+ }
554
1033
  case "snapshot":
555
1034
  const accounts = [...message.accounts];
556
1035
  const reconciled = reconcileAttemptView(this.snapshot.attempt, accounts);
@@ -561,6 +1040,7 @@ window.__ModuleLoader__.load({
561
1040
  if (reconciled === void 0) delete nextSnapshot.attempt;
562
1041
  else nextSnapshot.attempt = reconciled;
563
1042
  this.update(nextSnapshot);
1043
+ if (this.modelsVisible) for (const account of accounts) this.readModels(account.provider, this.snapshot.modelPages?.[account.provider]?.offset ?? 0);
564
1044
  return;
565
1045
  case "started":
566
1046
  const { error: _error, errorDetail: _errorDetail, ...current } = this.snapshot;
@@ -671,11 +1151,19 @@ window.__ModuleLoader__.load({
671
1151
  .dsh-oauth-actions{display:flex;gap:6px;margin-left:auto}.dsh-oauth-button{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;min-height:30px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:15px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:18px;cursor:pointer}.dsh-oauth-button[data-variant=primary]{border-color:transparent;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.dsh-oauth-button[data-variant=danger]{border-color:transparent;color:var(--dsw-alias-state-error-primary)}.dsh-oauth-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.dsh-oauth-button[data-variant=primary]:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover);color:var(--dsw-alias-label-primary-foreground)}.dsh-oauth-button[data-variant=danger]:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.dsh-oauth-button:focus-visible{outline:none;box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.dsh-oauth-button:disabled{cursor:not-allowed;opacity:.4}
672
1152
  .dsh-oauth-flow{display:flex;flex-direction:column;gap:8px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l3);font-size:13px;line-height:20px}.dsh-oauth-link{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:8px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);text-decoration:none}.dsh-oauth-link:hover{background:var(--dsw-alias-interactive-bg-hover-solid)}.dsh-oauth-link-label{font-weight:500}.dsh-oauth-link-host{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px}.dsh-oauth-link-icon{color:var(--dsw-alias-brand-text)}.dsh-oauth-code{display:flex;align-items:center;gap:8px}.dsh-oauth-code code{padding:5px 8px;border-radius:6px;background:var(--dsw-alias-bg-module-platform);font-size:14px;letter-spacing:.08em}
673
1153
  .dsh-oauth-prompt{display:flex;flex-direction:column;gap:7px}.dsh-oauth-input,.dsh-oauth-select{box-sizing:border-box;width:100%;height:34px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary)}
1154
+
1155
+ .dsh-oauth-model-provider { border: 1px solid var(--border-color, currentColor); border-radius: 8px; padding: 16px; margin-block: 16px; }
1156
+ .dsh-oauth-model-provider button { margin: 4px; }
1157
+ .dsh-oauth-model-list { padding: 0; list-style: none; }
1158
+ .dsh-oauth-model-list li { padding-block: 8px; border-bottom: 1px solid var(--border-color, currentColor); overflow-wrap: anywhere; }
1159
+ .dsh-oauth-model-list code { margin-inline: 8px; }
1160
+ .dsh-oauth-model-form { display: grid; gap: 12px; margin-block: 12px; }
1161
+ .dsh-oauth-model-form label { display: grid; gap: 4px; }
1162
+ .dsh-oauth-model-provider nav { display: flex; align-items: center; gap: 8px; }
674
1163
  `;
675
1164
  //#endregion
676
1165
  //#region src/client/index.ts
677
1166
  const NS = "settings.oauth-accounts";
678
- const MODEL_SETTINGS_VISIBILITY = "modelSettingsVisibility";
679
1167
  /** Required browser services for settings placement and localized copy. */
680
1168
  const inject = ["slots", "locale"];
681
1169
  /** Register the OAuth account page and its app-lifetime connection controller. */
@@ -697,11 +1185,16 @@ window.__ModuleLoader__.load({
697
1185
  ctx.effect(() => () => {
698
1186
  controller.dispose();
699
1187
  }, "dsh-oauth-adapter: controller");
700
- ctx.inject([MODEL_SETTINGS_VISIBILITY], (scope) => {
701
- const visibility = scope.get(MODEL_SETTINGS_VISIBILITY);
702
- scope.effect(() => visibility.registerHiddenProviders(PROVIDERS.map((provider) => provider.id)), "dsh-oauth-adapter: hide OAuth providers from Models settings");
703
- });
704
1188
  const t = ctx.locale.bind(NS);
1189
+ ctx.slots.inject("settings.models.footer", () => ctx.slots.register({
1190
+ name: "settings.models.footer",
1191
+ id: "oauth-models",
1192
+ order: 10,
1193
+ inject: () => ({
1194
+ controller,
1195
+ t
1196
+ })
1197
+ }, OAuthModelsSection));
705
1198
  ctx.slots.inject("settings.section", () => ctx.slots.register({
706
1199
  name: "settings.section",
707
1200
  id: "oauth-accounts",