@dan-ai-studio/dshopencodego 0.1.54 → 0.1.56

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.en.md CHANGED
@@ -109,6 +109,8 @@ Configuration lives in the profile's `cordis.patch.yml`:
109
109
  some-legacy-model: null # a whole-entry null: both fields explicitly fall back to the catalog values
110
110
  modelProtocols: # last resort: per-model protocol override
111
111
  some-new-model: openai-responses # the only legal values are anthropic-messages / openai-completions / openai-responses
112
+ pinnedAddresses: # fixed IP: host -> address (only the gateway host counts)
113
+ opencode.ai: 104.18.32.47 # IPv4 or IPv6; an invalid or absent address uses the system DNS
112
114
  retryPolicy: # optional: this route's retry policy, executed by dsh-llm-retry
113
115
  mode: normal # or always (retry every failure; maxRetries is ignored)
114
116
  maxRetries: 2 # default 5
@@ -141,6 +143,17 @@ A model that does not accept image input is rejected before the request is sent
141
143
 
142
144
  **Retries**: `retryPolicy` is optional — omit it and the host's own default applies. When set, the optional `dsh-llm-retry` plugin executes it, and a malformed policy fails where it is written rather than at the first failure.
143
145
 
146
+ **Fixed IP**: `pinnedAddresses` pins the gateway's name to one address, so the requests reach a closer edge — the edge the system resolves to is sometimes far away, and every request pays for that distance.
147
+
148
+ - **Only the resolution changes**: the name in the request, the host header, the TLS SNI and the certificate check all stay as they are; only the address in the socket is decided. The address therefore has to serve that name, because the certificate is issued for `opencode.ai`.
149
+ - **Only the gateway's own name counts**: the key must be the host of `baseURL` (`opencode.ai` by default). No other name is on this plugin's request path, so a pin for one has no effect. `models.dev` and the provider's documentation always use the system DNS.
150
+ - **Where it applies**: the model API request, the gateway listing (`/models`), and the `/usage` reading — the three requests that go to the gateway.
151
+ - **A gone address falls back**: when the pinned address cannot be reached (connection refused, route unreachable, TLS handshake failed), the request is sent **once more** through the system DNS, and the address is remembered as spent so the later requests go straight to the system's resolution. **A wrong address never costs more than no address**: only a request that was never sent is sent again.
152
+ - **Unset means untouched**: with no `pinnedAddresses`, the plugin uses the system's own fetch and adds nothing to it.
153
+ - **An address that is not an address is ignored**: a typo (`1.2.3.256`, `010.1.1.1`, …) does not fail the request; it is ignored and the request uses the system DNS.
154
+
155
+ The settings page (**OpenCode Go → Fixed IP**) writes the address for the gateway's current host, refuses an invalid one on the spot, and says "using the system DNS" once the address has been found to be gone.
156
+
144
157
  ## How a model is resolved
145
158
 
146
159
  Four evidence levels decide how a model is called, first hit wins:
@@ -247,7 +260,7 @@ npm test # contract tests against a real local HTTP mock gateway
247
260
 
248
261
  **Order matters**: `npm run build` must come before `npm test` — `tests/pack-contents.spec.ts` walks every `exports` target, and whether `lib/` exists decides whether that check runs or is silently skipped (it does not fail, it just stops protecting you). `package.json` no longer carries `prepare`, so `npm ci` will not build for you; all three CI workflows run `npm run build` explicitly.
249
262
 
250
- The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, the error mapping for stream idle timeouts and aborts, the exit-code contract of the verification scripts, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest, and the READMEs' claims about the upstream match the upstream actually installed). That is **52 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
263
+ The suite covers configuration validation, the protocol ladder, gateway and online metadata, the catalog projection, the adapter on the wire (all three protocols), Remote contracts and services, session-header invariants, usage windows and the meter, the client controller and usage pill, both halves' mounting and registration, the error mapping for stream idle timeouts and aborts, the exit-code contract of the verification scripts, and several **structural invariants** (inject declarations match the source's reads, peer coverage holds both ways, the READMEs match the manifest, and the READMEs' claims about the upstream match the upstream actually installed). That is **56 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
251
264
 
