dsh-kylin-media 0.1.0 → 0.2.0
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 +16 -5
- package/client.js +379 -21
- package/entry.js +2 -2
- package/lib/gateway.js +58 -3
- package/lib/routes.js +97 -2
- package/lib/store.js +191 -6
- package/lib/tools.js +26 -8
- package/package.json +3 -2
- package/scripts/client-test.mjs +363 -0
- package/scripts/selftest.mjs +89 -4
- package/scripts/smoke.mjs +6 -0
- package/skills/kylin-media/SKILL.md +3 -2
package/README.md
CHANGED
|
@@ -50,13 +50,22 @@ npm publish # prepack 会自动跑 smoke
|
|
|
50
50
|
|
|
51
51
|
| 项 | 说明 |
|
|
52
52
|
|---|---|
|
|
53
|
-
| 网关地址 | 默认 `http://127.0.0.1:8288`;局域网用 `http://<主机IP>:8288`,frp /
|
|
53
|
+
| 网关地址 | 默认 `http://127.0.0.1:8288`;局域网用 `http://<主机IP>:8288`,frp / 反代填对外地址。编辑的是「当前线路」的地址 |
|
|
54
|
+
| 连接线路 | 可同时保存多条地址(本机 / 局域网 / frp 公网),点圆点切换当前线路,「测试连接」与所有 `media_*` 都用它;「全部测速」并发探测每条线路并标出最快可用的那条 |
|
|
54
55
|
| 访问 Key | 网关 `API_KEY_MODE=always` 时必填;保存后不再回显明文,留空表示沿用已保存的值 |
|
|
55
56
|
| 单次最多等待 | 工具调用内等待任务的秒数(10-600);超时不影响网关侧任务,可继续用 `media_job` |
|
|
56
57
|
| 产物目录 | 留空 = `<会话目录>/media-output/<任务 id>/`;也可填绝对路径 |
|
|
57
58
|
| 自动下载 | 关闭后只返回远端 URL,用 `media_job action=download` 取回 |
|
|
59
|
+
| 默认模型 | 「可用能力」卡片里逐类型列出网关实时目录中的模型,点选即设为该类型默认模型并立即保存;`media_*` 未传 `model` 时用它,没选则用该类型第一个可用模型 |
|
|
58
60
|
|
|
59
|
-
页面上的「测试连接」会读取 `/api/health` 与 `/api/models`,显示网关版本、鉴权模式、ComfyUI
|
|
61
|
+
页面上的「测试连接」会读取 `/api/health` 与 `/api/models`,显示网关版本、鉴权模式、ComfyUI 与双卡队列,并按能力类型列出具体模型(含可用性与权重缺失),可直接点选默认模型。连接失败时会给出排查提示。
|
|
62
|
+
|
|
63
|
+
> 常见坑:网关默认监听明文 HTTP(`API_PORT=8288`)。frp / 反代把 8288 映射出去后仍然是 HTTP,地址要写 `http://<对外地址>:<端口>`;写成 `https://` 会在 TLS 握手阶段被断开,错误信息里会提示「这个端口没有完成 TLS 握手…把地址改成 http:// 再试」。反过来,如果前面套了 nginx / Caddy 的 HTTPS,则必须写 `https://`(自签证书还需要让运行宿主信任该 CA)。
|
|
64
|
+
|
|
65
|
+
> 为什么「本机 / 局域网」地址连不上:
|
|
66
|
+
> 1. `127.0.0.1` / `localhost` 只有**网关和插件跑在同一台机器**时才通。网关在远端(例如 Windows 主机)而插件在 Mac 上时,本机地址必然被拒绝。
|
|
67
|
+
> 2. 局域网地址要求两者**在同一网段**,且网关监听 `HOST=0.0.0.0`(默认)、系统防火墙放行 8288(仓库里有 `add-firewall-rule.bat`)。可先用 `curl http://<主机IP>:8288/api/health` 验证。
|
|
68
|
+
> 3. 上面两条都不满足时只能用 frp / 公网地址。把三条地址都加进「连接线路」,点「全部测速」即可看出哪条通、哪条最快;失败原因直接写在行内(拒绝连接 / 不可达 / 域名解析失败 / 协议写反)。
|
|
60
69
|
|
|
61
70
|
## 工具
|
|
62
71
|
|
|
@@ -105,10 +114,12 @@ media_audio action=speech text="你好,这是本地语音合成。" format=mp3
|
|
|
105
114
|
|
|
106
115
|
```bash
|
|
107
116
|
cd dsh-kylin-media
|
|
108
|
-
npm test # smoke(包契约 + 客户端自注册 + locale/icon
|
|
117
|
+
npm test # smoke(包契约 + 客户端自注册 + locale/icon/技能)+ selftest(mock ctx + mock 网关)+ client-test(渲染自测)
|
|
109
118
|
```
|
|
110
119
|
|
|
111
|
-
`selftest` 会用临时 HOME 与 `127.0.0.1` 上的 mock 网关跑完整链路:设置持久化、403 / 400 / 404 / 405
|
|
120
|
+
`selftest` 会用临时 HOME 与 `127.0.0.1` 上的 mock 网关跑完整链路:设置持久化、403 / 400 / 404 / 405 围栏、默认模型选择与回退、提交轮询、产物下载、鉴权头、注册与卸载。
|
|
121
|
+
|
|
122
|
+
`client-test` 用最小 React 替身渲染设置页的「可用能力」卡片:类型卡片列出具体模型、默认模型高亮、点选后 POST `settings.update`、缺权重模型不可选,且渲染树不含明文 Key。
|
|
112
123
|
|
|
113
124
|
## 目录
|
|
114
125
|
|
|
@@ -126,7 +137,7 @@ dsh-kylin-media/
|
|
|
126
137
|
│ └── routes.js # fenced JSON API
|
|
127
138
|
├── skills/kylin-media/ # 随包技能
|
|
128
139
|
├── locale/ # 插件管理器展示元数据
|
|
129
|
-
└── scripts/ # smoke / selftest
|
|
140
|
+
└── scripts/ # smoke / selftest / client-test
|
|
130
141
|
```
|
|
131
142
|
|
|
132
143
|
## License
|
package/client.js
CHANGED
|
@@ -38,6 +38,24 @@ window.__ModuleLoader__.load({
|
|
|
38
38
|
testing: '测试中…',
|
|
39
39
|
clearKey: '清除 Key',
|
|
40
40
|
cleared: '已清除访问 Key',
|
|
41
|
+
lines: '连接线路',
|
|
42
|
+
linesHint: '点左侧圆点切换当前线路(测试与工具都用它);「测速」只查连通性。127.0.0.1 / localhost 只有当网关跑在同一台机器时才通,局域网地址要同网段且防火墙放行端口。',
|
|
43
|
+
lineAdd: '把当前地址加为线路',
|
|
44
|
+
lineTest: '测速',
|
|
45
|
+
linesUnavailable: '当前宿主版本还不支持线路管理:重启宿主(插件 0.2.0)后这里会显示可切换的线路。',
|
|
46
|
+
lineTestAll: '全部测速',
|
|
47
|
+
lineTesting: '测速中…',
|
|
48
|
+
lineFailed: '失败',
|
|
49
|
+
lineActive: '当前线路',
|
|
50
|
+
lineSwitch: '切换为当前线路',
|
|
51
|
+
lineRemove: '删除线路',
|
|
52
|
+
lineSwitched: '已切换到该线路',
|
|
53
|
+
lineAdded: '已添加线路',
|
|
54
|
+
lineRemoved: '已删除线路',
|
|
55
|
+
lineExists: '这个地址已经在列表里了',
|
|
56
|
+
lineKeepOne: '至少要保留一条线路',
|
|
57
|
+
noLineReachable: '所有线路都不通:检查网关是否运行、地址/端口是否正确、防火墙是否放行',
|
|
58
|
+
fastestLine: '最快线路:',
|
|
41
59
|
saved: '已保存',
|
|
42
60
|
failed: '操作失败:',
|
|
43
61
|
loadFailed: '配置加载失败,请确认插件已激活后重试。',
|
|
@@ -60,7 +78,13 @@ window.__ModuleLoader__.load({
|
|
|
60
78
|
noKey: '未配置访问 Key',
|
|
61
79
|
keyConfigured: '访问 Key 已配置',
|
|
62
80
|
capabilities: '可用能力',
|
|
63
|
-
capabilitiesSub: '来自网关 /api/models
|
|
81
|
+
capabilitiesSub: '来自网关 /api/models 的实时目录。点选一个模型即设为该能力的默认模型,media_* 工具没传 model 时用它;灰色表示权重缺失。',
|
|
82
|
+
modelsAuto: '自动(第一个可用模型)',
|
|
83
|
+
modelsStale: '已选:',
|
|
84
|
+
modelsEmpty: '没有注册模型',
|
|
85
|
+
missingWeights: '缺权重',
|
|
86
|
+
defaultSaved: '默认模型已更新',
|
|
87
|
+
defaultFailed: '默认模型保存失败:',
|
|
64
88
|
behavior: '任务与产物',
|
|
65
89
|
behaviorSub: '这些默认值对所有 media_* 工具生效。',
|
|
66
90
|
waitSeconds: '单次最多等待(秒)',
|
|
@@ -76,7 +100,7 @@ window.__ModuleLoader__.load({
|
|
|
76
100
|
available: '可用',
|
|
77
101
|
unprofiled: '未注册权重',
|
|
78
102
|
noCatalog: '尚未读取到能力目录,点「测试连接」重试。',
|
|
79
|
-
unreachableHint: '检查网关是否已启动、地址是否正确;若网关是 always 模式,还需填写正确的访问 Key。',
|
|
103
|
+
unreachableHint: '检查网关是否已启动、地址是否正确;若网关是 always 模式,还需填写正确的访问 Key。https:// 与 http:// 写反也会连不上(错误信息里会提示)。',
|
|
80
104
|
testFailed: '连接测试没有通过。',
|
|
81
105
|
keyRequired: '请先填写访问 Key(网关可能要求鉴权)。',
|
|
82
106
|
type_text2image: '文生图',
|
|
@@ -109,6 +133,24 @@ window.__ModuleLoader__.load({
|
|
|
109
133
|
testing: 'Testing…',
|
|
110
134
|
clearKey: 'Clear key',
|
|
111
135
|
cleared: 'Access key cleared',
|
|
136
|
+
lines: 'Connection lines',
|
|
137
|
+
linesHint: 'Click the dot to switch the active line (tests and tools use it). "Test" only checks reachability. 127.0.0.1 / localhost works only when the gateway runs on this machine; a LAN address needs the same subnet and an open firewall port.',
|
|
138
|
+
lineAdd: 'Add current address as a line',
|
|
139
|
+
lineTest: 'Test',
|
|
140
|
+
linesUnavailable: 'This host does not support connection lines yet: restart the host (plugin 0.2.0) to manage and switch lines here.',
|
|
141
|
+
lineTestAll: 'Test all',
|
|
142
|
+
lineTesting: 'Testing…',
|
|
143
|
+
lineFailed: 'failed',
|
|
144
|
+
lineActive: 'Active line',
|
|
145
|
+
lineSwitch: 'Switch to this line',
|
|
146
|
+
lineRemove: 'Remove line',
|
|
147
|
+
lineSwitched: 'Switched to this line',
|
|
148
|
+
lineAdded: 'Line added',
|
|
149
|
+
lineRemoved: 'Line removed',
|
|
150
|
+
lineExists: 'That address is already in the list',
|
|
151
|
+
lineKeepOne: 'Keep at least one line',
|
|
152
|
+
noLineReachable: 'No line is reachable: check the gateway process, address/port and firewall',
|
|
153
|
+
fastestLine: 'Fastest line: ',
|
|
112
154
|
saved: 'Saved',
|
|
113
155
|
failed: 'Operation failed: ',
|
|
114
156
|
loadFailed: 'Failed to load settings. Make sure the plugin is active, then retry.',
|
|
@@ -131,7 +173,13 @@ window.__ModuleLoader__.load({
|
|
|
131
173
|
noKey: 'No access key',
|
|
132
174
|
keyConfigured: 'Access key configured',
|
|
133
175
|
capabilities: 'Capabilities',
|
|
134
|
-
capabilitiesSub: 'Live catalogue from the gateway /api/models.
|
|
176
|
+
capabilitiesSub: 'Live catalogue from the gateway /api/models. Pick a model to make it the default for that capability; media_* tools use it when model is omitted. Grey means the weights are missing.',
|
|
177
|
+
modelsAuto: 'Auto (first available model)',
|
|
178
|
+
modelsStale: 'Selected: ',
|
|
179
|
+
modelsEmpty: 'No registered models',
|
|
180
|
+
missingWeights: 'weights missing',
|
|
181
|
+
defaultSaved: 'Default model updated',
|
|
182
|
+
defaultFailed: 'Failed to save the default model: ',
|
|
135
183
|
behavior: 'Jobs and artifacts',
|
|
136
184
|
behaviorSub: 'These defaults apply to every media_* tool.',
|
|
137
185
|
waitSeconds: 'Max wait per call (s)',
|
|
@@ -147,7 +195,7 @@ window.__ModuleLoader__.load({
|
|
|
147
195
|
available: 'available',
|
|
148
196
|
unprofiled: 'unprofiled weights',
|
|
149
197
|
noCatalog: 'No catalogue yet — run "Test connection" again.',
|
|
150
|
-
unreachableHint: 'Check that the gateway is running and the address is right; if it uses always mode, also check the access key.',
|
|
198
|
+
unreachableHint: 'Check that the gateway is running and the address is right; if it uses always mode, also check the access key. A mismatched https:// or http:// scheme also fails to connect (the error says which).',
|
|
151
199
|
testFailed: 'Connection test did not pass.',
|
|
152
200
|
keyRequired: 'Enter the access key first (the gateway may require auth).',
|
|
153
201
|
type_text2image: 'Text to image',
|
|
@@ -201,6 +249,20 @@ window.__ModuleLoader__.load({
|
|
|
201
249
|
'.km-hint{font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-tertiary,#8a919c)}',
|
|
202
250
|
'.km-keyrow{display:flex;gap:8px;align-items:stretch}',
|
|
203
251
|
'.km-keyrow input{flex:1;min-width:0}',
|
|
252
|
+
'.km-lines{display:flex;flex-direction:column;gap:6px;margin-top:8px}',
|
|
253
|
+
'.km-line{display:flex;align-items:center;gap:10px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.22));border-radius:var(--dsw-radius-sm,8px);padding:7px 10px;background:var(--dsw-alias-bg-layer-1,transparent);min-width:0}',
|
|
254
|
+
'.km-line[data-active=true]{border-color:var(--dsw-alias-brand-primary,#4c7dff);background:color-mix(in srgb, var(--dsw-alias-brand-primary,#4c7dff) 9%, transparent)}',
|
|
255
|
+
'.km-line-radio{appearance:none;flex:none;width:14px;height:14px;padding:0;border-radius:50%;border:1px solid var(--dsw-alias-border-l3,rgba(127,127,127,.45));background:transparent;cursor:pointer;position:relative}',
|
|
256
|
+
'.km-line-radio[data-active=true]{border-color:var(--dsw-alias-brand-primary,#4c7dff)}',
|
|
257
|
+
'.km-line-radio[data-active=true]::after{content:"";position:absolute;inset:2px;border-radius:50%;background:var(--dsw-alias-brand-primary,#4c7dff)}',
|
|
258
|
+
'.km-line-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}',
|
|
259
|
+
'.km-line-name{font-size:12px;color:var(--dsw-alias-label-primary,#1f2329)}',
|
|
260
|
+
'.km-line-url{font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);font-size:11px;color:var(--dsw-alias-label-tertiary,#8a919c);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
261
|
+
'.km-line-status{flex:none;font-size:11px;color:var(--dsw-alias-label-tertiary,#8a919c);white-space:nowrap}',
|
|
262
|
+
'.km-line-status[data-state=ok]{color:var(--dsw-alias-state-success-primary,#2f8f5b)}',
|
|
263
|
+
'.km-line-status[data-state=error]{color:var(--dsw-alias-state-error-primary,#d9534f)}',
|
|
264
|
+
'.km-line-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}',
|
|
265
|
+
'.km-btn-mini{padding:4px 9px;font-size:11.5px;line-height:1.2}',
|
|
204
266
|
'.km-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:18px;padding-top:14px;border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.12))}',
|
|
205
267
|
'.km-btn{appearance:none;display:inline-flex;align-items:center;gap:6px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25));background:transparent;color:var(--dsw-alias-label-primary,#1f2329);border-radius:var(--dsw-radius-sm,8px);padding:7px 14px;font:inherit;font-size:13px;line-height:1.2;cursor:pointer;transition:background-color .16s ease,border-color .16s ease,color .16s ease}',
|
|
206
268
|
'.km-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.07));border-color:var(--dsw-alias-border-l3,rgba(127,127,127,.4))}',
|
|
@@ -216,11 +278,25 @@ window.__ModuleLoader__.load({
|
|
|
216
278
|
'.km-state{margin-top:14px;font-size:12.5px;line-height:1.75;color:var(--dsw-alias-label-secondary,#5c6470);padding:10px 12px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.12));border-left:3px solid var(--dsw-alias-border-l3,rgba(127,127,127,.3));border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-alias-bg-layer-1,transparent)}',
|
|
217
279
|
'.km-state[data-kind=ok]{border-left-color:var(--dsw-alias-state-success-primary,#2f8f5b)}',
|
|
218
280
|
'.km-state[data-kind=error]{color:var(--dsw-alias-state-error-primary,#d9534f);border-left-color:var(--dsw-alias-state-error-primary,#d9534f)}',
|
|
219
|
-
'.km-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(
|
|
281
|
+
'.km-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px}',
|
|
220
282
|
'.km-tile{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.22));border-radius:var(--dsw-radius-sm,10px);padding:11px 12px;background:var(--dsw-alias-bg-layer-1,transparent);display:flex;flex-direction:column;gap:5px;min-width:0}',
|
|
221
283
|
'.km-tile strong{font-size:12.5px;font-weight:600;color:var(--dsw-alias-label-primary,#1f2329)}',
|
|
222
|
-
'.km-tile span{font-size:11.5px;color:var(--dsw-alias-label-tertiary,#8a919c)}',
|
|
284
|
+
'.km-tile>span,.km-tile-head span{font-size:11.5px;color:var(--dsw-alias-label-tertiary,#8a919c)}',
|
|
223
285
|
'.km-tile[data-state=missing]{opacity:.55}',
|
|
286
|
+
'.km-tile-head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}',
|
|
287
|
+
'.km-tile-models{gap:8px}',
|
|
288
|
+
'.km-models{display:flex;flex-direction:column;gap:1px}',
|
|
289
|
+
'.km-model{appearance:none;display:flex;align-items:center;gap:7px;width:100%;box-sizing:border-box;background:transparent;border:1px solid transparent;border-radius:var(--dsw-radius-sm,8px);padding:4px 6px;font:inherit;font-size:11.5px;line-height:1.45;color:var(--dsw-alias-label-secondary,#5c6470);text-align:left;cursor:pointer;transition:background-color .14s ease,border-color .14s ease,color .14s ease}',
|
|
290
|
+
'.km-model:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.07))}',
|
|
291
|
+
'.km-model:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#4c7dff);outline-offset:1px}',
|
|
292
|
+
'.km-model[data-selected=true]{border-color:var(--dsw-alias-brand-primary,#4c7dff);color:var(--dsw-alias-label-primary,#1f2329);background:color-mix(in srgb, var(--dsw-alias-brand-primary,#4c7dff) 12%, transparent)}',
|
|
293
|
+
'.km-model[data-disabled=true]{opacity:.5;cursor:not-allowed}',
|
|
294
|
+
'.km-model-radio{width:11px;height:11px;flex:none;border-radius:50%;border:1px solid var(--dsw-alias-border-l3,rgba(127,127,127,.45));position:relative}',
|
|
295
|
+
'.km-model[data-selected=true] .km-model-radio{border-color:var(--dsw-alias-brand-primary,#4c7dff)}',
|
|
296
|
+
'.km-model[data-selected=true] .km-model-radio::after{content:"";position:absolute;inset:2px;border-radius:50%;background:var(--dsw-alias-brand-primary,#4c7dff)}',
|
|
297
|
+
'.km-model-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11.5px;color:inherit}',
|
|
298
|
+
'.km-model-badge{flex:none;font-size:10px;line-height:1;padding:2px 5px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.3));color:var(--dsw-alias-label-tertiary,#8a919c)}',
|
|
299
|
+
'.km-model-empty,.km-model-stale{font-size:11px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#8a919c)}',
|
|
224
300
|
'.km-check{display:flex;align-items:flex-start;gap:8px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-secondary,#5c6470);cursor:pointer}',
|
|
225
301
|
'.km-check input{width:15px;height:15px;margin:1px 0 0;accent-color:var(--dsw-alias-brand-primary,#4c7dff);flex:none;cursor:pointer}',
|
|
226
302
|
'.km-foot{margin:0;color:var(--dsw-alias-label-tertiary,#8a919c);font-size:11.5px;line-height:1.7}',
|
|
@@ -233,14 +309,24 @@ window.__ModuleLoader__.load({
|
|
|
233
309
|
return function () { if (style.parentNode) style.parentNode.removeChild(style); };
|
|
234
310
|
}
|
|
235
311
|
|
|
312
|
+
/* 写操作一律 POST:路由只放行 GET 的 state,其余动作 GET 会被 405 拒绝,
|
|
313
|
+
所以这里绝不按「有没有 body」决定方法。 */
|
|
236
314
|
function request(action, body) {
|
|
237
|
-
var
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
|
|
315
|
+
var payload = body === undefined ? {} : body;
|
|
316
|
+
return fetch(API + '/' + action, {
|
|
317
|
+
method: 'POST',
|
|
318
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
319
|
+
body: JSON.stringify(payload)
|
|
320
|
+
}).then(function (response) {
|
|
321
|
+
return response.json().catch(function () {
|
|
322
|
+
return { ok: false, error: { code: 'http-' + response.status, message: 'HTTP ' + response.status } };
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* 只读查询(目前只有 state 允许 GET)。 */
|
|
328
|
+
function query(action) {
|
|
329
|
+
return fetch(API + '/' + action, { headers: { accept: 'application/json' } }).then(function (response) {
|
|
244
330
|
return response.json().catch(function () {
|
|
245
331
|
return { ok: false, error: { code: 'http-' + response.status, message: 'HTTP ' + response.status } };
|
|
246
332
|
});
|
|
@@ -282,10 +368,188 @@ window.__ModuleLoader__.load({
|
|
|
282
368
|
var busyHook = React.useState(false);
|
|
283
369
|
var busy = busyHook[0];
|
|
284
370
|
var setBusy = busyHook[1];
|
|
371
|
+
var defsHook = React.useState({});
|
|
372
|
+
var defaults = defsHook[0];
|
|
373
|
+
var setDefaults = defsHook[1];
|
|
374
|
+
var modelNoticeHook = React.useState(null);
|
|
375
|
+
var modelNotice = modelNoticeHook[0];
|
|
376
|
+
var setModelNotice = modelNoticeHook[1];
|
|
377
|
+
var linesHook = React.useState({});
|
|
378
|
+
var lineResults = linesHook[0];
|
|
379
|
+
var setLineResults = linesHook[1];
|
|
380
|
+
var linesBusyHook = React.useState(false);
|
|
381
|
+
var linesBusy = linesBusyHook[0];
|
|
382
|
+
var setLinesBusy = linesBusyHook[1];
|
|
383
|
+
|
|
384
|
+
/* 点选即保存:{ type: modelId },空串表示回到「自动」。 */
|
|
385
|
+
function selectModel(type, modelId) {
|
|
386
|
+
var previous = defaults;
|
|
387
|
+
var next = Object.assign({}, defaults);
|
|
388
|
+
if (modelId === '') delete next[type];
|
|
389
|
+
else next[type] = modelId;
|
|
390
|
+
setDefaults(next);
|
|
391
|
+
setModelNotice(null);
|
|
392
|
+
request('settings.update', { patch: { defaultModels: next } }).then(function (payload) {
|
|
393
|
+
if (!payload.ok) throw new Error(payloadError(payload));
|
|
394
|
+
setSnapshot(function (current) {
|
|
395
|
+
if (current === null || current.data === null || current.data === undefined) return current;
|
|
396
|
+
return Object.assign({}, current, {
|
|
397
|
+
data: Object.assign({}, current.data, { defaultModels: next })
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
setModelNotice({ kind: 'ok', text: t('defaultSaved') });
|
|
401
|
+
}).catch(function (error) {
|
|
402
|
+
setDefaults(previous);
|
|
403
|
+
setModelNotice({ kind: 'error', text: t('defaultFailed') + (error && error.message ? error.message : String(error)) });
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function hostOf(url) {
|
|
408
|
+
try { return new URL(url).host || url; } catch (error) { return url; }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function setLineResult(id, value) {
|
|
412
|
+
setLineResults(function (previous) {
|
|
413
|
+
var next = Object.assign({}, previous);
|
|
414
|
+
next[id] = value;
|
|
415
|
+
return next;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* 线路相关的接口都会返回最新 publicState:直接落到快照与地址输入框。 */
|
|
420
|
+
function applyState(state) {
|
|
421
|
+
setSnapshot({ status: 'ready', data: state, error: null });
|
|
422
|
+
setDefaults(state.defaultModels || {});
|
|
423
|
+
setDraft(function (previous) {
|
|
424
|
+
if (previous === null) {
|
|
425
|
+
return {
|
|
426
|
+
baseUrl: state.baseUrl,
|
|
427
|
+
waitSeconds: String(state.waitSeconds),
|
|
428
|
+
outputDir: state.outputDir,
|
|
429
|
+
autoDownload: state.autoDownload
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return Object.assign({}, previous, { baseUrl: state.baseUrl });
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function selectEndpoint(id) {
|
|
437
|
+
if (busy || linesBusy) return;
|
|
438
|
+
setNotice(null);
|
|
439
|
+
request('endpoints.select', { id: id }).then(function (payload) {
|
|
440
|
+
if (!payload.ok) throw new Error(payloadError(payload));
|
|
441
|
+
applyState(payload.value);
|
|
442
|
+
setNotice({ kind: 'ok', text: t('lineSwitched') });
|
|
443
|
+
runTest();
|
|
444
|
+
}).catch(function (error) {
|
|
445
|
+
setNotice({ kind: 'error', text: t('failed') + (error && error.message ? error.message : String(error)) });
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function probeLine(id) {
|
|
450
|
+
setLineResult(id, { status: 'loading' });
|
|
451
|
+
request('endpoints.probe', { id: id }).then(function (payload) {
|
|
452
|
+
if (!payload.ok) throw new Error(payloadError(payload));
|
|
453
|
+
var value = payload.value;
|
|
454
|
+
setLineResult(id, value.reachable
|
|
455
|
+
? { status: 'ok', latencyMs: value.latencyMs, message: '' }
|
|
456
|
+
: { status: 'error', latencyMs: value.latencyMs, message: value.error ? value.error.message : '' });
|
|
457
|
+
}).catch(function (error) {
|
|
458
|
+
setLineResult(id, { status: 'error', message: error && error.message ? error.message : String(error) });
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function currentLines() {
|
|
463
|
+
return Array.isArray(data.endpoints) ? data.endpoints : [];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function probeAll() {
|
|
467
|
+
if (linesBusy) return;
|
|
468
|
+
var lines = currentLines();
|
|
469
|
+
if (lines.length === 0) return;
|
|
470
|
+
setLinesBusy(true);
|
|
471
|
+
setNotice(null);
|
|
472
|
+
var pending = {};
|
|
473
|
+
for (var index = 0; index < lines.length; index += 1) pending[lines[index].id] = { status: 'loading' };
|
|
474
|
+
setLineResults(pending);
|
|
475
|
+
request('endpoints.probeAll', {}).then(function (payload) {
|
|
476
|
+
if (!payload.ok) throw new Error(payloadError(payload));
|
|
477
|
+
var next = {};
|
|
478
|
+
var best = null;
|
|
479
|
+
for (var probe = 0; probe < payload.value.results.length; probe += 1) {
|
|
480
|
+
var result = payload.value.results[probe];
|
|
481
|
+
next[result.id] = result.reachable
|
|
482
|
+
? { status: 'ok', latencyMs: result.latencyMs, message: '' }
|
|
483
|
+
: { status: 'error', latencyMs: result.latencyMs, message: result.error ? result.error.message : '' };
|
|
484
|
+
if (result.reachable && (best === null || result.latencyMs < best.latencyMs)) best = result;
|
|
485
|
+
}
|
|
486
|
+
setLineResults(next);
|
|
487
|
+
if (best === null) setNotice({ kind: 'error', text: t('noLineReachable') });
|
|
488
|
+
else setNotice({ kind: 'ok', text: t('fastestLine') + best.name + ' · ' + String(best.latencyMs) + 'ms' });
|
|
489
|
+
}).catch(function (error) {
|
|
490
|
+
setNotice({ kind: 'error', text: t('failed') + (error && error.message ? error.message : String(error)) });
|
|
491
|
+
}).then(function () {
|
|
492
|
+
setLinesBusy(false);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function addLine() {
|
|
497
|
+
if (linesBusy || draft === null) return;
|
|
498
|
+
var url = String(draft.baseUrl || '').trim().replace(/\/+$/, '');
|
|
499
|
+
if (url === '') return;
|
|
500
|
+
var lines = currentLines();
|
|
501
|
+
for (var index = 0; index < lines.length; index += 1) {
|
|
502
|
+
if (lines[index].baseUrl === url) {
|
|
503
|
+
setNotice({ kind: 'error', text: t('lineExists') });
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
setLinesBusy(true);
|
|
508
|
+
setNotice(null);
|
|
509
|
+
request('settings.update', { patch: { endpoints: lines.concat([{ name: hostOf(url), baseUrl: url }]) } }).then(function (payload) {
|
|
510
|
+
if (!payload.ok) throw new Error(payloadError(payload));
|
|
511
|
+
applyState(payload.value);
|
|
512
|
+
setNotice({ kind: 'ok', text: t('lineAdded') });
|
|
513
|
+
}).catch(function (error) {
|
|
514
|
+
setNotice({ kind: 'error', text: t('failed') + (error && error.message ? error.message : String(error)) });
|
|
515
|
+
}).then(function () {
|
|
516
|
+
setLinesBusy(false);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function removeLine(id) {
|
|
521
|
+
if (linesBusy) return;
|
|
522
|
+
var lines = currentLines();
|
|
523
|
+
var kept = [];
|
|
524
|
+
var removedActive = false;
|
|
525
|
+
for (var index = 0; index < lines.length; index += 1) {
|
|
526
|
+
if (lines[index].id === id) {
|
|
527
|
+
removedActive = lines[index].id === data.activeEndpointId;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
kept.push(lines[index]);
|
|
531
|
+
}
|
|
532
|
+
if (kept.length === 0) {
|
|
533
|
+
setNotice({ kind: 'error', text: t('lineKeepOne') });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
setLinesBusy(true);
|
|
537
|
+
setNotice(null);
|
|
538
|
+
request('settings.update', { patch: { endpoints: kept } }).then(function (payload) {
|
|
539
|
+
if (!payload.ok) throw new Error(payloadError(payload));
|
|
540
|
+
applyState(payload.value);
|
|
541
|
+
setNotice({ kind: 'ok', text: t('lineRemoved') });
|
|
542
|
+
if (removedActive) runTest();
|
|
543
|
+
}).catch(function (error) {
|
|
544
|
+
setNotice({ kind: 'error', text: t('failed') + (error && error.message ? error.message : String(error)) });
|
|
545
|
+
}).then(function () {
|
|
546
|
+
setLinesBusy(false);
|
|
547
|
+
});
|
|
548
|
+
}
|
|
285
549
|
|
|
286
550
|
function runTest() {
|
|
287
551
|
setTest({ status: 'loading', data: null, error: null });
|
|
288
|
-
request('test').then(function (payload) {
|
|
552
|
+
request('test', {}).then(function (payload) {
|
|
289
553
|
if (!payload.ok) throw new Error(payloadError(payload));
|
|
290
554
|
setTest({ status: 'ready', data: payload.value, error: null });
|
|
291
555
|
}).catch(function (error) {
|
|
@@ -295,9 +559,10 @@ window.__ModuleLoader__.load({
|
|
|
295
559
|
|
|
296
560
|
function load() {
|
|
297
561
|
setSnapshot({ status: 'loading', data: null, error: null });
|
|
298
|
-
|
|
562
|
+
query('state').then(function (payload) {
|
|
299
563
|
if (!payload.ok) throw new Error(payloadError(payload));
|
|
300
564
|
setSnapshot({ status: 'ready', data: payload.value, error: null });
|
|
565
|
+
setDefaults(payload.value.defaultModels || {});
|
|
301
566
|
setDraft({
|
|
302
567
|
baseUrl: payload.value.baseUrl,
|
|
303
568
|
waitSeconds: String(payload.value.waitSeconds),
|
|
@@ -342,10 +607,11 @@ window.__ModuleLoader__.load({
|
|
|
342
607
|
}).then(function (payload) {
|
|
343
608
|
if (payload !== null && !payload.ok) throw new Error(payloadError(payload));
|
|
344
609
|
setKeyInput('');
|
|
345
|
-
return
|
|
610
|
+
return query('state');
|
|
346
611
|
}).then(function (payload) {
|
|
347
612
|
if (!payload.ok) throw new Error(payloadError(payload));
|
|
348
613
|
setSnapshot({ status: 'ready', data: payload.value, error: null });
|
|
614
|
+
setDefaults(payload.value.defaultModels || {});
|
|
349
615
|
setNotice({ kind: 'ok', text: t('saved') });
|
|
350
616
|
runTest();
|
|
351
617
|
}).catch(function (error) {
|
|
@@ -359,7 +625,7 @@ window.__ModuleLoader__.load({
|
|
|
359
625
|
if (busy) return;
|
|
360
626
|
setBusy(true);
|
|
361
627
|
setNotice(null);
|
|
362
|
-
request('key.clear').then(function (payload) {
|
|
628
|
+
request('key.clear', {}).then(function (payload) {
|
|
363
629
|
if (!payload.ok) throw new Error(payloadError(payload));
|
|
364
630
|
setKeyInput('');
|
|
365
631
|
setSnapshot({ status: 'ready', data: payload.value, error: null });
|
|
@@ -394,6 +660,7 @@ window.__ModuleLoader__.load({
|
|
|
394
660
|
}
|
|
395
661
|
}
|
|
396
662
|
|
|
663
|
+
var endpoints = Array.isArray(data.endpoints) ? data.endpoints : null;
|
|
397
664
|
var connectionCard = React.createElement('section', { className: 'km-card' },
|
|
398
665
|
React.createElement('div', { className: 'km-card-head' },
|
|
399
666
|
React.createElement('h3', null, t('connection')),
|
|
@@ -429,7 +696,61 @@ window.__ModuleLoader__.load({
|
|
|
429
696
|
onClick: clearKey,
|
|
430
697
|
disabled: busy
|
|
431
698
|
}, t('clearKey')) : null),
|
|
432
|
-
React.createElement('span', { className: 'km-hint' }, t('keyHint')))
|
|
699
|
+
React.createElement('span', { className: 'km-hint' }, t('keyHint'))),
|
|
700
|
+
React.createElement('div', { className: 'km-field km-span2' },
|
|
701
|
+
React.createElement('label', null, t('lines')),
|
|
702
|
+
React.createElement('span', { className: 'km-hint' }, t('linesHint')),
|
|
703
|
+
endpoints === null ? React.createElement('span', { className: 'km-hint' }, t('linesUnavailable')) : null,
|
|
704
|
+
React.createElement('div', { className: 'km-lines' },
|
|
705
|
+
(endpoints || []).map(function (endpoint) {
|
|
706
|
+
var active = endpoint.id === data.activeEndpointId;
|
|
707
|
+
var result = lineResults[endpoint.id] || { status: 'idle' };
|
|
708
|
+
var statusText = result.status === 'loading' ? t('lineTesting')
|
|
709
|
+
: result.status === 'ok' ? String(result.latencyMs) + 'ms'
|
|
710
|
+
: result.status === 'error' ? t('lineFailed') : t('idle');
|
|
711
|
+
return React.createElement('div', {
|
|
712
|
+
key: endpoint.id,
|
|
713
|
+
className: 'km-line',
|
|
714
|
+
'data-active': active ? 'true' : 'false'
|
|
715
|
+
},
|
|
716
|
+
React.createElement('button', {
|
|
717
|
+
type: 'button',
|
|
718
|
+
className: 'km-line-radio',
|
|
719
|
+
'data-active': active ? 'true' : 'false',
|
|
720
|
+
title: active ? t('lineActive') : t('lineSwitch'),
|
|
721
|
+
onClick: function () { selectEndpoint(endpoint.id); }
|
|
722
|
+
}),
|
|
723
|
+
React.createElement('div', { className: 'km-line-main' },
|
|
724
|
+
React.createElement('div', { className: 'km-line-name' }, endpoint.name),
|
|
725
|
+
React.createElement('div', { className: 'km-line-url' }, endpoint.baseUrl)),
|
|
726
|
+
React.createElement('span', { className: 'km-line-status', 'data-state': result.status, title: result.message || '' }, statusText),
|
|
727
|
+
React.createElement('button', {
|
|
728
|
+
type: 'button',
|
|
729
|
+
className: 'km-btn km-btn-mini',
|
|
730
|
+
onClick: function () { probeLine(endpoint.id); },
|
|
731
|
+
disabled: result.status === 'loading'
|
|
732
|
+
}, t('lineTest')),
|
|
733
|
+
React.createElement('button', {
|
|
734
|
+
type: 'button',
|
|
735
|
+
className: 'km-btn km-btn-mini km-btn-danger',
|
|
736
|
+
onClick: function () { removeLine(endpoint.id); },
|
|
737
|
+
disabled: linesBusy || endpoints === null || endpoints.length <= 1,
|
|
738
|
+
title: t('lineRemove')
|
|
739
|
+
}, '×'));
|
|
740
|
+
})),
|
|
741
|
+
React.createElement('div', { className: 'km-line-actions' },
|
|
742
|
+
React.createElement('button', {
|
|
743
|
+
type: 'button',
|
|
744
|
+
className: 'km-btn km-btn-mini',
|
|
745
|
+
onClick: addLine,
|
|
746
|
+
disabled: linesBusy
|
|
747
|
+
}, t('lineAdd')),
|
|
748
|
+
React.createElement('button', {
|
|
749
|
+
type: 'button',
|
|
750
|
+
className: 'km-btn km-btn-mini',
|
|
751
|
+
onClick: probeAll,
|
|
752
|
+
disabled: linesBusy
|
|
753
|
+
}, linesBusy ? t('testing') : t('lineTestAll'))))),
|
|
433
754
|
|
|
434
755
|
React.createElement('div', { className: 'km-actions' },
|
|
435
756
|
React.createElement('button', {
|
|
@@ -471,17 +792,54 @@ window.__ModuleLoader__.load({
|
|
|
471
792
|
var capabilitiesCard = React.createElement('section', { className: 'km-card' },
|
|
472
793
|
React.createElement('h3', null, t('capabilities')),
|
|
473
794
|
React.createElement('p', { className: 'km-card-sub' }, t('capabilitiesSub')),
|
|
795
|
+
modelNotice !== null ? React.createElement('p', { className: 'km-notice', 'data-kind': modelNotice.kind, style: { margin: '0 0 10px' } }, modelNotice.text) : null,
|
|
474
796
|
React.createElement('div', { className: 'km-tiles' },
|
|
475
797
|
TYPE_ORDER.map(function (type) {
|
|
476
798
|
var entry = catalogByType[type];
|
|
799
|
+
var hasModelList = entry !== undefined && entry !== null && Array.isArray(entry.models);
|
|
800
|
+
var models = hasModelList ? entry.models : [];
|
|
801
|
+
var chosen = defaults[type] === undefined ? '' : defaults[type];
|
|
477
802
|
var count = entry ? String(entry.available) + '/' + String(entry.count) + ' ' + t('available') : (catalog ? '—' : t('idle'));
|
|
803
|
+
var chosenKnown = false;
|
|
804
|
+
for (var probe = 0; probe < models.length; probe += 1) if (models[probe].id === chosen) chosenKnown = true;
|
|
478
805
|
return React.createElement('div', {
|
|
479
806
|
key: type,
|
|
480
|
-
className: 'km-tile',
|
|
807
|
+
className: 'km-tile' + (models.length > 0 ? ' km-tile-models' : ''),
|
|
481
808
|
'data-state': entry && entry.available === 0 ? 'missing' : 'ready'
|
|
482
809
|
},
|
|
483
|
-
React.createElement('
|
|
484
|
-
|
|
810
|
+
React.createElement('div', { className: 'km-tile-head' },
|
|
811
|
+
React.createElement('strong', null, t('type_' + type)),
|
|
812
|
+
React.createElement('span', null, count)),
|
|
813
|
+
models.length === 0
|
|
814
|
+
? (hasModelList ? React.createElement('span', { className: 'km-model-empty' }, t('modelsEmpty')) : null)
|
|
815
|
+
: React.createElement('div', { className: 'km-models' },
|
|
816
|
+
React.createElement('button', {
|
|
817
|
+
type: 'button',
|
|
818
|
+
className: 'km-model',
|
|
819
|
+
'data-selected': chosen === '' ? 'true' : 'false',
|
|
820
|
+
onClick: function () { selectModel(type, ''); }
|
|
821
|
+
},
|
|
822
|
+
React.createElement('span', { className: 'km-model-radio' }),
|
|
823
|
+
React.createElement('span', { className: 'km-model-name' }, t('modelsAuto'))),
|
|
824
|
+
chosen !== '' && !chosenKnown ? React.createElement('span', { className: 'km-model-stale' }, t('modelsStale') + chosen) : null,
|
|
825
|
+
models.map(function (model) {
|
|
826
|
+
var unavailable = model.available !== true;
|
|
827
|
+
return React.createElement('button', {
|
|
828
|
+
key: model.id,
|
|
829
|
+
type: 'button',
|
|
830
|
+
className: 'km-model',
|
|
831
|
+
'data-selected': chosen === model.id ? 'true' : 'false',
|
|
832
|
+
'data-disabled': unavailable ? 'true' : 'false',
|
|
833
|
+
disabled: unavailable,
|
|
834
|
+
title: model.id,
|
|
835
|
+
onClick: function () { selectModel(type, model.id); }
|
|
836
|
+
},
|
|
837
|
+
React.createElement('span', { className: 'km-model-radio' }),
|
|
838
|
+
React.createElement('span', { className: 'km-model-name' }, model.name),
|
|
839
|
+
unavailable ? React.createElement('span', { className: 'km-model-badge' }, t('missingWeights')) : null);
|
|
840
|
+
})
|
|
841
|
+
)
|
|
842
|
+
);
|
|
485
843
|
})),
|
|
486
844
|
catalog === null ? React.createElement('p', { className: 'km-hint', style: { margin: '12px 0 0' } }, t('noCatalog')) : null,
|
|
487
845
|
catalog && catalog.unprofiled > 0 ? React.createElement('p', { className: 'km-hint', style: { margin: '8px 0 0' } }, String(catalog.unprofiled) + ' ' + t('unprofiled')) : null
|
package/entry.js
CHANGED
|
@@ -17,11 +17,11 @@ import { createTools } from './lib/tools.js'
|
|
|
17
17
|
|
|
18
18
|
export const name = 'dsh-kylin-media'
|
|
19
19
|
export const inject = ['tools', 'webServer']
|
|
20
|
-
export const VERSION = '0.
|
|
20
|
+
export const VERSION = '0.2.0'
|
|
21
21
|
|
|
22
22
|
const ROOT = dirname(fileURLToPath(import.meta.url))
|
|
23
23
|
const SKILLS_DIR = join(ROOT, 'skills')
|
|
24
|
-
const CONFIG_FIELDS = ['baseUrl', 'waitSeconds', 'pollIntervalMs', 'requestTimeoutMs', 'autoDownload', 'outputDir']
|
|
24
|
+
const CONFIG_FIELDS = ['baseUrl', 'waitSeconds', 'pollIntervalMs', 'requestTimeoutMs', 'autoDownload', 'outputDir', 'defaultModels']
|
|
25
25
|
|
|
26
26
|
function pickConfig(raw) {
|
|
27
27
|
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return {}
|