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/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.
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
67962
|
-
// 相同即视为同一个账号(上游所有渠道在凭据落定后都用 `updateAccount` 把
|
|
67963
|
-
// nickname 改成真实显示名,所以昵称才是稳定身份,id 每次都是新随机值)。
|
|
68048
|
+
// 是纯追加(无任何去重),于是同一账号攒出多条,账号池页一张卡里显示
|
|
68049
|
+
// 3-4 个一模一样的人。
|
|
67964
68050
|
//
|
|
67965
|
-
// ⚠️
|
|
67966
|
-
//
|
|
67967
|
-
//
|
|
67968
|
-
//
|
|
67969
|
-
//
|
|
67970
|
-
//
|
|
67971
|
-
//
|
|
67972
|
-
//
|
|
67973
|
-
//
|
|
67974
|
-
|
|
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
|
|
67986
|
-
|
|
67987
|
-
|
|
67988
|
-
|
|
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 ===
|
|
68116
|
+
if (kept === void 0) {
|
|
67991
68117
|
groups.set(key, account);
|
|
67992
68118
|
continue;
|
|
67993
68119
|
}
|
|
67994
|
-
const
|
|
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):启动时补静音「黑名单为空但已有账号」的渠道 ──
|