252
265
  ## Releasing (maintainers)
253
266
 
package/README.md CHANGED
@@ -109,6 +109,8 @@ DSH 桌面端是 Electron 壳 + 完整的 dsh Web 应用,本插件对它同样
109
109
  some-legacy-model: null # 整条 null:该模型两个字段都显式回退到目录值
110
110
  modelProtocols: # 最后兜底:逐模型协议覆盖
111
111
  some-new-model: openai-responses # 合法值只有 anthropic-messages / openai-completions / openai-responses
112
+ pinnedAddresses: # 固定 IP:域名 → 地址(只对网关域名生效)
113
+ opencode.ai: 104.18.32.47 # 可写 IPv4 或 IPv6;不合法或未设置时走系统 DNS
112
114
  retryPolicy: # 可选:本路由的重试策略(由 dsh-llm-retry 执行)
113
115
  mode: normal # 也可用 always(对所有失败重试,maxRetries 被忽略)
114
116
  maxRetries: 2 # 默认 5
@@ -141,6 +143,17 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
141
143
 
142
144
  **重试**:`retryPolicy` 是可选的,不写就用宿主默认。写了就由可选的 `dsh-llm-retry` 插件执行,配置错误在写入时报错而不是等到失败发生。
143
145
 
146
+ **固定 IP**:`pinnedAddresses` 把网关域名固定到一个地址,用来接入更近的边缘节点(系统解析到的边缘有时离得很远,而每个请求都要为那段距离付费)。
147
+
148
+ - **只改解析,不改域名**:请求里的域名、Host 头、TLS 的 SNI 与证书校验全部照旧,只有 socket 连到哪个地址被换掉。因此该地址必须能服务这个域名——证书是发给 `opencode.ai` 的。
149
+ - **只对网关域名生效**:键必须等于 `baseURL` 的域名(默认 `opencode.ai`)。其它名字不在本插件的请求路径上,写了不生效。`models.dev` 与厂商文档始终走系统 DNS。
150
+ - **作用范围**:模型 API 请求、网关 listing(`/models`)、`/usage` 读取——这三类都是发往网关的请求。
151
+ - **地址失效自动回退**:固定地址连不上时(连接被拒、路由不可达、TLS 握手失败),请求会用系统 DNS **重试一次**,并把这个地址记为失效,之后的请求直接走系统 DNS。**填错 IP 不会比不填更糟**:只有"请求还没发出去"才重试,已经送达的请求不会发第二遍。
152
+ - **不设置就是原样**:`pinnedAddresses` 为空时,插件用的就是系统自己的 fetch,不加任何东西。
153
+ - **非法地址被忽略**:写错的 IP(`1.2.3.256`、`010.1.1.1` 等)不会让请求失败,只是被忽略并回退系统 DNS。
154
+
155
+ 设置页(**OpenCode Go → 固定 IP**)里可以直接填地址:填的是网关域名当前对应的那个键,非法地址当场拒绝,地址失效后页面会显示"已回退系统 DNS"。
156
+
144
157
  ## 模型是怎么判定的
145
158
 
146
159
  一个模型"怎么调"由四级证据决定,先命中先赢:
@@ -247,7 +260,7 @@ npm test # 契约测试:真实 HTTP mock 网关,断言请求头/
247
260
 
248
261
  **顺序有讲究**:`npm run build` 要排在 `npm test` 之前——`tests/pack-contents.spec.ts` 会遍历 `exports` 的每个目标,`lib/` 存在与否决定这条检查是执行还是被跳过(不报错,但会少一层保护)。`package.json` 已移除 `prepare`,因此 `npm ci` 不会再替你隐式构建;三个 CI workflow 都显式跑了 `npm run build`。
