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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-xdpool",
3
3
  "displayName": "DSH XD Pool",
4
4
  "description": "One DSH model pool for every free quota you have: merge all locally signed-in WorkBuddy accounts with auto-failover, sign in to 13 vendors by QR/web, aggregate 76 vendors' free tiers, plus a usage dashboard, model routing, a local OpenAI-compatible gateway and a points-automation suite.",
5
- "version": "2.4.2",
5
+ "version": "2.4.4",
6
6
  "license": "MIT",
7
7
  "author": "XDTrees",
8
8
  "homepage": "https://github.com/XDTrees/dsh-xdpool#readme",
@@ -30936,6 +30936,31 @@ function resolveHideWithoutAccountFlag(raw) {
30936
30936
 
30937
30937
  // vendor/channel-pack/src/remote-catalog-gate.ts
30938
30938
  var REMOTE_CATALOG_COOLDOWN_MS = 3e4;
30939
+ // [xdpool-local] How long a successfully fetched model catalog is trusted.
30940
+ //
30941
+ // Each adapter below caches its catalog for the life of the process ("if
30942
+ // remoteModels is set, return"), so a vendor renaming a model or changing its
30943
+ // multiplier only reached the card after a restart. The rename is often a price
30944
+ // change (the multiplier is part of `name`), which makes a restart-only refresh
30945
+ // a bad way to learn what a model costs. Ten minutes, overridable for testing.
30946
+ var REMOTE_CATALOG_TTL_MS = 6e5;
30947
+ var REMOTE_CATALOG_STAMP = new WeakMap();
30948
+ function remoteModelsStale(owner) {
30949
+ if (owner.remoteModels === void 0) return true;
30950
+ var override = Number(process.env.DSH_XDPOOL_CATALOG_TTL_MS ?? "");
30951
+ var ttl = Number.isFinite(override) && override >= 0 ? override : REMOTE_CATALOG_TTL_MS;
30952
+ var now = Date.now();
30953
+ var stamp = REMOTE_CATALOG_STAMP.get(owner);
30954
+ if (stamp === void 0 || now - stamp < ttl) {
30955
+ if (stamp === void 0) REMOTE_CATALOG_STAMP.set(owner, now);
30956
+ return false;
30957
+ }
30958
+ // Do NOT clear the cache: the loader below only overwrites on success, so a
30959
+ // failed refresh keeps serving the last known good catalog instead of falling
30960
+ // back to bare ids. The gate's 30s cooldown bounds the retry rate.
30961
+ REMOTE_CATALOG_STAMP.set(owner, now);
30962
+ return true;
30963
+ }
30939
30964
  var RemoteCatalogGate = class {
30940
30965
  inFlight;
30941
30966
  retryAt;
@@ -32985,7 +33010,7 @@ var CodeArtsAdapter = class extends LlmAdapter {
32985
33010
  */
32986
33011
  async ensureRemoteModels() {
32987
33012
  const fetchRemote = this.options.fetchRemoteModels;
32988
- if (this.remoteModels !== void 0 || fetchRemote === void 0) return;
33013
+ if (fetchRemote === void 0 || !remoteModelsStale(this)) return;
32989
33014
  await this.catalogGate.run(async () => {
32990
33015
  const models = await fetchRemote();
32991
33016
  if (models.length === 0) return false;
@@ -33141,6 +33166,38 @@ var CodeArtsAdapter = class extends LlmAdapter {
33141
33166
  headers.set("Chat-Id", this.chatId);
33142
33167
  headers.set("Session-Id", this.sessionId);
33143
33168
  headers.set("lang", "en");
33169
+ // [xdpool-local] billion-context(bili)在 DSH 内做上下文压缩,而压缩必须重写请求体;
33170
+ // 本请求的签名覆盖请求体(SDK-HMAC-SHA256),重写即失效。bili 手里没有可重签的凭证,
33171
+ // 于是按它的 compress-or-refuse 契约**本地 403 拒绝转发**,而 pack 侧只看到 403
33172
+ // (httpErrorCode 对 403 无条件返回 "AUTH"),最终被 lib/index.js 的
33173
+ // SESSION_DEAD_MARKERS 归类成「API 密钥无效」—— 用户看到凭证失效,实际凭证完全有效。
33174
+ // 把签名三要素交给 bili,它就能自己重签,上下文压缩得以保留。
33175
+ //
33176
+ // 这两个头不参与华为签名(bili 的 SIGNED_HEADER_NAMES 只有 authorization /
33177
+ // x-sdk-date / x-sdk-content-sha256 / x-security-token / maas_type),且是在
33178
+ // signRequestHuawei 算完签名之后才设进去的,所以不影响上游校验。
33179
+ //
33180
+ // 安全前提(已实测确认):bili 在构造上游请求时**剥离**这两个头并用自己的凭证重签 ——
33181
+ // dist/index.js 的转发头复制循环里显式跳过 APIG_RESIGN_HEADER /
33182
+ // APIG_RESIGN_CREDENTIAL_HEADER,所以 secret_access_key 不会到达上游。
33183
+ //
33184
+ // 默认为什么仍是关:billion-context 只在 optionalDependencies 里,未安装时这两个头
33185
+ // 会直达上游;且 bili 的进程内 native client 自己就会构造这两个头(resignExtra),
33186
+ // 那条路径加不加都不影响它。开关留给「xdpool 直接对接 bili 代理路径」的场合。
33187
+ if (this.options.codeartsResignHeaders === true) {
33188
+ headers.set("x-bili-resign", "sdk-hmac-sha256");
33189
+ headers.set(
33190
+ "x-bili-resign-credential",
33191
+ Buffer.from(
33192
+ JSON.stringify({
33193
+ ak: credential.access_key_id,
33194
+ sk: credential.secret_access_key,
33195
+ token: credential.security_token ?? ""
33196
+ }),
33197
+ "utf8"
33198
+ ).toString("base64")
33199
+ );
33200
+ }
33144
33201
  response = await this.fetchImpl(url, {
33145
33202
  method: "POST",
33146
33203
  headers,
@@ -34977,7 +35034,7 @@ var BuddyAdapter = class _BuddyAdapter extends LlmAdapter2 {
34977
35034
  }
34978
35035
  async ensureRemoteModels() {
34979
35036
  const fetchRemote = this.options.fetchRemoteModels;
34980
- if (this.remoteModels !== void 0 || fetchRemote === void 0) return;
35037
+ if (fetchRemote === void 0 || !remoteModelsStale(this)) return;
34981
35038
  await this.catalogGate.run(async () => {
34982
35039
  const models = await fetchRemote();
34983
35040
  if (models.length === 0) return false;
@@ -35250,9 +35307,30 @@ var BuddyAdapter = class _BuddyAdapter extends LlmAdapter2 {
35250
35307
  }
35251
35308
  }
35252
35309
  const messages = serializeMessages2(options.messages, imageUrls);
35310
+ // [xdpool-local] The WorkBuddy gateways refuse role "developer" with business
35311
+ // code 11128 ("Illegal API invocation from an unapproved channel"), and DSH
35312
+ // sends workspace instructions under exactly that role. Convert in place: role
35313
+ // only, no merging and no reordering, so a rule at the front stays at the
35314
+ // front and one injected mid-conversation stays where it was.
35315
+ for (const message of messages) {
35316
+ if (message.role === "developer") message.role = "system";
35317
+ }
35253
35318
  if (options.system !== void 0 && options.system.length > 0) {
35254
35319
  messages.unshift({ role: "system", content: options.system });
35255
35320
  }
35321
+ // [xdpool-local] The same gateways scan system and assistant CONTENT for
35322
+ // other clients' fingerprints and answer 11128 / "this call failed channel
35323
+ // verification". The match is a case-insensitive substring, so the fix has
35324
+ // to rewrite the phrase, not shift it: a leading newline clears the
35325
+ // start-anchored one and does nothing to the "DeepSeek Harness Web GUI"
35326
+ // pair (measured on a live account 2026-10-21; the pool's own copy of this
35327
+ // list is BANNED_SYSTEM_PHRASES in src/upstream.ts). Runs AFTER the prepend
35328
+ // above, because that is where the host's system text lands.
35329
+ for (const message of messages) {
35330
+ if ((message.role === "system" || message.role === "assistant") && typeof message.content === "string") {
35331
+ message.content = message.content.replace(/powered by DeepSeek Harness/gi, "running on DeepSeek Harness").replace(/DeepSeek Harness Web GUI/gi, "DeepSeek Harness web interface");
35332
+ }
35333
+ }
35256
35334
  const tools = options.tools?.map((tool) => ({
35257
35335
  type: "function",
35258
35336
  function: {
@@ -35493,6 +35571,16 @@ var BuddyAdapter = class _BuddyAdapter extends LlmAdapter2 {
35493
35571
  headers.set("X-IDE-Version", this.product.clientVersion);
35494
35572
  headers.set(HTTP_HEADER_PRODUCT, this.product.attributionName);
35495
35573
  headers.set("User-Agent", resolveUserAgent(this.product, options.model));
35574
+ // [xdpool-local] The international gateway turned its channel verification
35575
+ // from soft to hard: missing ANY one of these three headers and every turn
35576
+ // comes back 400 / 11128 "request illegal", which reaches the user as a
35577
+ // blocked request with nothing wrong in the body. The official client sets all
35578
+ // three on every /v2/chat/completions. `sessionId` is already the process-
35579
+ // stable prompt-cache key, so it is the conversation; the other two are fresh
35580
+ // per request and per message.
35581
+ headers.set("X-Conversation-ID", this.sessionId);
35582
+ headers.set("X-Conversation-Request-ID", crypto.randomUUID());
35583
+ headers.set("X-Conversation-Message-ID", crypto.randomUUID());
35496
35584
  try {
35497
35585
  return await this.fetchImpl(`${this.product.endpoint}/v2/chat/completions`, {
35498
35586
  method: "POST",
@@ -36490,7 +36578,7 @@ var LobsteraiAdapter = class extends LlmAdapter3 {
36490
36578
  */
36491
36579
  async ensureRemoteModels() {
36492
36580
  const fetchRemote = this.options.fetchRemoteModels;
36493
- if (this.remoteModels !== void 0 || fetchRemote === void 0) return;
36581
+ if (fetchRemote === void 0 || !remoteModelsStale(this)) return;
36494
36582
  await this.catalogGate.run(async () => {
36495
36583
  const models = await fetchRemote();
36496
36584
  if (models.length === 0) return false;
@@ -40216,7 +40304,7 @@ var TraeAdapter = class extends LlmAdapter5 {
40216
40304
  /** 懒加载远端模型目录(仅拉取一次)。 */
40217
40305
  async ensureRemoteModels() {
40218
40306
  const fetchRemote = this.options.fetchRemoteModels;
40219
- if (this.remoteModels !== void 0 || fetchRemote === void 0) return;
40307
+ if (fetchRemote === void 0 || !remoteModelsStale(this)) return;
40220
40308
  await this.catalogGate.run(async () => {
40221
40309
  const models = await fetchRemote();
40222
40310
  if (models.length === 0) return false;
@@ -41718,7 +41806,7 @@ var ClineAdapter = class extends LlmAdapter6 {
41718
41806
  * 不去重会打出多份重复的远端请求(两个端点各一次,乘以并发数)。
41719
41807
  */
41720
41808
  async ensureRemoteModels() {
41721
- if (this.remoteModels !== void 0) return;
41809
+ if (!remoteModelsStale(this)) return;
41722
41810
  await this.catalogGate.run(async () => {
41723
41811
  try {
41724
41812
  const credential = await this.options.resolveCredential();
@@ -42385,7 +42473,7 @@ var LoomyAdapter = class extends LlmAdapter7 {
42385
42473
  * 并用 {@link RemoteCatalogGate} 的冷却挡住「每模型重试一次」的放大。
42386
42474
  */
42387
42475
  async loadModels() {
42388
- if (this.remoteModels !== void 0) return this.remoteModels;
42476
+ if (!remoteModelsStale(this)) return this.remoteModels;
42389
42477
  const fetchRemote = this.options.fetchRemoteModels;
42390
42478
  if (fetchRemote !== void 0) {
42391
42479
  await this.catalogGate.run(async () => {
@@ -42841,7 +42929,7 @@ var RaccoonAdapter = class extends LlmAdapter8 {
42841
42929
  * 并用 {@link RemoteCatalogGate} 的冷却挡住「每模型重试一次」的放大。
42842
42930
  */
42843
42931
  async loadModels() {
42844
- if (this.remoteModels !== void 0) return this.remoteModels;
42932
+ if (!remoteModelsStale(this)) return this.remoteModels;
42845
42933
  const fetchRemote = this.options.fetchRemoteModels;
42846
42934
  if (fetchRemote !== void 0) {
42847
42935
  await this.catalogGate.run(async () => {
@@ -54580,7 +54668,7 @@ var ZcodeAdapter = class extends LlmAdapter9 {
54580
54668
  * 并用 {@link RemoteCatalogGate} 的冷却挡住「每模型重试一次」的放大。
54581
54669
  */
54582
54670
  async loadModels() {
54583
- if (this.remoteModels !== void 0) return this.remoteModels;
54671
+ if (!remoteModelsStale(this)) return this.remoteModels;
54584
54672
  const fetchRemote = this.options.fetchRemoteModels;
54585
54673
  if (fetchRemote !== void 0) {
54586
54674
  await this.catalogGate.run(async () => {
@@ -66156,7 +66244,7 @@ var MinimaxAdapter = class extends LlmAdapter11 {
66156
66244
  * 并用 {@link RemoteCatalogGate} 的冷却挡住「每模型重试一次」的放大。
66157
66245
  */
66158
66246
  async loadModels() {
66159
- if (this.remoteModels !== void 0) return this.remoteModels;
66247
+ if (!remoteModelsStale(this)) return this.remoteModels;
66160
66248
  const fetchRemote = this.options.fetchRemoteModels;
66161
66249
  if (fetchRemote !== void 0) {
66162
66250
  await this.catalogGate.run(async () => {
@@ -67955,50 +68043,86 @@ function apply(ctx, config = {}) {
67955
68043
  // 非空(用户勾选的模型已被移出黑名单),不会重复静音。
67956
68044
  let channelPackModelAdaptersRef;
67957
68045
  const rawAddAccount = pool.addAccount.bind(pool);
67958
- // ── 本地适配(dsh-xdpool):清掉同渠道同昵称的重复账号 ──
68046
+ // ── 本地适配(dsh-xdpool):清掉同一账号的重复条目 ──
67959
68047
  // 需求(2026-10-16):用户在扫码登录页反复点「添加账号」,上游 `addAccount`
67960
- // 是纯追加(无任何去重),于是同一渠道攒出多条同号账号,账号池页一张卡里
67961
- // 显示 3-4 个一模一样的人。判定键取 (provider, nickname):同一渠道里昵称
67962
- // 相同即视为同一个账号(上游所有渠道在凭据落定后都用 `updateAccount` 把
67963
- // nickname 改成真实显示名,所以昵称才是稳定身份,id 每次都是新随机值)。
68048
+ // 是纯追加(无任何去重),于是同一账号攒出多条,账号池页一张卡里显示
68049
+ // 3-4 个一模一样的人。
67964
68050
  //
67965
- // ⚠️ 保留哪一条不能只看 createdAt newest。反复点「添加账号」时,**新加的
67966
- // 那条是登录还没走完的半成品**(expiresAt 缺失),而**旧的那条是完整可用
67967
- // 的旧凭据**(zcode 的凭据甚至不带过期时间)。按 newest 保留会留下半成品、
67968
- // 删掉能用的 —— 这正是本机 zcode 的实际形态(10-16 15:15 的三条无
67969
- // expiresAt,16:15 新加的那条也无 expiresAt,而其它渠道都有)。所以判定
67970
- // 优先级改成「可用性」:
67971
- // ① expiresAt 为空视为长期凭据、可用;有值时必须晚于「现在」才算可用;
67972
- // ② 双方都可用 → 留旧的(先登录的那条,凭据久经使用);
67973
- // 旧的已过期而新的可用 → 换新;其余 → 留旧。
67974
- const channelAccountIsUsable = (account, now) => {
68051
+ // ⚠️ 判定键必须是上游身份(凭据里的 `user_id`),不能是昵称。
68052
+ // 2026-10-23 实测教训:按 (provider, nickname) 分组会把**两位不同的人**
68053
+ // 误判成同一个 —— 本机有两个账号昵称都叫「青楫渡」(uid 29890334 与
68054
+ // 8e606182),结果扫进来的第二个被当成重复条目删掉。
68055
+ // 同一份代码还有第二个更严重的后果:新扫进来的凭据只要遇到一条
68056
+ // 「还没过期」的旧条目就会被删(旧规则「双方都可用 → 留旧的」),
68057
+ // 用户重新扫码后账号池毫无变化,卡片继续显示「登录已被拒绝」。
68058
+ //
68059
+ // 保留哪一条:优先「真实条目」而非登录半成品(半成品的 nickname 还是
68060
+ // 随机 id,因为上游要到 `updateAccount` 才写入真实显示名),再优先
68061
+ // 未过期的,最后取最新创建的那条 —— 用户刚完成的那次登录才是他此刻
68062
+ // 在用的凭据。
68063
+ const channelAccountUsable = (account, now) => {
67975
68064
  const expiresAt = Number(account.expiresAt);
67976
68065
  if (!Number.isFinite(expiresAt) || expiresAt <= 0) return true;
67977
68066
  return expiresAt > now;
67978
68067
  };
68068
+ const channelAccountIsPlaceholder = (account) =>
68069
+ typeof account.nickname !== "string" || account.nickname === "" || account.nickname === account.id;
68070
+ const channelAccountCreatedAt = (account) => {
68071
+ const createdAt = Number(account.createdAt);
68072
+ return Number.isFinite(createdAt) ? createdAt : 0;
68073
+ };
68074
+ /** True when `candidate` should survive and `incumbent` should be dropped. */
68075
+ const channelAccountBeats = (candidate, incumbent, now) => {
68076
+ const candidatePlaceholder = channelAccountIsPlaceholder(candidate);
68077
+ const incumbentPlaceholder = channelAccountIsPlaceholder(incumbent);
68078
+ if (candidatePlaceholder !== incumbentPlaceholder) return incumbentPlaceholder;
68079
+ const candidateUsable = channelAccountUsable(candidate, now);
68080
+ const incumbentUsable = channelAccountUsable(incumbent, now);
68081
+ if (candidateUsable !== incumbentUsable) return candidateUsable;
68082
+ return channelAccountCreatedAt(candidate) > channelAccountCreatedAt(incumbent);
68083
+ };
68084
+ /**
68085
+ * Resolve the upstream identity a row belongs to.
68086
+ *
68087
+ * Returns undefined when the credential cannot be read: an unresolvable row
68088
+ * cannot be proven to be a duplicate of anything, and deleting it would trade
68089
+ * one bug for a worse one.
68090
+ */
68091
+ const channelAccountIdentity = async (account) => {
68092
+ const refName = typeof account.credentialRef === "string" ? account.credentialRef : "";
68093
+ if (refName === "") return void 0;
68094
+ try {
68095
+ const credential = await pool.resolveCredentialByRef(refName);
68096
+ if (credential === void 0 || credential === null || typeof credential !== "object") return void 0;
68097
+ const userId = credential.user_id;
68098
+ return typeof userId === "string" && userId !== "" ? userId : void 0;
68099
+ } catch {
68100
+ return void 0;
68101
+ }
68102
+ };
67979
68103
  const dedupeChannelAccounts = async () => {
67980
68104
  try {
67981
68105
  const all = await pool.listAllAccounts();
67982
68106
  const now = Date.now();
67983
- const groups = new Map();
68107
+ const groups = /* @__PURE__ */ new Map();
67984
68108
  for (const account of all) {
67985
- const nickname = typeof account.nickname === "string" ? account.nickname : "";
67986
- // 昵称为空的条目无从判断是否重复(登录尚未落定的中间态),一律保留。
67987
- if (account.provider === "" || nickname === "") continue;
67988
- const key = `${account.provider}\u0000${nickname}`;
68109
+ const provider = typeof account.provider === "string" ? account.provider : "";
68110
+ if (provider === "") continue;
68111
+ const identity = await channelAccountIdentity(account);
68112
+ // 认不出身份的条目一律保留:宁可留一条重复,也不能删掉别人的账号。
68113
+ if (identity === void 0) continue;
68114
+ const key = `${provider}\u0000${identity}`;
67989
68115
  const kept = groups.get(key);
67990
- if (kept === undefined) {
68116
+ if (kept === void 0) {
67991
68117
  groups.set(key, account);
67992
68118
  continue;
67993
68119
  }
67994
- const keptUsable = channelAccountIsUsable(kept, now);
67995
- const accountUsable = channelAccountIsUsable(account, now);
67996
- const replace = !keptUsable && accountUsable;
68120
+ const replace = channelAccountBeats(account, kept, now);
67997
68121
  const drop = replace ? kept : account;
67998
68122
  if (replace) groups.set(key, account);
67999
68123
  await pool.removeAccount(drop.id);
68000
68124
  ctx.logger.info(
68001
- `[channel-pack] ${drop.provider} 重复账号 ${drop.nickname}(${drop.id}):已清理,保留 ${(replace ? account : kept).id}`
68125
+ `[channel-pack] ${drop.provider} 重复账号 ${drop.nickname ?? drop.id}(${drop.id}):已清理,保留 ${(replace ? account : kept).id}`
68002
68126
  );
68003
68127
  }
68004
68128
  } catch (error) {
@@ -68007,6 +68131,7 @@ function apply(ctx, config = {}) {
68007
68131
  );
68008
68132
  }
68009
68133
  };
68134
+
68010
68135
  // 启动时先扫一遍:用户升级前攒下的重复只能靠这一次清掉。
68011
68136
  void Promise.resolve().then(() => dedupeChannelAccounts());
68012
68137
  // ── 本地适配(dsh-xdpool):启动时补静音「黑名单为空但已有账号」的渠道 ──