dsh-xdpool 2.4.2 → 2.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/lib/bin.js +81 -7
- package/lib/client.js +332 -1
- package/lib/index.d.ts +189 -23
- package/lib/index.js +753 -39
- package/package.json +1 -1
- package/vendor/channel-pack/pack.js +159 -34
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,88 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
本项目的所有重要变更都会记录在此文件。
|
|
3
3
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
|
+
## 2.4.4 — 扫码登录后仍显示「登录已被拒绝」,以及侧栏入口
|
|
5
|
+
|
|
6
|
+
> 两个都不算新功能:一个是**我上一版自己的代码**把刚扫的凭据判成「旧的」;另一个是入口位置——账号池一直只能从「设置」里进。
|
|
7
|
+
|
|
8
|
+
### 扫了码、账号池还是说「登录已被拒绝」
|
|
9
|
+
|
|
10
|
+
**你看到的**:某两个账号卡片一直显示「登录已被拒绝 / 上游已不再接受这个登录状态」,提示你去 WorkBuddy 桌面端重新登录。可你的登录途径是这个插件的扫码登录,而且刚刚扫过——扫完还是那句。
|
|
11
|
+
|
|
12
|
+
**真实原因**(两处,都在本插件自己的代码里,不在上游):
|
|
13
|
+
|
|
14
|
+
1. **新鲜度排序把扫码凭据排在最后**。同一个账号可能同时存在两份凭据:桌面 App 留在 `auth` 目录里的快照文件,和你扫码后由本插件铸造的凭据。`compareFreshness` 原先只认三档——桌面 live 文件、`lastRefreshAtMs`、`expiresAtMs`。扫码凭据的 `sourcePath` 是 `channel-pack:<provider>:<ref>`,既不是 live 文件名、也永远没有 `lastRefreshAtMs`,于是**无条件输给任何残留的旧快照**。它输一次,凭据就不被替换;不替换,就走不到「清除死亡标记」那一行——卡片上那句提示于是永远不会消失,反复扫码也没反应。
|
|
15
|
+
|
|
16
|
+
现在改为按来源分三级:桌面 live 文件 > 本插件扫码凭据 > 其它残留快照,同级才继续比签发时间和有效期。
|
|
17
|
+
|
|
18
|
+
2. **渠道包的去重按昵称分组,会删掉刚扫进来的凭据**。本插件对渠道包做过本地补丁:同渠道同昵称视为重复条目,只保留一条。问题有两个——重扫时新凭据遇到未过期的旧条目会被当作重复删掉(连凭据一起删);而**同一个人的两个账号如果昵称相同,也会被当成同一个人删掉一个**。本机正好有两个都叫「青楫渡」的账号。
|
|
19
|
+
|
|
20
|
+
现在改用凭据里的上游身份 `user_id` 判重,并且:认不出身份的一律保留(宁可留一条重复,也不能删掉别人的账号);登录半成品(昵称还是生成 id、没有有效期)在比较中让位给已完成登录的条目;都可用时按 `createdAt` 留新的。
|
|
21
|
+
|
|
22
|
+
### 左侧栏可以直接进账号池了
|
|
23
|
+
|
|
24
|
+
账号池此前只能从「设置 → XD Pool」进。现在侧栏底部(设置按钮附近)多了一个图标,点开就是同一个账号池卡片,`Esc` 或点面板外关闭。
|
|
25
|
+
|
|
26
|
+
按钮与面板是两个互不相邻的宿主插槽(`sidebar.footer.action` 与 `shell.overlay`),没有共同祖先,因此开合状态放在模块级而不是组件状态里;面板直接渲染同一个 `PoolCard`,不复制一份界面。宿主只给侧栏注册者 `{ wide }`,没有「打开设置」的接口——这也是同类插件都用自己的浮层而不是跳转的原因。老宿主缺少 `shell.overlay` 时只损失这个入口,不影响设置页。
|
|
27
|
+
|
|
28
|
+
### 测试
|
|
29
|
+
|
|
30
|
+
账号发现新增 3 例:扫码凭据胜过旧快照、桌面 live 文件仍胜过扫码凭据、凭据被替换后死亡标记清除;另有 1 例钉住「同昵称的两个人不会被合并」。全套 **79 文件 1023 用例**。
|
|
31
|
+
|
|
32
|
+
## 2.4.3 — 渠道包补上 11128、粘性绑定跨重启、网关白名单、目录 TTL,外加 CodeArts 重签开关
|
|
33
|
+
|
|
34
|
+
> 2.4.1 与 2.4.2 修的是**池车道**(`src/upstream.ts`);这一版把同一套 11128 规则补进**渠道车道**自己那份 channel pack,并顺带解决三件独立的事:会话粘性绑定不该止于进程边界、网关页面的勾选该是真开关、远端模型目录该会过期。四个 PR 来自社区([@GIGIGIGIbaby](https://github.com/GIGIGIGIbaby)),复核与发布由维护者完成。另附一个默认关闭的 CodeArts 重签开关(见末节)。
|
|
35
|
+
|
|
36
|
+
### 渠道账号也吃 400 / 11128(PR [#63](https://github.com/XDTrees/dsh-xdpool/pull/63))
|
|
37
|
+
|
|
38
|
+
`workbuddy` 渠道在 `channels.ts` 的 `POOL_OWNED_CHANNELS` 列表里,但它**不走** `src/upstream.ts` —— 它由 channel pack 自己的 WorkBuddy 适配器应答。2.4.1 的两道内容指纹、2.4.2 的 role 转换与三个会话身份头都只加在了池车道,于是渠道账号继续吃到 2.4.2 已经替别人修好的那个 `400 / 11128`:
|
|
39
|
+
|
|
40
|
+
| 项 | 渠道适配器改前 | 改后 |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| 首条消息 role | `developer` | `system`(就地转换) |
|
|
43
|
+
| `powered by DeepSeek Harness` | 原样发出 | `running on DeepSeek Harness` |
|
|
44
|
+
| `DeepSeek Harness Web GUI` | 原样发出 | `DeepSeek Harness web interface` |
|
|
45
|
+
| `X-Conversation-ID` / `-Request-ID` / `-Message-ID` | 一个都不带 | 三个都带 |
|
|
46
|
+
|
|
47
|
+
三处编辑各自标了 `[xdpool-local]`,配套的 `tests/chat-identity-headers.test.ts` 直接对**已发布的 bundle** 做源断言(pack.js 是外部构建产物、无法 `import`,所以只能在字节层 pin 住)—— 下次 pack 刷新若把这三处冲掉,测试会变红,而不是静默回退。
|
|
48
|
+
|
|
49
|
+
### 会话粘性绑定跨过进程边界(PR [#64](https://github.com/XDTrees/dsh-xdpool/pull/64))
|
|
50
|
+
|
|
51
|
+
卡片承诺「一个会话一个账号」,但这个承诺原先止于进程边界:绑定存在一个 `Map` 里,每次重启都重新挑选,于是同一个会话一天里可能被三个账号各服务一遍,每次都按完整 prompt 计费。现在这张表写进插件状态目录(只存哈希到账号 id,不含任何消息内容),构造时加载,写入合并成一次原子 rename,并沿用与内存版相同的 LRU 上限。
|
|
52
|
+
|
|
53
|
+
测试特意把会话绑到一个「默认挑选不会选中」的账号 —— 新建的空池本来就会同意未绑定会话的选择,所以「两个池答案一致」在禁用加载时也能通过;实测把加载去掉会让其中三个用例变红。
|
|
54
|
+
|
|
55
|
+
### 网关页面的勾选不再是纯展示(PR [#65](https://github.com/XDTrees/dsh-xdpool/pull/65))
|
|
56
|
+
|
|
57
|
+
Gateway 页的模型勾选框与 LAN 共享列表此前只是**展示过滤器**:`/v1/models` 会隐藏没勾的模型,但请求路径照单全收。一台还留着旧模型 id 的手机、或照文档猜一个 id 的第二编辑器,就能继续拿卡片上写着「关」的模型花用户的钱。
|
|
58
|
+
|
|
59
|
+
现在强制发生在**调用方实际发出的那个 id 上**,且在任何车道剥前缀或改写 body **之前**,覆盖每条车道:pool、aggregate、free、combo,以及登录渠道。列表与闸门读同一份集合——这正是让卡片上的勾选有意义的原因。
|
|
60
|
+
|
|
61
|
+
### 远端模型目录会过期(PR [#66](https://github.com/XDTrees/dsh-xdpool/pull/66))
|
|
62
|
+
|
|
63
|
+
bundle 里每个适配器都把 `remoteModels` 缓存到进程结束,于是厂商改名或调整倍率要等重启才反映到卡片上——而**改名通常就是改价**,因为倍率是名字的一部分。现在由一道 TTL 闸门决定缓存能否应答,时长十分钟,可用 `DSH_XDPOOL_CATALOG_TTL_MS` 覆盖以便测试。
|
|
64
|
+
|
|
65
|
+
闸门刻意**不清缓存**:加载器只在成功时覆盖,所以刷新失败会继续沿用最后一份已知良好的目录,而不是退化成一堆裸 id;网关侧 30s 的冷却又限住了重试速率。同样无需重建 lib —— `channels.ts` 在运行期按路径加载 `pack.js`。
|
|
66
|
+
|
|
67
|
+
### 可选:CodeArts 请求带重签头(默认关闭,`[xdpool-local]`)
|
|
68
|
+
|
|
69
|
+
在 CodeArts 的 chat 请求上追加两个头:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
x-bili-resign: sdk-hmac-sha256
|
|
73
|
+
x-bili-resign-credential: base64({"ak":"…","sk":"…","token":"…"})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
用途是让 billion-context 用这对凭证**重签**一条它接管的 SDK-HMAC-SHA256 请求:该方案的签名覆盖请求体,压缩改写会让签名失效,而 bili 拿不到重签凭证时会按 compress-or-refuse 契约**本地拒发**(`bili_resign_unavailable`,表现为 403,再被误读成「密钥无效」)。带上这对头,bili 就能重算签名,压缩能力得以保留(billion-context 的 #1884 逃生口)。
|
|
77
|
+
|
|
78
|
+
**为什么默认关闭**:`billion-context` 只在 `optionalDependencies` 里,未安装时这两个头会**直达上游**,而它们携带的是 AK/SK;另外 bili 的进程内 native client 自己会构造这两个头(`resignExtra`),xdpool 加不加都不影响它。开关留给「xdpool 直接对接 bili 代理路径」或将来需要显式携带凭证的场合 —— 启用方式是在 pool 构造时传 `codeartsResignHeaders: true`(当前没有对应的用户级配置项)。
|
|
79
|
+
|
|
80
|
+
配套 `tests/codearts-resign-headers.test.ts` 用源断言 pin 住四件事:两个头名齐全、字段名与 bili `decodeApigCredential` 读取的 `ak`/`sk`/`token` 一致、插入位置在 `signRequestHuawei` 之后、以及默认关闭。
|
|
81
|
+
|
|
82
|
+
### 其他
|
|
83
|
+
|
|
84
|
+
- 新增测试:渠道包 11128 规则源断言、粘性绑定跨重启(含「去掉加载即变红」的对照)、网关白名单与列表一致性、目录 TTL。
|
|
85
|
+
|
|
4
86
|
## 2.4.2 — 第二道内容指纹:`Web GUI` 那句,以及 assistant 历史
|
|
5
87
|
> 2.4.1 之后仍有 11128。这一版补上第二道内容指纹,并把改写范围扩到 assistant 历史。修复来自社区 PR [#62](https://github.com/XDTrees/dsh-xdpool/pull/62)([@jmglsi](https://github.com/jmglsi)),实测复核与发布由维护者完成。
|
|
6
88
|
### 同一条提示词里还有第二道指纹
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createDecipheriv, createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { copyFile, mkdir, readFile, readdir, rename, rm, writeFile, writeFile as writeFile$1 } from "node:fs/promises";
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { basename, dirname, join, resolve } from "node:path";
|
|
@@ -3084,24 +3084,49 @@ function isLiveAuthFile(path) {
|
|
|
3084
3084
|
return basename(path) === WORKBUDDY_LIVE_FILENAME;
|
|
3085
3085
|
}
|
|
3086
3086
|
/**
|
|
3087
|
+
* True for a credential this plugin minted from the channel-pack sign-in flow.
|
|
3088
|
+
*
|
|
3089
|
+
* `packCredentialToWorkBuddy` stamps `channel-pack:<provider>:<ref>` here, so the
|
|
3090
|
+
* prefix is the only thing distinguishing a scanned credential from a file the
|
|
3091
|
+
* desktop app wrote. Nothing about it is a path on disk — there is no file to
|
|
3092
|
+
* stat and no mtime to read.
|
|
3093
|
+
*/
|
|
3094
|
+
function isChannelPackSource(path) {
|
|
3095
|
+
return path.startsWith("channel-pack:");
|
|
3096
|
+
}
|
|
3097
|
+
/**
|
|
3087
3098
|
* Which of two credentials for the same account the pool should keep.
|
|
3088
3099
|
*
|
|
3089
3100
|
* Ordering, highest first:
|
|
3090
3101
|
*
|
|
3091
3102
|
* 1. the live file the desktop app is signed in with;
|
|
3092
|
-
* 2.
|
|
3093
|
-
* 3. the
|
|
3103
|
+
* 2. a credential minted by our own channel-pack sign-in;
|
|
3104
|
+
* 3. the credential the upstream issued most recently (`lastRefreshAtMs`);
|
|
3105
|
+
* 4. the longer stored expiry, as a fallback for documents that carry no issue
|
|
3094
3106
|
* time (the plugin's own refreshed copy, older builds).
|
|
3095
3107
|
*
|
|
3096
3108
|
* The stored expiry alone is NOT a freshness signal: the upstream does not
|
|
3097
3109
|
* rewrite it when it revokes a token, so a long-dead backup can claim to expire
|
|
3098
3110
|
* later than the token that actually works. Selecting on it made every upstream
|
|
3099
3111
|
* call return 401 while a perfectly good credential sat in the same directory.
|
|
3112
|
+
*
|
|
3113
|
+
* Rank 2 exists because a scanned credential loses both of the lower tests by
|
|
3114
|
+
* construction: it is not the live filename, and the sign-in flow records no
|
|
3115
|
+
* `lastRefreshAtMs`. Ranking it below a leftover snapshot meant a user who had
|
|
3116
|
+
* just scanned a fresh sign-in still saw "sign-in was rejected", because the
|
|
3117
|
+
* abandoned file kept winning the comparison and the dead mark was never
|
|
3118
|
+
* cleared. The live file stays on top: that is the session the running desktop
|
|
3119
|
+
* app is genuinely using right now, and it is newer than anything we mint.
|
|
3100
3120
|
*/
|
|
3101
3121
|
function compareFreshness(a, b) {
|
|
3102
|
-
const
|
|
3103
|
-
|
|
3104
|
-
|
|
3122
|
+
const rankOf = (credential) => {
|
|
3123
|
+
if (isLiveAuthFile(credential.sourcePath)) return 2;
|
|
3124
|
+
if (isChannelPackSource(credential.sourcePath)) return 1;
|
|
3125
|
+
return 0;
|
|
3126
|
+
};
|
|
3127
|
+
const aRank = rankOf(a);
|
|
3128
|
+
const bRank = rankOf(b);
|
|
3129
|
+
if (aRank !== bRank) return bRank - aRank;
|
|
3105
3130
|
const aIssued = a.lastRefreshAtMs ?? 0;
|
|
3106
3131
|
const bIssued = b.lastRefreshAtMs ?? 0;
|
|
3107
3132
|
if (aIssued !== bIssued) return bIssued - aIssued;
|
|
@@ -3363,6 +3388,9 @@ var WorkBuddyAccountPool = class {
|
|
|
3363
3388
|
* the LRU order: re-binding deletes then re-inserts.
|
|
3364
3389
|
*/
|
|
3365
3390
|
affinity = /* @__PURE__ */ new Map();
|
|
3391
|
+
/** Where the bindings above are persisted; undefined keeps them in memory. */
|
|
3392
|
+
affinityPath;
|
|
3393
|
+
affinitySaveTimer;
|
|
3366
3394
|
lastScanAtMs = 0;
|
|
3367
3395
|
/**
|
|
3368
3396
|
* The account id the user pinned, or undefined for none.
|
|
@@ -3472,6 +3500,8 @@ var WorkBuddyAccountPool = class {
|
|
|
3472
3500
|
this.distribution = options.distribution ?? "priority";
|
|
3473
3501
|
this.persistent = options.persistent ?? false;
|
|
3474
3502
|
this.storePath = accountStorePath();
|
|
3503
|
+
this.affinityPath = options.affinityPath;
|
|
3504
|
+
if (this.affinityPath !== void 0) this.loadAffinity();
|
|
3475
3505
|
}
|
|
3476
3506
|
/**
|
|
3477
3507
|
* Re-apply configuration that only affects discovery and cooldown policy,
|
|
@@ -3747,6 +3777,49 @@ var WorkBuddyAccountPool = class {
|
|
|
3747
3777
|
if (oldest === void 0) break;
|
|
3748
3778
|
this.affinity.delete(oldest);
|
|
3749
3779
|
}
|
|
3780
|
+
this.scheduleAffinitySave();
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* Restore bindings written by a previous process.
|
|
3784
|
+
*
|
|
3785
|
+
* Tolerant like the other state files: a malformed document or one bad entry
|
|
3786
|
+
* must not take the pool down, and an account id that no longer exists is
|
|
3787
|
+
* simply never matched (the next acquire re-picks and rebinds).
|
|
3788
|
+
*/
|
|
3789
|
+
loadAffinity() {
|
|
3790
|
+
if (this.affinityPath === void 0) return;
|
|
3791
|
+
try {
|
|
3792
|
+
const bindings = JSON.parse(readFileSync(this.affinityPath, "utf8")).bindings;
|
|
3793
|
+
if (typeof bindings !== "object" || bindings === null || Array.isArray(bindings)) return;
|
|
3794
|
+
for (const [key, value] of Object.entries(bindings)) {
|
|
3795
|
+
if (key === "" || typeof value !== "string" || value === "") continue;
|
|
3796
|
+
this.affinity.set(key, value);
|
|
3797
|
+
}
|
|
3798
|
+
while (this.affinity.size > STICKY_AFFINITY_LIMIT) {
|
|
3799
|
+
const oldest = this.affinity.keys().next().value;
|
|
3800
|
+
if (oldest === void 0) break;
|
|
3801
|
+
this.affinity.delete(oldest);
|
|
3802
|
+
}
|
|
3803
|
+
} catch {}
|
|
3804
|
+
}
|
|
3805
|
+
/** Write the bindings back, coalescing a burst into one atomic rename. */
|
|
3806
|
+
scheduleAffinitySave() {
|
|
3807
|
+
if (this.affinityPath === void 0 || this.affinitySaveTimer !== void 0) return;
|
|
3808
|
+
this.affinitySaveTimer = setTimeout(() => {
|
|
3809
|
+
this.affinitySaveTimer = void 0;
|
|
3810
|
+
const path = this.affinityPath;
|
|
3811
|
+
if (path === void 0) return;
|
|
3812
|
+
try {
|
|
3813
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
3814
|
+
writeFileSync(`${path}.tmp`, `${JSON.stringify({
|
|
3815
|
+
version: 1,
|
|
3816
|
+
bindings: Object.fromEntries(this.affinity)
|
|
3817
|
+
}, null, 2)}
|
|
3818
|
+
`, "utf8");
|
|
3819
|
+
renameSync(`${path}.tmp`, path);
|
|
3820
|
+
} catch {}
|
|
3821
|
+
}, 500);
|
|
3822
|
+
this.affinitySaveTimer.unref?.();
|
|
3750
3823
|
}
|
|
3751
3824
|
/**
|
|
3752
3825
|
* `sticky`: the account this conversation already used, when it can still
|
|
@@ -11217,7 +11290,8 @@ function createCore(logger) {
|
|
|
11217
11290
|
const pool = new WorkBuddyAccountPool({
|
|
11218
11291
|
...logger === void 0 ? {} : { logger },
|
|
11219
11292
|
client,
|
|
11220
|
-
persistent: true
|
|
11293
|
+
persistent: true,
|
|
11294
|
+
affinityPath: join(pluginDataDir(), "sticky-affinity.json")
|
|
11221
11295
|
});
|
|
11222
11296
|
return {
|
|
11223
11297
|
pool,
|
package/lib/client.js
CHANGED
|
@@ -1255,6 +1255,21 @@ window.__ModuleLoader__.load({
|
|
|
1255
1255
|
.dsm-xdpool-gw-actions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
1256
1256
|
.dsm-xdpool-gw-flash{margin:0;padding:6px 10px;border-radius:9px;border:1px solid color-mix(in srgb, var(--dsw-alias-border-l1,#ffffff14) 70%, transparent);background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#191a1e) 72%, transparent);color:var(--dsw-alias-label-secondary,#c6c9d0);font-size:11px;line-height:17px;word-break:break-word}
|
|
1257
1257
|
.dsm-xdpool-gw-btn{appearance:none;display:inline-flex;align-items:center;gap:6px;font:inherit;font-size:11.5px;font-weight:560;line-height:17px;padding:4px 12px;border-radius:8px;border:1px solid color-mix(in srgb, var(--dsw-alias-border-l2,#36373b) 85%, transparent);background:color-mix(in srgb, var(--dsw-alias-bg-layer-3,#2a2c33) 70%, transparent);color:var(--dsw-alias-label-secondary,#c6c9d0);cursor:pointer;white-space:nowrap;transition:border-color .16s ease,color .16s ease,background .16s ease,transform .18s var(--ease-instrument,cubic-bezier(.16,1,.3,1))}
|
|
1258
|
+
/* The gateway's shared-model picker. Grouped by lane, and each row carries BOTH
|
|
1259
|
+
the display name and the wire id, because for the login channels they are
|
|
1260
|
+
different words: the user knows the model as "Qwen3.8-Flash · 免费" and has to
|
|
1261
|
+
type "qfmodel" into the client. Showing only one of the two makes the list
|
|
1262
|
+
either unreadable or unusable. */
|
|
1263
|
+
.dsm-xdpool-gw-group{margin-top:10px;display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
1264
|
+
.dsm-xdpool-gw-group-title{margin:0 0 2px;color:var(--dsw-alias-label-tertiary,#8a90a0);font-size:11px;line-height:16px;font-weight:650;letter-spacing:.1px;text-transform:lowercase}
|
|
1265
|
+
.dsm-xdpool-gw-model{display:grid;grid-template-columns:16px minmax(0,1fr) auto;align-items:center;gap:8px;padding:3px 6px;border-radius:8px;cursor:pointer;min-width:0}
|
|
1266
|
+
.dsm-xdpool-gw-model:hover{background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#191a1e) 60%, transparent)}
|
|
1267
|
+
.dsm-xdpool-gw-model input{appearance:none;width:14px;height:14px;margin:0;border-radius:4px;border:1px solid var(--dsw-alias-border-l2,#36373b);background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#191a1e) 78%, transparent);cursor:pointer;display:grid;place-items:center}
|
|
1268
|
+
.dsm-xdpool-gw-model input:checked{background:var(--dsw-alias-brand-primary,#5686fe);border-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
1269
|
+
.dsm-xdpool-gw-model input:checked::after{content:"";width:6px;height:3px;border-left:1.5px solid #fff;border-bottom:1.5px solid #fff;transform:rotate(-45deg) translateY(-1px)}
|
|
1270
|
+
.dsm-xdpool-gw-model input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:1px}
|
|
1271
|
+
.dsm-xdpool-gw-model-name{color:var(--dsw-alias-label-primary,#e8e8ea);font-size:12px;line-height:17px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
1272
|
+
.dsm-xdpool-gw-model-id{font-family:var(--dsw-alias-font-mono,ui-monospace,Menlo,Consolas,monospace);font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary,#8a90a0);white-space:nowrap}
|
|
1258
1273
|
.dsm-xdpool-gw-btn:hover:not(:disabled){transform:translateY(-1px);border-color:color-mix(in srgb, var(--dsw-alias-brand-primary,#5686fe) 55%, var(--dsw-alias-border-l2,#36373b));color:var(--dsw-alias-label-primary,#e8e8ea)}
|
|
1259
1274
|
.dsm-xdpool-gw-btn:active:not(:disabled){transform:translateY(0);box-shadow:inset 0 2px 4px rgb(0 0 0 / 22%)}
|
|
1260
1275
|
.dsm-xdpool-gw-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5686fe);outline-offset:2px}
|
|
@@ -1655,6 +1670,39 @@ window.__ModuleLoader__.load({
|
|
|
1655
1670
|
):active:not(:disabled){transform:none}
|
|
1656
1671
|
}
|
|
1657
1672
|
|
|
1673
|
+
/* ── Docked entry beside the settings control ────────────────────────────────
|
|
1674
|
+
The sidebar button and the overlay are two separate registrations with no
|
|
1675
|
+
shared ancestor, so neither can carry the shell's own custom properties: the
|
|
1676
|
+
easing curves are re-declared here rather than inherited. Everything below is
|
|
1677
|
+
scoped to .dsm-xdpool-dock so it cannot reach into the host's sidebar. */
|
|
1678
|
+
.dsm-xdpool-dock{--ease-instrument:cubic-bezier(.16,1,.3,1);position:relative;z-index:1;display:flex;align-items:center;gap:8px;width:100%;min-width:0;padding:6px 8px;border:1px solid transparent;border-radius:9px;background:transparent;color:var(--dsw-alias-label-secondary,#c6c9d0);font:inherit;font-size:12px;line-height:17px;font-weight:560;text-align:left;cursor:pointer;overflow:hidden;transition:color .2s var(--ease-instrument),background .2s var(--ease-instrument),border-color .2s var(--ease-instrument)}
|
|
1679
|
+
.dsm-xdpool-dock:hover{color:var(--dsw-alias-label-primary,#e8e8ea);background:color-mix(in srgb, var(--dsw-alias-label-primary,#e8e8ea) 7%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-border-l2,#ffffff1f) 70%, transparent)}
|
|
1680
|
+
.dsm-xdpool-dock-on{color:var(--dsw-alias-label-primary,#e8e8ea);background:color-mix(in srgb, var(--dsw-alias-state-business-primary,#5686fe) 14%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-state-business-primary,#5686fe) 40%, transparent)}
|
|
1681
|
+
/* The sweep is the same gesture the card's own buttons make, so the dock reads
|
|
1682
|
+
as part of the plugin rather than a host control. */
|
|
1683
|
+
.dsm-xdpool-dock::after{content:"";position:absolute;top:0;bottom:0;left:-60%;width:42%;pointer-events:none;background:linear-gradient(100deg,transparent,rgb(255 255 255 / 12%),transparent);transform:skewX(-18deg);transition:left .5s var(--ease-instrument)}
|
|
1684
|
+
.dsm-xdpool-dock:hover::after{left:118%}
|
|
1685
|
+
.dsm-xdpool-dockicon{flex:none;width:16px;height:16px;background-color:currentColor;-webkit-mask-image:var(--dsm-xdpool-dock-glyph);mask-image:var(--dsm-xdpool-dock-glyph);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;-webkit-mask-size:16px 16px;mask-size:16px 16px}
|
|
1686
|
+
.dsm-xdpool-docklabel{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1687
|
+
/* No dark backdrop: the panel is a companion to the conversation, not a modal,
|
|
1688
|
+
and dimming the whole host for a settings card reads as a page navigation.
|
|
1689
|
+
Clicking outside still closes it — the scrim covers the viewport and is
|
|
1690
|
+
transparent, so only the click is caught. */
|
|
1691
|
+
.dsm-xdpool-dockscrim{position:fixed;inset:0;z-index:60;display:flex;align-items:stretch;justify-content:flex-end;background:transparent}
|
|
1692
|
+
.dsm-xdpool-dockpanel{--ease-instrument:cubic-bezier(.16,1,.3,1);display:flex;flex-direction:column;width:min(760px, 92vw);margin:14px;border:1px solid color-mix(in srgb, var(--dsw-alias-border-l2,#ffffff1f) 82%, transparent);border-radius:16px;background:color-mix(in srgb, var(--dsw-alias-bg-layer-1,#141517) 96%, transparent);box-shadow:0 20px 60px rgb(0 0 0 / 45%);overflow:hidden;animation:dsm-xdpool-dock-in .3s var(--ease-instrument)}
|
|
1693
|
+
@keyframes dsm-xdpool-dock-in{from{opacity:0;transform:translateX(18px)}to{opacity:1;transform:none}}
|
|
1694
|
+
.dsm-xdpool-dockhead{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid color-mix(in srgb, var(--dsw-alias-border-l1,#ffffff14) 80%, transparent)}
|
|
1695
|
+
.dsm-xdpool-docktitle{flex:1 1 auto;font-size:12px;line-height:17px;font-weight:640;color:var(--dsw-alias-label-primary,#e8e8ea)}
|
|
1696
|
+
.dsm-xdpool-dockclose{appearance:none;flex:none;width:22px;height:22px;padding:0;border:1px solid transparent;border-radius:7px;background:transparent;color:var(--dsw-alias-label-tertiary,#8a90a0);font:inherit;font-size:15px;line-height:1;cursor:pointer;transition:color .16s ease,background .16s ease}
|
|
1697
|
+
.dsm-xdpool-dockclose:hover{color:var(--dsw-alias-label-primary,#e8e8ea);background:color-mix(in srgb, var(--dsw-alias-label-primary,#e8e8ea) 9%, transparent)}
|
|
1698
|
+
/* The card inside the panel was built for the settings dialog's width; scroll
|
|
1699
|
+
the panel body, never the card, so its own sticky header keeps working. */
|
|
1700
|
+
.dsm-xdpool-dockbody{flex:1 1 auto;min-height:0;overflow:auto;padding:14px}
|
|
1701
|
+
@media (prefers-reduced-motion:reduce){
|
|
1702
|
+
.dsm-xdpool-dock::after{display:none}
|
|
1703
|
+
.dsm-xdpool-dockpanel{animation:none}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1658
1706
|
`.trim();
|
|
1659
1707
|
//#endregion
|
|
1660
1708
|
//#region src/client/avatar.ts
|
|
@@ -10776,6 +10824,35 @@ window.__ModuleLoader__.load({
|
|
|
10776
10824
|
const [revealed, setRevealed] = (0, react.useState)(false);
|
|
10777
10825
|
const [host, setHost] = (0, react.useState)("127.0.0.1");
|
|
10778
10826
|
const [port, setPort] = (0, react.useState)("8326");
|
|
10827
|
+
/**
|
|
10828
|
+
* The in-progress share list, or undefined while it still mirrors the server.
|
|
10829
|
+
*
|
|
10830
|
+
* Held as a draft for the same reason the model card holds one: saving is a
|
|
10831
|
+
* deliberate act here, because every unchecked row stops working on whatever
|
|
10832
|
+
* device is already pointed at the gateway.
|
|
10833
|
+
*/
|
|
10834
|
+
const [draftShared, setDraftShared] = (0, react.useState)(void 0);
|
|
10835
|
+
/**
|
|
10836
|
+
* Which ids are shared right now.
|
|
10837
|
+
*
|
|
10838
|
+
* `status.shared === undefined` means the user has never chosen, and the
|
|
10839
|
+
* gateway is serving everything — so the picker must show every row ticked
|
|
10840
|
+
* rather than an empty list, which would read as "nothing is shared" and invite
|
|
10841
|
+
* a save that closes a gateway that was open.
|
|
10842
|
+
*/
|
|
10843
|
+
const candidates = status?.candidates ?? [];
|
|
10844
|
+
const serverShared = status?.shared ?? candidates.map((row) => row.id);
|
|
10845
|
+
const shared = draftShared ?? new Set(serverShared);
|
|
10846
|
+
const sharedDirty = draftShared !== void 0 && (draftShared.size !== serverShared.length || [...draftShared].some((id) => !serverShared.includes(id)));
|
|
10847
|
+
const toggleShared = (id) => {
|
|
10848
|
+
const next = new Set(shared);
|
|
10849
|
+
if (next.has(id)) next.delete(id);
|
|
10850
|
+
else next.add(id);
|
|
10851
|
+
setDraftShared(next);
|
|
10852
|
+
};
|
|
10853
|
+
const setAllShared = (on) => {
|
|
10854
|
+
setDraftShared(on ? new Set(candidates.map((row) => row.id)) : /* @__PURE__ */ new Set());
|
|
10855
|
+
};
|
|
10779
10856
|
/** True once the server's values were adopted, so polls do not stomp edits. */
|
|
10780
10857
|
const seeded = (0, react.useRef)(false);
|
|
10781
10858
|
const mounted = (0, react.useRef)(true);
|
|
@@ -11015,6 +11092,92 @@ window.__ModuleLoader__.load({
|
|
|
11015
11092
|
})
|
|
11016
11093
|
]
|
|
11017
11094
|
}),
|
|
11095
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11096
|
+
className: "dsm-xdpool-sec",
|
|
11097
|
+
children: [
|
|
11098
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11099
|
+
className: "dsm-xdpool-sectitle",
|
|
11100
|
+
children: t?.("gw.share") ?? "Shared models"
|
|
11101
|
+
}),
|
|
11102
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11103
|
+
className: "dsm-xdpool-gw-note",
|
|
11104
|
+
children: t?.("gw.shareHint") ?? ""
|
|
11105
|
+
}),
|
|
11106
|
+
candidates.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11107
|
+
className: "dsm-xdpool-gw-note",
|
|
11108
|
+
children: t?.("gw.shareNone") ?? ""
|
|
11109
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
11110
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11111
|
+
className: "dsm-xdpool-gw-actions",
|
|
11112
|
+
children: [
|
|
11113
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11114
|
+
type: "button",
|
|
11115
|
+
className: "dsm-xdpool-gw-btn",
|
|
11116
|
+
"data-on": "false",
|
|
11117
|
+
disabled: busy,
|
|
11118
|
+
onClick: () => {
|
|
11119
|
+
setAllShared(true);
|
|
11120
|
+
},
|
|
11121
|
+
children: t?.("gw.shareAll") ?? "Select all"
|
|
11122
|
+
}),
|
|
11123
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11124
|
+
type: "button",
|
|
11125
|
+
className: "dsm-xdpool-gw-btn",
|
|
11126
|
+
"data-on": "false",
|
|
11127
|
+
disabled: busy,
|
|
11128
|
+
onClick: () => {
|
|
11129
|
+
setAllShared(false);
|
|
11130
|
+
},
|
|
11131
|
+
children: t?.("gw.shareNoneBtn") ?? "Select none"
|
|
11132
|
+
}),
|
|
11133
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11134
|
+
type: "button",
|
|
11135
|
+
className: "dsm-xdpool-gw-btn",
|
|
11136
|
+
"data-on": "true",
|
|
11137
|
+
disabled: busy || !sharedDirty,
|
|
11138
|
+
onClick: () => {
|
|
11139
|
+
write({ models: [...shared] }).then(() => {
|
|
11140
|
+
setDraftShared(void 0);
|
|
11141
|
+
});
|
|
11142
|
+
},
|
|
11143
|
+
children: t?.("gw.shareSave") ?? "Save"
|
|
11144
|
+
})
|
|
11145
|
+
]
|
|
11146
|
+
}),
|
|
11147
|
+
sharedDirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11148
|
+
className: "dsm-xdpool-gw-warn",
|
|
11149
|
+
children: t?.("gw.shareDirty") ?? ""
|
|
11150
|
+
}) : null,
|
|
11151
|
+
[...new Set(candidates.map((row) => row.source))].map((source) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11152
|
+
className: "dsm-xdpool-gw-group",
|
|
11153
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11154
|
+
className: "dsm-xdpool-gw-group-title",
|
|
11155
|
+
children: source
|
|
11156
|
+
}), candidates.filter((row) => row.source === source).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
11157
|
+
className: "dsm-xdpool-gw-model",
|
|
11158
|
+
children: [
|
|
11159
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
11160
|
+
type: "checkbox",
|
|
11161
|
+
checked: shared.has(row.id),
|
|
11162
|
+
disabled: busy,
|
|
11163
|
+
onChange: () => {
|
|
11164
|
+
toggleShared(row.id);
|
|
11165
|
+
}
|
|
11166
|
+
}),
|
|
11167
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11168
|
+
className: "dsm-xdpool-gw-model-name",
|
|
11169
|
+
children: row.name
|
|
11170
|
+
}),
|
|
11171
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11172
|
+
className: "dsm-xdpool-gw-model-id",
|
|
11173
|
+
children: row.id
|
|
11174
|
+
})
|
|
11175
|
+
]
|
|
11176
|
+
}, row.id))]
|
|
11177
|
+
}, source))
|
|
11178
|
+
] })
|
|
11179
|
+
]
|
|
11180
|
+
}),
|
|
11018
11181
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11019
11182
|
className: "dsm-xdpool-sec",
|
|
11020
11183
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -12195,7 +12358,7 @@ window.__ModuleLoader__.load({
|
|
|
12195
12358
|
*
|
|
12196
12359
|
* @module dsh-xdpool/version
|
|
12197
12360
|
*/
|
|
12198
|
-
const POOL_VERSION = "2.4.
|
|
12361
|
+
const POOL_VERSION = "2.4.4";
|
|
12199
12362
|
//#endregion
|
|
12200
12363
|
//#region src/client/PoolCard.tsx
|
|
12201
12364
|
/**
|
|
@@ -14589,6 +14752,138 @@ window.__ModuleLoader__.load({
|
|
|
14589
14752
|
*/
|
|
14590
14753
|
const POOL_NAV_ICON_MASK_URL = `data:image/svg+xml;utf8,${encodeURIComponent(POOL_NAV_ICON_SVG)}`;
|
|
14591
14754
|
//#endregion
|
|
14755
|
+
//#region src/client/dock.tsx
|
|
14756
|
+
/**
|
|
14757
|
+
* The docked entry: a `sidebar.footer.action` button that opens the pool card in
|
|
14758
|
+
* a `shell.overlay` panel.
|
|
14759
|
+
*
|
|
14760
|
+
* Why not link to the Settings page the card already lives on: the host's
|
|
14761
|
+
* sidebar slot hands a registrant exactly `{ wide }` — no `openSettings`, no
|
|
14762
|
+
* navigation seat. dsh-purge, dshmarket and everything else third-party solve it
|
|
14763
|
+
* the same way, by owning the surface: a button that toggles an overlay of its
|
|
14764
|
+
* own. Re-implementing the card for the overlay would mean two copies to keep in
|
|
14765
|
+
* step, so the overlay renders the very same `PoolCard` the settings section
|
|
14766
|
+
* renders, page state and all.
|
|
14767
|
+
*
|
|
14768
|
+
* The open flag lives at module scope rather than in React state because the
|
|
14769
|
+
* button and the panel are two separate slot registrations with no common
|
|
14770
|
+
* ancestor; module state plus a subscription is the only channel between them.
|
|
14771
|
+
*/
|
|
14772
|
+
/** Subscribers re-read {@link dockedOpen}; the setter notifies them. */
|
|
14773
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
14774
|
+
let dockedOpen = false;
|
|
14775
|
+
/** Read whether the docked panel is showing. */
|
|
14776
|
+
function isDockedOpen() {
|
|
14777
|
+
return dockedOpen;
|
|
14778
|
+
}
|
|
14779
|
+
/** Show or hide the docked panel, notifying every registrant. */
|
|
14780
|
+
function setDockedOpen(open) {
|
|
14781
|
+
if (dockedOpen === open) return;
|
|
14782
|
+
dockedOpen = open;
|
|
14783
|
+
for (const listener of [...listeners]) listener();
|
|
14784
|
+
}
|
|
14785
|
+
/** Subscribe to {@link setDockedOpen}. Returns the unsubscribe. */
|
|
14786
|
+
function subscribeDockedOpen(listener) {
|
|
14787
|
+
listeners.add(listener);
|
|
14788
|
+
return () => {
|
|
14789
|
+
listeners.delete(listener);
|
|
14790
|
+
};
|
|
14791
|
+
}
|
|
14792
|
+
/** Subscribe a component to the open flag. */
|
|
14793
|
+
function useDockedOpen() {
|
|
14794
|
+
const [open, setOpen] = (0, react.useState)(isDockedOpen);
|
|
14795
|
+
(0, react.useEffect)(() => {
|
|
14796
|
+
setOpen(isDockedOpen());
|
|
14797
|
+
return subscribeDockedOpen(() => {
|
|
14798
|
+
setOpen(isDockedOpen());
|
|
14799
|
+
});
|
|
14800
|
+
}, []);
|
|
14801
|
+
return open;
|
|
14802
|
+
}
|
|
14803
|
+
/**
|
|
14804
|
+
* The sidebar button.
|
|
14805
|
+
*
|
|
14806
|
+
* Renders a glyph and, only when the rail is wide, the product name: a label in
|
|
14807
|
+
* the collapsed 56px rail is clipped mid-word, which reads as a layout bug.
|
|
14808
|
+
*/
|
|
14809
|
+
function SidebarEntry({ wide }) {
|
|
14810
|
+
const open = useDockedOpen();
|
|
14811
|
+
const label = "XD Pool";
|
|
14812
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
14813
|
+
type: "button",
|
|
14814
|
+
className: `dsm-xdpool-dock${open ? " dsm-xdpool-dock-on" : ""}`,
|
|
14815
|
+
title: open ? "收起 XD Pool" : "打开 XD Pool",
|
|
14816
|
+
"aria-expanded": open,
|
|
14817
|
+
"aria-label": label,
|
|
14818
|
+
style: { "--dsm-xdpool-dock-glyph": `url("${POOL_NAV_ICON_MASK_URL}")` },
|
|
14819
|
+
onClick: () => {
|
|
14820
|
+
setDockedOpen(!isDockedOpen());
|
|
14821
|
+
},
|
|
14822
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14823
|
+
className: "dsm-xdpool-dockicon",
|
|
14824
|
+
"aria-hidden": "true"
|
|
14825
|
+
}), wide === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14826
|
+
className: "dsm-xdpool-docklabel",
|
|
14827
|
+
children: label
|
|
14828
|
+
}) : null]
|
|
14829
|
+
});
|
|
14830
|
+
}
|
|
14831
|
+
/**
|
|
14832
|
+
* The panel, rendered into `shell.overlay`.
|
|
14833
|
+
*
|
|
14834
|
+
* `children` is the card itself, passed in rather than imported so this module
|
|
14835
|
+
* stays free of the card's dependency graph (the overlay must not drag the
|
|
14836
|
+
* settings scope in with it when the host line has no scope to hand out).
|
|
14837
|
+
*/
|
|
14838
|
+
function DockedPanel({ children }) {
|
|
14839
|
+
const open = useDockedOpen();
|
|
14840
|
+
(0, react.useEffect)(() => {
|
|
14841
|
+
if (!open) return;
|
|
14842
|
+
const onKeyDown = (event) => {
|
|
14843
|
+
if (event.key === "Escape") setDockedOpen(false);
|
|
14844
|
+
};
|
|
14845
|
+
window.addEventListener("keydown", onKeyDown);
|
|
14846
|
+
return () => {
|
|
14847
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
14848
|
+
};
|
|
14849
|
+
}, [open]);
|
|
14850
|
+
if (!open) return null;
|
|
14851
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14852
|
+
className: "dsm-xdpool-dockscrim",
|
|
14853
|
+
role: "presentation",
|
|
14854
|
+
onClick: () => {
|
|
14855
|
+
setDockedOpen(false);
|
|
14856
|
+
},
|
|
14857
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14858
|
+
className: "dsm-xdpool-dockpanel",
|
|
14859
|
+
role: "dialog",
|
|
14860
|
+
"aria-modal": "false",
|
|
14861
|
+
"aria-label": "XD Pool",
|
|
14862
|
+
onClick: (event) => {
|
|
14863
|
+
event.stopPropagation();
|
|
14864
|
+
},
|
|
14865
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
14866
|
+
className: "dsm-xdpool-dockhead",
|
|
14867
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
14868
|
+
className: "dsm-xdpool-docktitle",
|
|
14869
|
+
children: "XD Pool"
|
|
14870
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
14871
|
+
type: "button",
|
|
14872
|
+
className: "dsm-xdpool-dockclose",
|
|
14873
|
+
"aria-label": "关闭",
|
|
14874
|
+
onClick: () => {
|
|
14875
|
+
setDockedOpen(false);
|
|
14876
|
+
},
|
|
14877
|
+
children: "×"
|
|
14878
|
+
})]
|
|
14879
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
14880
|
+
className: "dsm-xdpool-dockbody",
|
|
14881
|
+
children
|
|
14882
|
+
})]
|
|
14883
|
+
})
|
|
14884
|
+
});
|
|
14885
|
+
}
|
|
14886
|
+
//#endregion
|
|
14592
14887
|
//#region src/client/locales.ts
|
|
14593
14888
|
/**
|
|
14594
14889
|
* Plugin-card copy registered under the `settings.xdpool` locale
|
|
@@ -14961,6 +15256,15 @@ window.__ModuleLoader__.load({
|
|
|
14961
15256
|
"gw.usageHint": "Any OpenAI-compatible app works. Set its base URL to the endpoint above, pick a model out of /v1/models, and send this key as the API key. The relay passes /v1/models and /v1/chat/completions only; everything else is refused before a byte is forwarded.",
|
|
14962
15257
|
"gw.loading": "Reading the relay status…",
|
|
14963
15258
|
"gw.saveFailed": "Could not save",
|
|
15259
|
+
"gw.share": "Shared models",
|
|
15260
|
+
"gw.shareHint": "Only the ticked models are reachable over the gateway. Unticking one does not just hide it from /v1/models — the gateway refuses it, because the relay key is a single secret shared by every device on the network, and a listing a client can ignore is not a restriction.",
|
|
15261
|
+
"gw.shareNone": "Nothing to offer yet: no model is reachable until an account pool, an aggregate key or a signed-in channel provides one.",
|
|
15262
|
+
"gw.shareAll": "Select all",
|
|
15263
|
+
"gw.shareNoneBtn": "Select none",
|
|
15264
|
+
"gw.shareSave": "Save sharing",
|
|
15265
|
+
"gw.shareSaved": "Gateway sharing updated",
|
|
15266
|
+
"gw.shareDirty": "Not saved yet. A device already using an unticked model will start getting 403.",
|
|
15267
|
+
"gw.shareCount": "{shared} of {total} shared",
|
|
14964
15268
|
"ledger.title": "Data dashboard",
|
|
14965
15269
|
"ledger.sub": "Every number here is counted from calls this pool actually made.",
|
|
14966
15270
|
"ledger.window": "last {count} call(s)",
|
|
@@ -15463,6 +15767,15 @@ window.__ModuleLoader__.load({
|
|
|
15463
15767
|
"gw.usageHint": "任何 OpenAI 兼容的客户端都行:接口地址填上面那个入口,模型从 /v1/models 里挑,密钥填到这个 API 密钥的位置。中继只放行 /v1/models 和 /v1/chat/completions,其余路径在转发任何字节之前就拒掉。",
|
|
15464
15768
|
"gw.loading": "正在读取中继状态……",
|
|
15465
15769
|
"gw.saveFailed": "没能保存",
|
|
15770
|
+
"gw.share": "共享哪些模型",
|
|
15771
|
+
"gw.shareHint": "只有勾选的模型能通过网关访问。取消勾选不只是把它从 /v1/models 里藏起来——网关会直接拒绝它。因为中继密钥是整台网络上所有设备共用的同一把,而一份客户端可以无视的列表算不上限制。",
|
|
15772
|
+
"gw.shareNone": "暂时无可提供:在账号池、聚合渠道密钥或已登录的渠道给出模型之前,这里不会有任何条目。",
|
|
15773
|
+
"gw.shareAll": "全选",
|
|
15774
|
+
"gw.shareNoneBtn": "全不选",
|
|
15775
|
+
"gw.shareSave": "保存共享设置",
|
|
15776
|
+
"gw.shareSaved": "网关共享设置已更新",
|
|
15777
|
+
"gw.shareDirty": "尚未保存。已在使用某个被取消勾选模型的设备,会开始收到 403。",
|
|
15778
|
+
"gw.shareCount": "已共享 {shared} / {total}",
|
|
15466
15779
|
"ledger.title": "数据看板",
|
|
15467
15780
|
"ledger.sub": "这里每个数字,都是这个池子真实发出去的调用数出来的。",
|
|
15468
15781
|
"ledger.window": "最近 {count} 次调用",
|
|
@@ -15647,6 +15960,24 @@ window.__ModuleLoader__.load({
|
|
|
15647
15960
|
}
|
|
15648
15961
|
}, PoolCard));
|
|
15649
15962
|
installNavIcon(ctx, () => t("row.navLabel"));
|
|
15963
|
+
try {
|
|
15964
|
+
ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
|
|
15965
|
+
name: "sidebar.footer.action",
|
|
15966
|
+
id: "xdpool-dock",
|
|
15967
|
+
order: 60,
|
|
15968
|
+
label: "XD Pool"
|
|
15969
|
+
}, SidebarEntry));
|
|
15970
|
+
ctx.slots.inject("shell.overlay", () => ctx.slots.register({
|
|
15971
|
+
name: "shell.overlay",
|
|
15972
|
+
id: "xdpool-dock-panel",
|
|
15973
|
+
order: 60
|
|
15974
|
+
}, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DockedPanel, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PoolCard, { ...settingsScope === void 0 ? { t } : {
|
|
15975
|
+
t,
|
|
15976
|
+
settingsScope
|
|
15977
|
+
} }) })));
|
|
15978
|
+
} catch (error) {
|
|
15979
|
+
console.error("[dsh-xdpool] docked entry unavailable (settings page unaffected):", error);
|
|
15980
|
+
}
|
|
15650
15981
|
} catch (error) {
|
|
15651
15982
|
console.error("[dsh-xdpool] client page failed to load (host provider unaffected):", error);
|
|
15652
15983
|
}
|