249
262
 
250
- 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **52 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
263
+ 测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **56 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
251
264
 
252
265
  ## 发布(维护者)
253
266
 
package/lib/client.js CHANGED
@@ -172,6 +172,41 @@ var catalogRemote = {
172
172
  ]
173
173
  };
174
174
 
175
+ // src/network/contract.ts
176
+ function parseNetworkReading(value) {
177
+ if (value === null || typeof value !== "object") throw new Error("invalid network reading");
178
+ const row = value;
179
+ const host = row["host"];
180
+ if (typeof host !== "string") throw new Error("invalid network reading");
181
+ const address = row["address"];
182
+ const failed = row["failed"];
183
+ return {
184
+ host,
185
+ ...typeof address === "string" && address.length > 0 ? { address } : {},
186
+ ...typeof failed === "boolean" ? { failed } : {}
187
+ };
188
+ }
189
+ var codec2 = {
190
+ mode: "strict",
191
+ typeSymbol: "@dan-ai-studio/dshopencodego#NetworkReading",
192
+ schema: { parse: parseNetworkReading },
193
+ create: () => ({ parse: parseNetworkReading })
194
+ };
195
+ var networkRemote = {
196
+ package: "@dan-ai-studio/dshopencodego",
197
+ descriptors: [
198
+ {
199
+ id: "@dan-ai-studio/dshopencodego#opencodeGoNetwork/readNetwork",
200
+ service: "opencodeGoNetwork",
201
+ namespace: "opencodeGoNetwork",
202
+ method: "readNetwork",
203
+ invocation: { kind: "direct" },
204
+ parameters: [],
205
+ result: codec2
206
+ }
207
+ ]
208
+ };
209
+
175
210
  // src/update/contract.ts
176
211
  function parseUpdateStatus(value) {
177
212
  if (value === null || typeof value !== "object") throw new Error("invalid update reading");
@@ -191,7 +226,7 @@ function parseUpdateStatus(value) {
191
226
  checked: row["checked"]
192
227
  };
193
228
  }
194
- var codec2 = (typeSymbol, parse) => ({
229
+ var codec3 = (typeSymbol, parse) => ({
195
230
  mode: "strict",
196
231
  typeSymbol,
197
232
  schema: { parse },
@@ -207,7 +242,7 @@ var updateRemote = {
207
242
  method: "readUpdate",
208
243
  invocation: { kind: "direct" },
209
244
  parameters: [],
210
- result: codec2("@dan-ai-studio/dshopencodego#UpdateStatus", parseUpdateStatus)
245
+ result: codec3("@dan-ai-studio/dshopencodego#UpdateStatus", parseUpdateStatus)
211
246
  },
212
247
  {
213
248
  id: "@dan-ai-studio/dshopencodego#opencodeGoUpdate/refreshUpdate",
@@ -216,7 +251,7 @@ var updateRemote = {
216
251
  method: "refreshUpdate",
217
252
  invocation: { kind: "direct" },
218
253
  parameters: [],
219
- result: codec2("@dan-ai-studio/dshopencodego#UpdateStatus", parseUpdateStatus)
254
+ result: codec3("@dan-ai-studio/dshopencodego#UpdateStatus", parseUpdateStatus)
220
255
  }
221
256
  ]
222
257
  };
@@ -282,7 +317,7 @@ function parseGoMeter(value) {
282
317
  })
283
318
  };
284
319
  }
