@dan-ai-studio/dshopencodego 0.1.55 → 0.1.57
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 +16 -1
- package/README.md +16 -1
- package/lib/client.js +238 -9
- package/lib/index.js +31419 -167
- package/lib/types/adapter.d.ts +8 -0
- package/lib/types/catalog/constants.d.ts +5 -0
- package/lib/types/catalog/gateway.d.ts +6 -1
- package/lib/types/catalog/index.d.ts +18 -0
- package/lib/types/catalog/reading.d.ts +1 -1
- package/lib/types/config.d.ts +6 -0
- package/lib/types/network/contract.d.ts +32 -0
- package/lib/types/network/pinning.d.ts +97 -0
- package/lib/types/network/service.d.ts +27 -0
- package/lib/types/usage/service.d.ts +8 -0
- package/lib/types/usage/windows.d.ts +8 -0
- package/package.json +1 -1
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:
|
|
@@ -160,6 +173,8 @@ The gateway's `/v1/models` answers only *which* models exist, never *how* to cal
|
|
|
160
173
|
- a model the provider **retires** disappears on the next refresh;
|
|
161
174
|
- the bundled catalog is **frozen at the plugin's version** and affects only the quality of the "how to call it" decision, never *which* models exist.
|
|
162
175
|
|
|
176
|
+
When the gateway is **temporarily unavailable**, the plugin keeps the last listing that succeeded, marks it stale on the page, and retries in the background. The retry rate is governed by a **five-minute back-off** rather than by the TTL, so a gateway that comes back is picked up within five minutes; while it stays down it is asked once, not once per model request. The settings page's Recheck button revalidates at any time and is not bound by that back-off.
|
|
177
|
+
|
|
163
178
|
The only thing that waits on a plugin release is a missing wire quirk in the pi-ai catalog (DeepSeek's `thinkingFormat`, say) — the model itself is already usable.
|
|
164
179
|
|
|
165
180
|
## The model list in the settings page
|
|
@@ -247,7 +262,7 @@ npm test # contract tests against a real local HTTP mock gateway
|
|
|
247
262
|
|
|
248
263
|
**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
264
|
|
|
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 **
|
|
265
|
+
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 **57 test files** — a number `tests/readme-consistency.spec.ts` checks against the `tests/` directory, so it cannot drift.
|
|
251
266
|
|
|
252
267
|
## Releasing (maintainers)
|
|
253
268
|
|
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
|
一个模型"怎么调"由四级证据决定,先命中先赢:
|
|
@@ -160,6 +173,8 @@ API Key 通过 Harness 凭证库提供(引用名 `OPENCODE_GO_API_KEY`),
|
|
|
160
173
|
- 官方**下线**模型 → 下次刷新即消失;
|
|
161
174
|
- 内置目录**冻结**在插件版本里,仅影响"怎么调"的推断质量,不影响"有哪些"。
|
|
162
175
|
|
|
176
|
+
网关**临时不可用**时,插件保留上次成功获取的列表并在页面上标注陈旧,同时在后台重试:重试由**5 分钟的退避**控制,而不是等满一个 TTL,所以网关恢复后 5 分钟内列表就会更新;网关持续不可用时,它只被问一次,不会每次模型请求都去问一遍。设置页的「重新检查」随时可以强制重验,不受这个退避限制。
|
|
177
|
+
|
|
163
178
|
只有当 pi-ai 内置目录**缺了某个模型的线怪癖**(如 DeepSeek 的 `thinkingFormat`)时,才需要等插件同步 pi-ai 的新版本——而模型本身早已可用。
|
|
164
179
|
|
|
165
180
|
## 设置页里的模型列表
|
|
@@ -247,7 +262,7 @@ npm test # 契约测试:真实 HTTP mock 网关,断言请求头/
|
|
|
247
262
|
|
|
248
263
|
**顺序有讲究**:`npm run build` 要排在 `npm test` 之前——`tests/pack-contents.spec.ts` 会遍历 `exports` 的每个目标,`lib/` 存在与否决定这条检查是执行还是被跳过(不报错,但会少一层保护)。`package.json` 已移除 `prepare`,因此 `npm ci` 不会再替你隐式构建;三个 CI workflow 都显式跑了 `npm run build`。
|
|
249
264
|
|
|
250
|
-
测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **
|
|
265
|
+
测试覆盖配置校验、协议阶梯、网关与在线元数据、目录投影、适配器(三种协议的线上请求)、Remote 契约与服务、会话头不变量、用量窗口与计量、客户端控制器与用量组件、两半的挂载与登记、流空闲超时与中止的错误映射、验证脚本的退出码契约、以及几条**结构性不变量**(inject 声明与源码读取一致、peer 覆盖双向、README 与 manifest 一致、README 对上游的断言与实装的上游一致)。共 **57 个测试文件**——这个数字由 `tests/readme-consistency.spec.ts` 对着 `tests/` 目录校验,不会漂移。
|
|
251
266
|
|
|
252
267
|
## 发布(维护者)
|
|
253
268
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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") }),
|
|
@@ -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: [
|
|
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({
|