285
- var codec3 = (typeSymbol, parse) => ({
320
+ var codec4 = (typeSymbol, parse) => ({
286
321
  mode: "strict",
287
322
  typeSymbol,
288
323
  schema: { parse },
@@ -298,7 +333,7 @@ var usageRemote = {
298
333
  method: "readWindows",
299
334
  invocation: { kind: "direct" },
300
335
  parameters: [],
301
- result: codec3("@dan-ai-studio/dshopencodego#GoUsageWindows", parseUsageWindows)
336
+ result: codec4("@dan-ai-studio/dshopencodego#GoUsageWindows", parseUsageWindows)
302
337
  },
303
338
  {
304
339
  id: "@dan-ai-studio/dshopencodego#opencodeGoUsage/readMeter",
@@ -307,7 +342,7 @@ var usageRemote = {
307
342
  method: "readMeter",
308
343
  invocation: { kind: "direct" },
309
344
  parameters: [],
310
- result: codec3("@dan-ai-studio/dshopencodego#GoMeter", parseGoMeter)
345
+ result: codec4("@dan-ai-studio/dshopencodego#GoMeter", parseGoMeter)
311
346
  }
312
347
  ]
313
348
  };
@@ -324,6 +359,15 @@ var en = {
324
359
  keySave: "Save key",
325
360
  keyClear: "Remove key",
326
361
  keySaved: "Saved",
362
+ pinTitle: "Fixed IP",
363
+ pinHint: "Send the gateway name to one address to reach a closer edge. The name still goes into the certificate, so the address has to serve this name; an address that cannot be reached falls back to the system DNS.",
364
+ pinNone: "System DNS",
365
+ pinFailed: "Unreachable \u2014 using the system DNS",
366
+ pinUnavailable: "The pin could not be read",
367
+ pinSave: "Save address",
368
+ pinClear: "Remove address",
369
+ pinInvalid: "That is not an IPv4 or IPv6 address",
370
+ pinPlaceholder: "IPv4 or IPv6 address",
327
371
  catalogTitle: "Models",
328
372
  catalogRefresh: "Refresh",
329
373
  catalogSelectAll: "Select all",
@@ -425,6 +469,15 @@ var zh = {
425
469
  keySave: "\u4FDD\u5B58 Key",
426
470
  keyClear: "\u79FB\u9664 Key",
427
471
  keySaved: "\u5DF2\u4FDD\u5B58",
472
+ pinTitle: "\u56FA\u5B9A IP",
473
+ pinHint: "\u628A\u7F51\u5173\u57DF\u540D\u56FA\u5B9A\u5230\u4E00\u4E2A\u5730\u5740\uFF0C\u4EE5\u63A5\u5165\u66F4\u8FD1\u7684\u8FB9\u7F18\u8282\u70B9\u3002\u8BC1\u4E66\u91CC\u4ECD\u7136\u662F\u57DF\u540D\uFF0C\u6240\u4EE5\u8BE5\u5730\u5740\u5FC5\u987B\u80FD\u670D\u52A1\u8FD9\u4E2A\u57DF\u540D\uFF1B\u5730\u5740\u4E0D\u53EF\u8FBE\u65F6\u4F1A\u81EA\u52A8\u56DE\u9000\u7CFB\u7EDF DNS\u3002",
474
+ pinNone: "\u4F7F\u7528\u7CFB\u7EDF DNS",
475
+ pinFailed: "\u5730\u5740\u4E0D\u53EF\u8FBE\uFF0C\u5DF2\u56DE\u9000\u7CFB\u7EDF DNS",
476
+ pinUnavailable: "\u65E0\u6CD5\u8BFB\u53D6\u56FA\u5B9A\u5730\u5740",
477
+ pinSave: "\u4FDD\u5B58\u5730\u5740",
478
+ pinClear: "\u6E05\u9664\u5730\u5740",
479
+ pinInvalid: "\u8FD9\u4E0D\u662F\u5408\u6CD5\u7684 IPv4 \u6216 IPv6 \u5730\u5740",
480
+ pinPlaceholder: "IPv4 \u6216 IPv6 \u5730\u5740",
428
481
  catalogTitle: "\u6A21\u578B",
429
482
  catalogRefresh: "\u5237\u65B0",
430
483
  catalogSelectAll: "\u5168\u9009",
@@ -543,6 +596,48 @@ function isModelEnabled(model, visibility) {
543
596
  return typeof model.recommended === "boolean" ? model.recommended : model.deprecated !== true;
544
597
  }
545
598
 
599
+ // src/client/address.ts
600
+ function isDottedQuad(value) {
601
+ const parts = value.split(".");
602
+ if (parts.length !== 4) return false;
603
+ return parts.every((part) => {
604
+ if (!/^(0|[1-9][0-9]{0,2})$/.test(part)) return false;
605
+ return Number(part) <= 255;
606
+ });
607
+ }
608
+ function isIPv6(value) {
609
+ if (!value.includes(":")) return false;
610
+ const zone = value.indexOf("%");
611
+ const address = zone === -1 ? value : value.slice(0, zone);
612
+ if (zone !== -1) {
613
+ const name = value.slice(zone + 1);
614
+ if (name.length === 0 || name.includes("%")) return false;
615
+ }
616
+ const halves = address.split("::");
617
+ if (halves.length > 2) return false;
618
+ const groups = (part) => part.length === 0 ? [] : part.split(":");
619
+ const head = groups(halves[0] ?? "");
620
+ const tail = halves.length === 2 ? groups(halves[1] ?? "") : [];
621
+ const all = [...head, ...tail];
622
+ let count = 0;
623
+ for (const [index, group] of all.entries()) {
624
+ if (group.includes(".")) {
625
+ if (index !== all.length - 1) return false;
626
+ if (!isDottedQuad(group)) return false;
627
+ count += 2;
628
+ continue;
629
+ }
630
+ if (!/^[0-9a-fA-F]{1,4}$/.test(group)) return false;
631
+ count += 1;
632
+ }
633
+ return halves.length === 2 ? count <= 7 : count === 8;
634
+ }
635
+ function isAddress(value) {
636
+ const trimmed = value.trim();
637
+ if (trimmed.length === 0) return false;
638
+ return isDottedQuad(trimmed) || isIPv6(trimmed);
639
+ }
640
+
546
641
  // src/client/model-view.ts
547
642
  var INITIAL_FILTER = {
548
643
  query: "",
@@ -624,6 +719,9 @@ var INITIAL = {
624
719
  keyConfigured: void 0,
625
720
  failure: void 0,
626
721
  writeFailure: void 0,
722
+ network: void 0,
723
+ networkFailure: void 0,
724
+ pinInvalid: false,
627
725
  saved: void 0,
628
726
  settings: void 0,
629
727
  writable: false,
@@ -638,9 +736,12 @@ function settingsOf(value) {
638
736
  const minutes = record["refreshMinutes"];
639
737
  if (visibility === null || typeof visibility !== "object" || typeof minutes !== "number") return void 0;
640
738
  const entries = Object.entries(visibility).filter(([, enabled]) => typeof enabled === "boolean");
739
+ const pins = record["pinnedAddresses"];
740
+ const pinEntries = pins !== null && typeof pins === "object" ? Object.entries(pins).filter(([, address]) => typeof address === "string") : [];
641
741
  return {
642
742
  modelVisibility: Object.fromEntries(entries),
643
- refreshMinutes: minutes
743
+ refreshMinutes: minutes,
744
+ pinnedAddresses: Object.fromEntries(pinEntries)
644
745
  };
645
746
  }
646
747
  var SectionController = class _SectionController {
@@ -665,6 +766,7 @@ var SectionController = class _SectionController {
665
766
  }
666
767
  void this.load(false);
667
768
  void this.loadKey();
769
+ void this.loadNetwork();
668
770
  void this.checkUpdate(false);
669
771
  }
670
772
  subscribe = (listener) => {
@@ -708,6 +810,70 @@ var SectionController = class _SectionController {
708
810
  this.set({ keyConfigured: void 0 });
709
811
  }
710
812
  }
813
+ /**
814
+ * Read the gateway's pin.
815
+ *
816
+ * The Host owns the answer: the address in force is not always the one in
817
+ * the configuration, because an address that was found to be gone leaves
818
+ * the requests on the system's own resolution.
819
+ */
820
+ async loadNetwork() {
821
+ const remote = this.services.remote.opencodeGoNetwork;
822
+ if (remote === void 0) return;
823
+ try {
824
+ const result = await remote.readNetwork();
825
+ if (!result.ok) throw result.error;
826
+ this.set({ network: result.value, networkFailure: void 0 });
827
+ } catch (error) {
828
+ this.set({
829
+ network: void 0,
830
+ networkFailure: error instanceof Error ? error.message : "the gateway pin could not be read"
831
+ });
832
+ }
833
+ }
834
+ /**
835
+ * Pin the gateway to one address.
836
+ *
837
+ * The address is checked before it is written: a typo that reached the
838
+ * Host would be ignored there, so the page would claim a pin that is not
839
+ * in force while the request quietly used the system's resolution.
840
+ * @param address - the address as typed.
841
+ */
842
+ async savePin(address) {
843
+ const scope = this.services.scope;
844
+ const current = this.state.settings;
845
+ const host = this.state.network?.host ?? "";
846
+ if (scope === void 0 || current === void 0 || !this.state.writable) return;
847
+ if (host.length === 0) return;
848
+ const trimmed = address.trim();
849
+ if (!isAddress(trimmed)) {
850
+ this.set({ pinInvalid: true });
851
+ return;
852
+ }
853
+ await this.write(
854
+ () => scope.set("pinnedAddresses", { ...current.pinnedAddresses, [host]: trimmed }),
855
+ () => {
856
+ this.set({ pinInvalid: false });
857
+ void this.loadNetwork();
858
+ }
859
+ );
860
+ }
861
+ /** Unpin the gateway; its name goes back to the system's own resolution. */
862
+ async clearPin() {
863
+ const scope = this.services.scope;
864
+ const current = this.state.settings;
865
+ const host = this.state.network?.host ?? "";
866
+ if (scope === void 0 || current === void 0 || !this.state.writable) return;
867
+ if (host.length === 0) return;
868
+ const rest = { ...current.pinnedAddresses };
869
+ delete rest[host];
870
+ await this.write(
871
+ () => scope.set("pinnedAddresses", rest),
872
+ () => {
873
+ void this.loadNetwork();
874
+ }
875
+ );
876
+ }
711
877
  /**
712
878
  * The update Remote.
713
879
  *
@@ -1121,6 +1287,7 @@ function updateFailureText(message, t) {
1121
1287
  function Section({ controller, t, getLocale }) {
1122
1288
  const state = (0, import_react.useSyncExternalStore)(controller.subscribe, controller.snapshot, controller.snapshot);
1123
1289
  const [keyDraft, setKeyDraft] = (0, import_react.useState)("");
1290
+ const [pinDraft, setPinDraft] = (0, import_react.useState)("");
1124
1291
  const [busy, setBusy] = (0, import_react.useState)(false);
1125
1292
  const act = (0, import_react.useCallback)(async (operation) => {
1126
1293
  setBusy(true);
@@ -1133,7 +1300,11 @@ function Section({ controller, t, getLocale }) {
1133
1300
  (0, import_react.useEffect)(() => {
1134
1301
  setKeyDraft("");
1135
1302
  }, [state.saved]);
1303
+ (0, import_react.useEffect)(() => {
1304
+ setPinDraft(state.network?.address ?? "");
1305
+ }, [state.network?.address]);
1136
1306
  const reading = state.reading;
1307
+ const network = state.network;
1137
1308
  const visibility = state.settings?.modelVisibility ?? {};
1138
1309
  const editable = state.writable && state.settings !== void 0;
1139
1310
  const rows = reading === void 0 ? [] : visibleModels(reading.models, state.filter, visibility, getLocale?.());
@@ -1192,6 +1363,59 @@ function Section({ controller, t, getLocale }) {
1192
1363
  )
1193
1364
  ] })
1194
1365
  ] }),
1366
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.block, children: [
1367
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.row, children: [
1368
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("pinTitle") }),
1369
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: section_default.mono, children: network?.host ?? "\u2014" }),
1370
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: network?.failed === true ? section_default.warn : section_default.hint, children: network === void 0 ? "\u2014" : network.failed === true ? t("pinFailed") : network.address === void 0 ? t("pinNone") : network.address })
1371
+ ] }),
1372
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: section_default.hint, children: t("pinHint") }),
1373
+ state.networkFailure !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: section_default.warn, children: `${t("pinUnavailable")}: ${state.networkFailure}` }),
1374
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.row, children: [
1375
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1376
+ "input",
1377
+ {
1378
+ className: section_default.input,
1379
+ type: "text",
1380
+ "aria-label": t("pinTitle"),
1381
+ placeholder: t("pinPlaceholder"),
1382
+ value: pinDraft,
1383
+ onChange: (event) => {
1384
+ setPinDraft(event.target.value);
1385
+ }
1386
+ }
1387
+ ),
1388
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1389
+ "button",
1390
+ {
1391
+ type: "button",
1392
+ className: section_default.button,
1393
+ disabled: !editable || busy || pinDraft.trim().length === 0,
1394
+ onClick: () => {
1395
+ void act(async () => {
1396
+ await controller.savePin(pinDraft);
1397
+ });
1398
+ },
1399
+ children: t("pinSave")
1400
+ }
1401
+ ),
1402
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1403
+ "button",
1404
+ {
1405
+ type: "button",
1406
+ className: section_default.button,
1407
+ disabled: !editable || busy || network?.address === void 0,
1408
+ onClick: () => {
1409
+ void act(async () => {
1410
+ await controller.clearPin();
1411
+ });
1412
+ },
1413
+ children: t("pinClear")
1414
+ }
1415
+ )
1416
+ ] }),
1417
+ state.pinInvalid && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: section_default.warn, children: t("pinInvalid") })
1418
+ ] }),
1195
1419
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.block, children: [
1196
1420
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: section_default.row, children: [
1197
1421
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("catalogTitle") }),
@@ -1412,7 +1636,7 @@ function failureOf(error) {
1412
1636
  }
1413
1637
  return { message: "", retainPrevious: false };
1414
1638
  }
1415
- var POLL_MS = 6e4;
1639
+ var POLL_MS = 5 * 6e4;
1416
1640
  var MAX_UNPRICED_IDS = 3;
1417
1641
  function formatUSD(value) {
1418
1642
  if (value === 0) return "$0.0000";
@@ -1581,7 +1805,12 @@ function apply(ctx) {
1581
1805
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dshopencodego: copy dictionaries");
1582
1806
  const mounted = ctx.remote.$mount({
1583
1807
  package: usageRemote.package,
1584
- descriptors: [...usageRemote.descriptors, ...catalogRemote.descriptors, ...updateRemote.descriptors]
1808
+ descriptors: [
1809
+ ...usageRemote.descriptors,
1810
+ ...catalogRemote.descriptors,
1811
+ ...updateRemote.descriptors,
1812
+ ...networkRemote.descriptors
1813
+ ]
1585
1814
  });
1586
1815
  ctx.effect(() => {
1587
1816
  let dispose;
@@ -1593,7 +1822,7 @@ function apply(ctx) {
1593
1822
  void dispose?.();
1594
1823
  };
1595
1824
  }, "dshopencodego: usage Remote");
1596
- ctx.inject(["configForms", "remote.opencodeGoCatalog", "remote.opencodeGoUpdate", "remote.credentials"], (scope) => {
1825
+ ctx.inject(["configForms", "remote.opencodeGoCatalog", "remote.opencodeGoUpdate", "remote.opencodeGoNetwork", "remote.credentials"], (scope) => {
1597
1826
  const forms = scope.get("configForms");
1598
1827
  const pluginManager = {};
1599
1828
  const controller = new SectionController({