dsh-xdpool 2.5.0 → 2.6.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/CHANGELOG.md +158 -0
- package/README.en.md +18 -4
- package/README.md +19 -5
- package/assets/community-qr-qq.jpg +0 -0
- package/assets/community-qr-wechat.jpg +0 -0
- package/cordis.patch.yml +17 -41
- package/lib/bin.js +604 -87
- package/lib/client.js +1227 -891
- package/lib/index.d.ts +430 -143
- package/lib/index.js +2388 -1361
- package/package.json +1 -4
- package/vendor/channel-pack/pack.js +222 -16
package/lib/bin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createDecipheriv, createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { copyFile, mkdir, readFile, readdir, rename, rm, writeFile, writeFile as writeFile$1 } from "node:fs/promises";
|
|
3
|
+
import { chmod, 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";
|
|
6
6
|
import { execFile, execFileSync } from "node:child_process";
|
|
@@ -108,24 +108,53 @@ function isAbortError(error) {
|
|
|
108
108
|
}
|
|
109
109
|
/** Cap on the reassembled compaction reply, guarding against a runaway stream. */
|
|
110
110
|
const COMPLETION_TEXT_LIMIT = 65536;
|
|
111
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* Insufficient-credit markers, ASCII lowercase plus the original Chinese.
|
|
113
|
+
*
|
|
114
|
+
* The PLURAL forms are listed separately on purpose: this table is matched with
|
|
115
|
+
* `includes`, so `'credits exhausted'.includes('credit exhausted')` is false.
|
|
116
|
+
* The gateway's own 14018 message is `Credits exhausted.` — plural — which
|
|
117
|
+
* therefore matched nothing, fell through to `soft_rate`, and cooled the account
|
|
118
|
+
* and rotated instead of parking it. A rate-limit cooldown on a drained account
|
|
119
|
+
* means every model on it keeps being retried and keeps failing (#from the
|
|
120
|
+
* connect-workbuddy cross-check).
|
|
121
|
+
*/
|
|
112
122
|
const HARD_CREDIT_MARKERS = [
|
|
113
123
|
"insufficient credit",
|
|
124
|
+
"insufficient credits",
|
|
114
125
|
"no credit",
|
|
126
|
+
"no credits",
|
|
115
127
|
"credit exhausted",
|
|
128
|
+
"credits exhausted",
|
|
129
|
+
"credit exhausted.",
|
|
116
130
|
"out of credit",
|
|
131
|
+
"out of credits",
|
|
117
132
|
"quota exceeded",
|
|
118
133
|
"quota exhaust",
|
|
134
|
+
"quota exhausted",
|
|
119
135
|
"payment required",
|
|
120
136
|
"credit not enough",
|
|
137
|
+
"credits not enough",
|
|
121
138
|
"not enough credit",
|
|
139
|
+
"not enough credits",
|
|
122
140
|
"积分不足",
|
|
123
141
|
"额度不足",
|
|
124
142
|
"余额不足",
|
|
125
143
|
"积分用完",
|
|
126
144
|
"额度用尽",
|
|
127
|
-
"没有积分"
|
|
145
|
+
"没有积分",
|
|
146
|
+
"额度耗尽",
|
|
147
|
+
"积分耗尽"
|
|
128
148
|
];
|
|
149
|
+
/**
|
|
150
|
+
* Business codes that mean "this account cannot pay", independent of wording.
|
|
151
|
+
*
|
|
152
|
+
* `14018` is the gateway's credits-exhausted code; the number survived several
|
|
153
|
+
* rewordings of the sentence around it, so matching it is steadier than matching
|
|
154
|
+
* prose. Read from the body rather than from the HTTP status because the gateway
|
|
155
|
+
* answers these inside a 200 or a 4xx envelope.
|
|
156
|
+
*/
|
|
157
|
+
const HARD_CREDIT_CODES = ["14018", "14003"];
|
|
129
158
|
/** Session-invalidation markers that mean "this credential is dead; use another".
|
|
130
159
|
* Kept alongside the HTTP-status rule in `classifyUpstreamError`: the status is
|
|
131
160
|
* enough for a direct 401/403, but some failures arrive wrapped in a 200
|
|
@@ -370,7 +399,10 @@ const CHAT_IDENTITY_HEADERS = Object.freeze({
|
|
|
370
399
|
const BANNED_SYSTEM_PHRASES = Object.freeze([
|
|
371
400
|
[/powered by DeepSeek Harness/gi, "running on DeepSeek Harness"],
|
|
372
401
|
[/DeepSeek Harness Web GUI/gi, "DeepSeek Harness web interface"],
|
|
373
|
-
[/Relative paths resolve from the workspace root/gi, "Relative paths are resolved against the workspace root"]
|
|
402
|
+
[/Relative paths resolve from the workspace root/gi, "Relative paths are resolved against the workspace root"],
|
|
403
|
+
[/You are a coding agent running in the Codex CLI, a terminal-based coding assistant/gi, "You are a coding agent running in this environment, a terminal-based coding assistant"],
|
|
404
|
+
[/You are a coding agent running in the Codex CLI/gi, "You are a coding agent running in this environment"],
|
|
405
|
+
[/Codex CLI is an open source project led by OpenAI/gi, "This coding agent is an open source project"]
|
|
374
406
|
]);
|
|
375
407
|
/**
|
|
376
408
|
* Rewrite fingerprinted system/assistant content, leaving everything else
|
|
@@ -676,6 +708,7 @@ function classifyUpstreamError(status, body) {
|
|
|
676
708
|
if (status === 402) return "hard_credit";
|
|
677
709
|
if (status === 401 || status === 403) return "session_dead";
|
|
678
710
|
const lower = body.toLowerCase();
|
|
711
|
+
for (const code of HARD_CREDIT_CODES) if (body.includes(`"code":${code}`) || body.includes(`"code": ${code}`) || body.includes(`"code":"${code}"`)) return "hard_credit";
|
|
679
712
|
for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
|
|
680
713
|
for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
|
|
681
714
|
if (status === 429) return "soft_rate";
|
|
@@ -993,7 +1026,8 @@ function parseTask(value) {
|
|
|
993
1026
|
status: typeof raw["status"] === "string" ? raw["status"] : "",
|
|
994
1027
|
claimable: !claimed && target > 0 && current >= target,
|
|
995
1028
|
claimed,
|
|
996
|
-
locked: raw["locked"] === true
|
|
1029
|
+
locked: raw["locked"] === true,
|
|
1030
|
+
jumpUrl: typeof raw["jump_url"] === "string" ? raw["jump_url"] : ""
|
|
997
1031
|
};
|
|
998
1032
|
}
|
|
999
1033
|
var WorkBuddyUpstreamClient = class {
|
|
@@ -2095,6 +2129,92 @@ var WorkBuddyUpstreamClient = class {
|
|
|
2095
2129
|
}
|
|
2096
2130
|
};
|
|
2097
2131
|
//#endregion
|
|
2132
|
+
//#region src/file-perms.ts
|
|
2133
|
+
/**
|
|
2134
|
+
* Write a file that holds credentials, without leaving it world-readable.
|
|
2135
|
+
*
|
|
2136
|
+
* Every credential this plugin keeps on disk — the durable account list, the
|
|
2137
|
+
* ignore list, any mirrored balance — travelled through a plain `writeFile` with
|
|
2138
|
+
* the process umask as the only protection. On a POSIX host that typically means
|
|
2139
|
+
* `0644`: readable by every other user on the machine, and these files carry
|
|
2140
|
+
* `accessToken` / `refreshToken` verbatim.
|
|
2141
|
+
*
|
|
2142
|
+
* Two rules, and the order matters:
|
|
2143
|
+
*
|
|
2144
|
+
* 1. **Create, don't patch.** `mode` on `writeFile` only applies when the file is
|
|
2145
|
+
* created. The atomic-write pattern already writes a fresh temp file and
|
|
2146
|
+
* renames it over the target, so the temp is created with `0600` and the
|
|
2147
|
+
* rename carries that permission to the destination. Chmod'ing afterwards
|
|
2148
|
+
* would also work but leaves a window where the file exists with the old
|
|
2149
|
+
* mode.
|
|
2150
|
+
* 2. **Still chmod best-effort.** A file written by an older version already
|
|
2151
|
+
* exists with `0644`; the rename replaces the inode, so rule 1 covers it —
|
|
2152
|
+
* but a host that keeps the target in place (or a future caller that writes
|
|
2153
|
+
* directly) would not be covered, and `chmod` is cheap.
|
|
2154
|
+
*
|
|
2155
|
+
* WINDOWS: `mode` is ignored by Node on win32, and there is no ACL equivalent in
|
|
2156
|
+
* the standard library. The honest statement is what actually protects these
|
|
2157
|
+
* files there: they live under the user's own profile (%USERPROFILE%, %LOCALAPPDATA%),
|
|
2158
|
+
* whose inherited ACL excludes other standard users by default. Calling `icacls`
|
|
2159
|
+
* per write would spawn a process on every balance update to re-apply a
|
|
2160
|
+
* protection the parent directory already provides, so this module does not. A
|
|
2161
|
+
* profile relocated to a shared directory is outside what this code can detect —
|
|
2162
|
+
* the README says where the files live so the user can check.
|
|
2163
|
+
*
|
|
2164
|
+
* @module dsh-xdpool/file-perms
|
|
2165
|
+
*/
|
|
2166
|
+
/** Owner-only, for a file that holds a secret. */
|
|
2167
|
+
const SECRET_FILE_MODE = 384;
|
|
2168
|
+
/** Owner-only, for the directory that holds them. */
|
|
2169
|
+
const SECRET_DIR_MODE = 448;
|
|
2170
|
+
/**
|
|
2171
|
+
* True on a platform where POSIX modes are meaningful.
|
|
2172
|
+
*
|
|
2173
|
+
* Computed through a `typeof process` guard rather than read straight off the
|
|
2174
|
+
* global: this module is host-only in intent, but `store.ts` imports it and the
|
|
2175
|
+
* browser bundle imports `store.ts` for its row types, so the module body DOES
|
|
2176
|
+
* get bundled for the browser. A bare `process.platform` there throws
|
|
2177
|
+
* `ReferenceError: process is not defined` at first render — and it is the kind
|
|
2178
|
+
* of failure that only shows up in the packaged app.
|
|
2179
|
+
*/
|
|
2180
|
+
const POSIX = typeof process === "undefined" ? false : process.platform !== "win32";
|
|
2181
|
+
/**
|
|
2182
|
+
* Create the directory a secret lives in, owner-only.
|
|
2183
|
+
*
|
|
2184
|
+
* `recursive` so a fresh install works, and the mode applies to every directory
|
|
2185
|
+
* this call creates. An existing directory keeps its mode — `mkdir` does not
|
|
2186
|
+
* change what is already there — so `chmod` runs too, best-effort: tightening a
|
|
2187
|
+
* directory that a previous version created permissively is exactly the case
|
|
2188
|
+
* worth fixing, and a failure is not worth aborting a save over.
|
|
2189
|
+
*/
|
|
2190
|
+
async function ensureSecretDir(dir) {
|
|
2191
|
+
await mkdir(dir, {
|
|
2192
|
+
recursive: true,
|
|
2193
|
+
...POSIX ? { mode: SECRET_DIR_MODE } : {}
|
|
2194
|
+
});
|
|
2195
|
+
if (!POSIX) return;
|
|
2196
|
+
try {
|
|
2197
|
+
await chmod(dir, SECRET_DIR_MODE);
|
|
2198
|
+
} catch {}
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Write `contents` to `path` with owner-only permissions.
|
|
2202
|
+
*
|
|
2203
|
+
* Not atomic on its own — callers that need the crash-safe temp+rename dance keep
|
|
2204
|
+
* doing it and just route the temp write through here, which is why the mode is
|
|
2205
|
+
* applied at creation: the rename then carries it to the final file.
|
|
2206
|
+
*/
|
|
2207
|
+
async function writeSecretFile(path, contents) {
|
|
2208
|
+
await writeFile$1(path, contents, POSIX ? {
|
|
2209
|
+
encoding: "utf8",
|
|
2210
|
+
mode: SECRET_FILE_MODE
|
|
2211
|
+
} : "utf8");
|
|
2212
|
+
if (!POSIX) return;
|
|
2213
|
+
try {
|
|
2214
|
+
await chmod(path, SECRET_FILE_MODE);
|
|
2215
|
+
} catch {}
|
|
2216
|
+
}
|
|
2217
|
+
//#endregion
|
|
2098
2218
|
//#region src/ignored.ts
|
|
2099
2219
|
/**
|
|
2100
2220
|
* The permanent ignore list: accounts the user has thrown out of the pool.
|
|
@@ -2195,9 +2315,9 @@ async function writeIgnoredAccounts(accounts, path = ignoredIdsPath()) {
|
|
|
2195
2315
|
version: 1,
|
|
2196
2316
|
accounts
|
|
2197
2317
|
}, null, 2);
|
|
2198
|
-
await
|
|
2318
|
+
await ensureSecretDir(dirname(path));
|
|
2199
2319
|
const temp = `${path}.tmp`;
|
|
2200
|
-
await
|
|
2320
|
+
await writeSecretFile(temp, `${payload}\n`);
|
|
2201
2321
|
await rename(temp, path);
|
|
2202
2322
|
}
|
|
2203
2323
|
/**
|
|
@@ -2323,9 +2443,9 @@ async function saveStoredAccounts(path, accounts) {
|
|
|
2323
2443
|
version: 1,
|
|
2324
2444
|
accounts: stored
|
|
2325
2445
|
}, null, 2) + "\n";
|
|
2326
|
-
await
|
|
2446
|
+
await ensureSecretDir(join(path, ".."));
|
|
2327
2447
|
const tmp = `${path}.tmp`;
|
|
2328
|
-
await
|
|
2448
|
+
await writeSecretFile(tmp, document);
|
|
2329
2449
|
await rename(tmp, path);
|
|
2330
2450
|
}
|
|
2331
2451
|
//#endregion
|
|
@@ -3089,16 +3209,20 @@ function parseWorkBuddyAuth(text, sourcePath, decrypt) {
|
|
|
3089
3209
|
const refreshExpiresAtMs = typeof auth["refreshExpiresAt"] === "number" ? expiryToMs(auth["refreshExpiresAt"]) : void 0;
|
|
3090
3210
|
if (refreshExpiresAtMs !== void 0 && refreshExpiresAtMs > 0 && refreshExpiresAtMs < Date.now()) return;
|
|
3091
3211
|
const lastRefreshAtMs = typeof auth["lastRefreshTime"] === "number" ? expiryToMs(auth["lastRefreshTime"]) : void 0;
|
|
3212
|
+
const nickname = optionalString(decryptableString(identity["nickname"], decrypt).value);
|
|
3213
|
+
const uin = optionalString(identity["uin"]);
|
|
3214
|
+
const uid = optionalString(identity["uid"]);
|
|
3215
|
+
const enterpriseId = optionalString(identity["enterpriseId"]);
|
|
3092
3216
|
return {
|
|
3093
3217
|
accessToken,
|
|
3094
3218
|
refreshToken: decryptableString(auth["refreshToken"], decrypt).value,
|
|
3095
3219
|
expiresAtMs: typeof auth["expiresAt"] === "number" ? expiryToMs(auth["expiresAt"]) : 0,
|
|
3096
3220
|
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
3097
3221
|
...lastRefreshAtMs === void 0 ? {} : { lastRefreshAtMs },
|
|
3098
|
-
...
|
|
3099
|
-
...
|
|
3100
|
-
...
|
|
3101
|
-
...
|
|
3222
|
+
...nickname === void 0 ? {} : { nickname },
|
|
3223
|
+
...uin === void 0 ? {} : { uin },
|
|
3224
|
+
...uid === void 0 ? {} : { uid },
|
|
3225
|
+
...enterpriseId === void 0 ? {} : { enterpriseId },
|
|
3102
3226
|
domain: typeof auth["domain"] === "string" ? auth["domain"] : "",
|
|
3103
3227
|
sourcePath
|
|
3104
3228
|
};
|
|
@@ -3443,6 +3567,16 @@ var WorkBuddyAccountPool = class {
|
|
|
3443
3567
|
*/
|
|
3444
3568
|
disabledIds = /* @__PURE__ */ new Set();
|
|
3445
3569
|
/**
|
|
3570
|
+
* The account ids the store held after the last successful write.
|
|
3571
|
+
*
|
|
3572
|
+
* Read back on the next scan to say WHICH accounts left, not just how many.
|
|
3573
|
+
* The list is written wholesale, so a scan that does not find an account
|
|
3574
|
+
* deletes it — and that account's credential is then unrecoverable without a
|
|
3575
|
+
* fresh sign-in (#117). `undefined` until the first write, so the first scan
|
|
3576
|
+
* never reports every account as "leave".
|
|
3577
|
+
*/
|
|
3578
|
+
lastPersistedIds;
|
|
3579
|
+
/**
|
|
3446
3580
|
* Account ids the user threw out of the pool for good.
|
|
3447
3581
|
*
|
|
3448
3582
|
* Enforced BEFORE the credential is parsed: `scan()` skips a file whose
|
|
@@ -3519,6 +3653,11 @@ var WorkBuddyAccountPool = class {
|
|
|
3519
3653
|
* A function rather than a list because that pool is mounted asynchronously
|
|
3520
3654
|
* and can be re-read on demand; `undefined` means "no such source", which is
|
|
3521
3655
|
* the state every test and every pre-mount startup is in.
|
|
3656
|
+
*
|
|
3657
|
+
* The `| undefined` is explicit because `applyExternalSource` CLEARS the field by
|
|
3658
|
+
* assigning `undefined` — under `exactOptionalPropertyTypes` a bare `?:` means
|
|
3659
|
+
* "may be absent, never undefined", which made that documented clearing action
|
|
3660
|
+
* a type error.
|
|
3522
3661
|
*/
|
|
3523
3662
|
externalSource;
|
|
3524
3663
|
constructor(options = {}) {
|
|
@@ -3624,9 +3763,10 @@ var WorkBuddyAccountPool = class {
|
|
|
3624
3763
|
}
|
|
3625
3764
|
const byId = /* @__PURE__ */ new Map();
|
|
3626
3765
|
for (const account of this.accounts) byId.set(account.id, account);
|
|
3627
|
-
if (this.persistent
|
|
3766
|
+
if (this.persistent) {
|
|
3628
3767
|
const stored = await loadStoredAccounts(this.storePath);
|
|
3629
3768
|
for (const entry of stored) {
|
|
3769
|
+
if (this.ignoredIds.size > 0 && this.ignoredIds.has(entry.id)) continue;
|
|
3630
3770
|
if (byId.has(entry.id)) continue;
|
|
3631
3771
|
byId.set(entry.id, {
|
|
3632
3772
|
id: entry.id,
|
|
@@ -3676,7 +3816,10 @@ var WorkBuddyAccountPool = class {
|
|
|
3676
3816
|
this.accounts = ordered;
|
|
3677
3817
|
this.lastScanAtMs = Date.now();
|
|
3678
3818
|
if (this.persistent) try {
|
|
3819
|
+
const leaving = this.lastPersistedIds === void 0 ? [] : [...this.lastPersistedIds].filter((id) => !this.accounts.some((account) => account.id === id));
|
|
3820
|
+
if (leaving.length > 0) this.logger?.warn?.(`dsh-xdpool: ${String(leaving.length)} account(s) are no longer in the pool after a rescan and were removed from the store: ${leaving.join(", ")}. Their credentials are gone unless they are signed in again.`);
|
|
3679
3821
|
await saveStoredAccounts(this.storePath, this.accounts);
|
|
3822
|
+
this.lastPersistedIds = new Set(this.accounts.map((account) => account.id));
|
|
3680
3823
|
} catch (error) {
|
|
3681
3824
|
this.logger?.warn?.("dsh-xdpool: could not persist the account store", error);
|
|
3682
3825
|
}
|
|
@@ -3696,11 +3839,11 @@ var WorkBuddyAccountPool = class {
|
|
|
3696
3839
|
* model id the legacy account-wide check applies (callers that cannot name a
|
|
3697
3840
|
* model, e.g. CLI diagnostics).
|
|
3698
3841
|
*/
|
|
3699
|
-
available(now, modelId, region) {
|
|
3842
|
+
available(now, modelId, region, reserveExempt) {
|
|
3700
3843
|
return this.accounts.filter((account) => {
|
|
3701
3844
|
if (this.disabledIds.has(account.id)) return false;
|
|
3702
3845
|
const reserve = this.creditReserves.get(account.id);
|
|
3703
|
-
if (reserve !== void 0 && reserve > 0) {
|
|
3846
|
+
if (!reserveExempt?.has(account.id) && reserve !== void 0 && reserve > 0) {
|
|
3704
3847
|
const balance = this.creditBalances.get(account.id);
|
|
3705
3848
|
if (balance !== void 0 && balance <= reserve) return false;
|
|
3706
3849
|
}
|
|
@@ -3724,7 +3867,7 @@ var WorkBuddyAccountPool = class {
|
|
|
3724
3867
|
* Counts are over the region's accounts, since a provider only ever sees its
|
|
3725
3868
|
* own gateway.
|
|
3726
3869
|
*/
|
|
3727
|
-
unavailableReason(modelId, region) {
|
|
3870
|
+
unavailableReason(modelId, region, reserveExempt) {
|
|
3728
3871
|
const now = Date.now();
|
|
3729
3872
|
const inRegion = this.accounts.filter((account) => region === void 0 || regionOf(account.credential.domain) === region);
|
|
3730
3873
|
if (inRegion.length === 0) return {
|
|
@@ -3737,6 +3880,29 @@ var WorkBuddyAccountPool = class {
|
|
|
3737
3880
|
let cooling = 0;
|
|
3738
3881
|
let disabled = 0;
|
|
3739
3882
|
let dead = 0;
|
|
3883
|
+
/**
|
|
3884
|
+
* Accounts that survived every OTHER filter and are held back only by their
|
|
3885
|
+
* credit floor.
|
|
3886
|
+
*
|
|
3887
|
+
* Counted rather than returned on the first hit. The old `return` fired the
|
|
3888
|
+
* moment it met one reserved account, so a single account parked under its
|
|
3889
|
+
* floor reported `reserve` for the whole gateway — even while four other
|
|
3890
|
+
* accounts were healthy and had just been tried. The caller turns
|
|
3891
|
+
* `reason === 'reserve'` into a 402 telling the user to lower the reserve,
|
|
3892
|
+
* which pointed at a setting that was not the problem (#98: a five-account
|
|
3893
|
+
* pool reported `reserve_floor` because of one account that had sat below
|
|
3894
|
+
* its floor for weeks, while the actual state was "the rest are cooling").
|
|
3895
|
+
*/
|
|
3896
|
+
let reserved = 0;
|
|
3897
|
+
/**
|
|
3898
|
+
* Accounts that reached the end of the walk unblocked.
|
|
3899
|
+
*
|
|
3900
|
+
* Non-zero means the pool still has somewhere to send a request, so a
|
|
3901
|
+
* reserve is NOT what this answer is about: editing a number cannot fix a
|
|
3902
|
+
* request that some other account is already able to serve, and saying
|
|
3903
|
+
* otherwise sends the reader to the wrong setting.
|
|
3904
|
+
*/
|
|
3905
|
+
let usable = 0;
|
|
3740
3906
|
for (const account of inRegion) {
|
|
3741
3907
|
if (this.disabledIds.has(account.id)) {
|
|
3742
3908
|
disabled += 1;
|
|
@@ -3752,16 +3918,14 @@ var WorkBuddyAccountPool = class {
|
|
|
3752
3918
|
continue;
|
|
3753
3919
|
}
|
|
3754
3920
|
const reserve = this.creditReserves.get(account.id);
|
|
3755
|
-
if (reserve !== void 0 && reserve > 0) {
|
|
3921
|
+
if (!reserveExempt?.has(account.id) && reserve !== void 0 && reserve > 0) {
|
|
3756
3922
|
const balance = this.creditBalances.get(account.id);
|
|
3757
|
-
if (balance !== void 0 && balance <= reserve)
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
dead,
|
|
3762
|
-
reason: "reserve"
|
|
3763
|
-
};
|
|
3923
|
+
if (balance !== void 0 && balance <= reserve) {
|
|
3924
|
+
reserved += 1;
|
|
3925
|
+
continue;
|
|
3926
|
+
}
|
|
3764
3927
|
}
|
|
3928
|
+
usable += 1;
|
|
3765
3929
|
}
|
|
3766
3930
|
if (dead > 0) return {
|
|
3767
3931
|
total: inRegion.length,
|
|
@@ -3784,6 +3948,13 @@ var WorkBuddyAccountPool = class {
|
|
|
3784
3948
|
dead,
|
|
3785
3949
|
reason: "disabled"
|
|
3786
3950
|
};
|
|
3951
|
+
if (usable === 0 && reserved > 0) return {
|
|
3952
|
+
total: inRegion.length,
|
|
3953
|
+
cooling,
|
|
3954
|
+
disabled,
|
|
3955
|
+
dead,
|
|
3956
|
+
reason: "reserve"
|
|
3957
|
+
};
|
|
3787
3958
|
return {
|
|
3788
3959
|
total: inRegion.length,
|
|
3789
3960
|
cooling,
|
|
@@ -3960,16 +4131,18 @@ var WorkBuddyAccountPool = class {
|
|
|
3960
4131
|
* Scans on first use, and rescans when every known account is cooling down: a
|
|
3961
4132
|
* fresh desktop login is the usual way out of an exhausted pool.
|
|
3962
4133
|
*
|
|
4134
|
+
* `reserveExempt` is host-only: account ids whose own credentials freshly
|
|
4135
|
+
* confirmed a zero-credit model. It never bypasses disabled, dead credentials, cooldowns or region.
|
|
3963
4136
|
* `conversationKey` is only consulted under `sticky`; other modes ignore it.
|
|
3964
4137
|
*/
|
|
3965
|
-
async acquire(modelId, region, conversationKey) {
|
|
4138
|
+
async acquire(modelId, region, conversationKey, reserveExempt) {
|
|
3966
4139
|
if (this.accounts.length === 0) await this.scan();
|
|
3967
4140
|
const maxTries = this.accounts.length + 1;
|
|
3968
4141
|
for (let attempt = 0; attempt < maxTries; attempt += 1) {
|
|
3969
|
-
let pool = this.available(Date.now(), modelId, region);
|
|
4142
|
+
let pool = this.available(Date.now(), modelId, region, reserveExempt);
|
|
3970
4143
|
if (pool.length === 0 && attempt === 0) {
|
|
3971
4144
|
await this.scan();
|
|
3972
|
-
pool = this.available(Date.now(), modelId, region);
|
|
4145
|
+
pool = this.available(Date.now(), modelId, region, reserveExempt);
|
|
3973
4146
|
}
|
|
3974
4147
|
if (pool.length === 0) return void 0;
|
|
3975
4148
|
if (this.preferredAccountId !== void 0) {
|
|
@@ -4162,6 +4335,43 @@ var WorkBuddyAccountPool = class {
|
|
|
4162
4335
|
return this.accounts.filter((account) => (account.credentialDeadUntilMs ?? 0) > now);
|
|
4163
4336
|
}
|
|
4164
4337
|
/**
|
|
4338
|
+
* Try to bring benched accounts back WITHOUT waiting out the cooldown.
|
|
4339
|
+
*
|
|
4340
|
+
* The cooldown is a guess about how long a problem lasts, and the guess is
|
|
4341
|
+
* usually wrong in one direction or the other: a token revoked by a password
|
|
4342
|
+
* change stays dead for the full window even after the user signs in again in
|
|
4343
|
+
* some other client, and a transient upstream hiccup benches a perfectly good
|
|
4344
|
+
* account for half an hour.
|
|
4345
|
+
*
|
|
4346
|
+
* So instead of only waiting, ask. For each benched account, one cheap
|
|
4347
|
+
* authenticated read: if it answers, the credential works and the account goes
|
|
4348
|
+
* back into rotation immediately. Nothing about the failure is trusted — only a
|
|
4349
|
+
* successful call clears it — and a probe that fails keeps the original
|
|
4350
|
+
* cooldown untouched (probing must never EXTEND a bench, or a broken endpoint
|
|
4351
|
+
* would keep every account locked out for as long as it stayed broken).
|
|
4352
|
+
*
|
|
4353
|
+
* `probe` is injected rather than imported so this class stays free of the
|
|
4354
|
+
* upstream client: the pool knows about accounts, not about HTTP.
|
|
4355
|
+
*
|
|
4356
|
+
* Returns the accounts it revived, for logging and for the run summary.
|
|
4357
|
+
*/
|
|
4358
|
+
async reviveBenched(probe) {
|
|
4359
|
+
const benched = this.deadCredentials();
|
|
4360
|
+
if (benched.length === 0) return [];
|
|
4361
|
+
const revived = [];
|
|
4362
|
+
for (const account of benched) {
|
|
4363
|
+
try {
|
|
4364
|
+
await probe(account.credential);
|
|
4365
|
+
} catch {
|
|
4366
|
+
continue;
|
|
4367
|
+
}
|
|
4368
|
+
this.clearCredentialDead(account.id);
|
|
4369
|
+
revived.push(account);
|
|
4370
|
+
this.logger?.info?.(`dsh-xdpool: ${account.label} answered a probe; back in rotation without waiting out the cooldown`);
|
|
4371
|
+
}
|
|
4372
|
+
return revived;
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4165
4375
|
* Refresh the account's access token when it is within the margin (or already
|
|
4166
4376
|
* expired), in-flight de-duped per account. A failed refresh keeps the
|
|
4167
4377
|
* existing token when it has not yet expired, so an unreachable refresh
|
|
@@ -4308,8 +4518,23 @@ ${new Date(until).toISOString()}
|
|
|
4308
4518
|
* (`kimi-k3-1`). A stale fallback is worse than a short one — it looks
|
|
4309
4519
|
* authoritative while being wrong.
|
|
4310
4520
|
*
|
|
4311
|
-
*
|
|
4312
|
-
*
|
|
4521
|
+
* NO PRICES AND NO PROMO BADGES HERE. This table is what the picker shows when
|
|
4522
|
+
* the startup fetch fails, and a multiplier captured months ago is a price the
|
|
4523
|
+
* user will budget against while it is quietly wrong. Two specific harms:
|
|
4524
|
+
*
|
|
4525
|
+
* - `multiplier: 0` renders the "free" badge, so a model that was free at
|
|
4526
|
+
* capture time keeps claiming to be free after the promotion ends;
|
|
4527
|
+
* - a non-zero multiplier renders next to the model, so a stale figure reads
|
|
4528
|
+
* as the current rate.
|
|
4529
|
+
*
|
|
4530
|
+
* `undefined` is the honest state: the card prints no rate and no badge, which
|
|
4531
|
+
* is exactly true — this build does not know the price. The live fetch fills
|
|
4532
|
+
* both in within a second of a working startup, and `catalogSource` already
|
|
4533
|
+
* tells the card to say the list is the built-in one.
|
|
4534
|
+
*
|
|
4535
|
+
* Windows, output ceilings and the `craft` capability tag stay: those are
|
|
4536
|
+
* properties of the model, not of a campaign, and they are what makes the list
|
|
4537
|
+
* usable offline at all.
|
|
4313
4538
|
*/
|
|
4314
4539
|
const FALLBACK_WORKBUDDY_MODELS = [
|
|
4315
4540
|
{
|
|
@@ -4326,8 +4551,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
4326
4551
|
contextWindow: 96e4,
|
|
4327
4552
|
maxOutputTokens: 64e3,
|
|
4328
4553
|
supportsImages: true,
|
|
4329
|
-
|
|
4330
|
-
tags: ["craft", "badge:夜间免费:#FF0000"]
|
|
4554
|
+
tags: ["craft"]
|
|
4331
4555
|
},
|
|
4332
4556
|
{
|
|
4333
4557
|
id: "hy3",
|
|
@@ -4335,40 +4559,35 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
4335
4559
|
contextWindow: 192e3,
|
|
4336
4560
|
maxOutputTokens: 64e3,
|
|
4337
4561
|
supportsImages: true,
|
|
4338
|
-
|
|
4339
|
-
tags: ["craft", "badge:限时免费:#FF0000"]
|
|
4562
|
+
tags: ["craft"]
|
|
4340
4563
|
},
|
|
4341
4564
|
{
|
|
4342
4565
|
id: "hy3-x",
|
|
4343
4566
|
name: "Hy3",
|
|
4344
4567
|
contextWindow: 192e3,
|
|
4345
4568
|
maxOutputTokens: 64e3,
|
|
4346
|
-
supportsImages: true
|
|
4347
|
-
multiplier: .05
|
|
4569
|
+
supportsImages: true
|
|
4348
4570
|
},
|
|
4349
4571
|
{
|
|
4350
4572
|
id: "space-bunny",
|
|
4351
4573
|
name: "Space-Bunny",
|
|
4352
4574
|
contextWindow: 1e6,
|
|
4353
4575
|
maxOutputTokens: 64e3,
|
|
4354
|
-
supportsImages: true
|
|
4355
|
-
multiplier: .08
|
|
4576
|
+
supportsImages: true
|
|
4356
4577
|
},
|
|
4357
4578
|
{
|
|
4358
4579
|
id: "deepseek-v4.1-flash",
|
|
4359
4580
|
name: "Deepseek-V4.1-Flash",
|
|
4360
4581
|
contextWindow: 1e6,
|
|
4361
4582
|
maxOutputTokens: 128e3,
|
|
4362
|
-
supportsImages: true
|
|
4363
|
-
multiplier: .11
|
|
4583
|
+
supportsImages: true
|
|
4364
4584
|
},
|
|
4365
4585
|
{
|
|
4366
4586
|
id: "deepseek-v4-pro",
|
|
4367
4587
|
name: "Deepseek-V4-Pro",
|
|
4368
4588
|
contextWindow: 1e6,
|
|
4369
4589
|
maxOutputTokens: 128e3,
|
|
4370
|
-
supportsImages: true
|
|
4371
|
-
multiplier: .51
|
|
4590
|
+
supportsImages: true
|
|
4372
4591
|
},
|
|
4373
4592
|
{
|
|
4374
4593
|
id: "deepseek-v4-flash",
|
|
@@ -4382,16 +4601,14 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
4382
4601
|
name: "GLM-5.3",
|
|
4383
4602
|
contextWindow: 1e6,
|
|
4384
4603
|
maxOutputTokens: 64e3,
|
|
4385
|
-
supportsImages: true
|
|
4386
|
-
multiplier: .79
|
|
4604
|
+
supportsImages: true
|
|
4387
4605
|
},
|
|
4388
4606
|
{
|
|
4389
4607
|
id: "glm-5.3-flash",
|
|
4390
4608
|
name: "GLM-5.3-Flash",
|
|
4391
4609
|
contextWindow: 1e6,
|
|
4392
4610
|
maxOutputTokens: 131072,
|
|
4393
|
-
supportsImages: true
|
|
4394
|
-
multiplier: .06
|
|
4611
|
+
supportsImages: true
|
|
4395
4612
|
},
|
|
4396
4613
|
{
|
|
4397
4614
|
id: "glm-5.2",
|
|
@@ -4399,64 +4616,56 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
4399
4616
|
contextWindow: 1e6,
|
|
4400
4617
|
maxOutputTokens: 64e3,
|
|
4401
4618
|
supportsImages: true,
|
|
4402
|
-
|
|
4403
|
-
tags: ["craft", "badge:夜间折扣:#1E90FF"]
|
|
4619
|
+
tags: ["craft"]
|
|
4404
4620
|
},
|
|
4405
4621
|
{
|
|
4406
4622
|
id: "glm-5.1",
|
|
4407
4623
|
name: "GLM-5.1",
|
|
4408
4624
|
contextWindow: 2e5,
|
|
4409
4625
|
maxOutputTokens: 48e3,
|
|
4410
|
-
supportsImages: false
|
|
4411
|
-
multiplier: .79
|
|
4626
|
+
supportsImages: false
|
|
4412
4627
|
},
|
|
4413
4628
|
{
|
|
4414
4629
|
id: "glm-5v-turbo",
|
|
4415
4630
|
name: "GLM-5v-Turbo",
|
|
4416
4631
|
contextWindow: 2e5,
|
|
4417
4632
|
maxOutputTokens: 64e3,
|
|
4418
|
-
supportsImages: true
|
|
4419
|
-
multiplier: .71
|
|
4633
|
+
supportsImages: true
|
|
4420
4634
|
},
|
|
4421
4635
|
{
|
|
4422
4636
|
id: "kimi-k3-1",
|
|
4423
4637
|
name: "Kimi-K3",
|
|
4424
4638
|
contextWindow: 1e6,
|
|
4425
4639
|
maxOutputTokens: 32e3,
|
|
4426
|
-
supportsImages: true
|
|
4427
|
-
multiplier: 1.62
|
|
4640
|
+
supportsImages: true
|
|
4428
4641
|
},
|
|
4429
4642
|
{
|
|
4430
4643
|
id: "kimi-k2.8-preview",
|
|
4431
4644
|
name: "Kimi-K2.8-Preview",
|
|
4432
4645
|
contextWindow: 1e6,
|
|
4433
4646
|
maxOutputTokens: 64e3,
|
|
4434
|
-
supportsImages: true
|
|
4435
|
-
multiplier: .77
|
|
4647
|
+
supportsImages: true
|
|
4436
4648
|
},
|
|
4437
4649
|
{
|
|
4438
4650
|
id: "kimi-k2.7",
|
|
4439
4651
|
name: "Kimi-K2.7-Code",
|
|
4440
4652
|
contextWindow: 256e3,
|
|
4441
4653
|
maxOutputTokens: 32e3,
|
|
4442
|
-
supportsImages: true
|
|
4443
|
-
multiplier: .57
|
|
4654
|
+
supportsImages: true
|
|
4444
4655
|
},
|
|
4445
4656
|
{
|
|
4446
4657
|
id: "kimi-k2.6",
|
|
4447
4658
|
name: "Kimi-K2.6",
|
|
4448
4659
|
contextWindow: 256e3,
|
|
4449
4660
|
maxOutputTokens: 32e3,
|
|
4450
|
-
supportsImages: true
|
|
4451
|
-
multiplier: .52
|
|
4661
|
+
supportsImages: true
|
|
4452
4662
|
},
|
|
4453
4663
|
{
|
|
4454
4664
|
id: "minimax-m3",
|
|
4455
4665
|
name: "MiniMax-M3",
|
|
4456
4666
|
contextWindow: 512e3,
|
|
4457
4667
|
maxOutputTokens: 64e3,
|
|
4458
|
-
supportsImages: true
|
|
4459
|
-
multiplier: .25
|
|
4668
|
+
supportsImages: true
|
|
4460
4669
|
}
|
|
4461
4670
|
];
|
|
4462
4671
|
/**
|
|
@@ -4529,11 +4738,24 @@ var WorkBuddyCatalog = class {
|
|
|
4529
4738
|
}
|
|
4530
4739
|
/**
|
|
4531
4740
|
* The models DSH should actually offer, after applying the user's selection:
|
|
4532
|
-
* disabled models are dropped, an explicit image list
|
|
4533
|
-
*
|
|
4741
|
+
* disabled models are dropped, an explicit image list decides image support,
|
|
4742
|
+
* and a per-model budget caps the advertised window.
|
|
4534
4743
|
*
|
|
4535
4744
|
* An absent `enabledModelIds` means "everything" — a fresh install with no
|
|
4536
4745
|
* saved selection must not present an empty picker.
|
|
4746
|
+
*
|
|
4747
|
+
* IMAGE SUPPORT IS THE USER'S SET, NOT AN OR. The upstream flag is `true` for
|
|
4748
|
+
* essentially every model of this vendor (measured: 15 of 16 on CN, 23 of 23
|
|
4749
|
+
* on international), so `supportsImages || imageSet.has(id)` could only ever
|
|
4750
|
+
* turn the flag ON — un-ticking the box in the card changed nothing, and the
|
|
4751
|
+
* adapter kept advertising `['text','image']`. The checkbox existed and did
|
|
4752
|
+
* not work, which is worse than not offering it.
|
|
4753
|
+
*
|
|
4754
|
+
* So when the user HAS saved an image selection, that set is the answer, in
|
|
4755
|
+
* both directions. When they have not (undefined — a fresh install, or a
|
|
4756
|
+
* config written before this fix), the upstream capability stands: silently
|
|
4757
|
+
* disabling image input for everyone who never touched the switch would be a
|
|
4758
|
+
* regression, and the absent case is exactly "no opinion".
|
|
4537
4759
|
*/
|
|
4538
4760
|
visible() {
|
|
4539
4761
|
const enabled = this.selection.enabledModelIds;
|
|
@@ -4543,7 +4765,7 @@ var WorkBuddyCatalog = class {
|
|
|
4543
4765
|
const budgets = this.selection.contextBudgets;
|
|
4544
4766
|
return this.models.filter((model) => allow === void 0 || allow.has(model.id)).map((model) => {
|
|
4545
4767
|
const next = { ...model };
|
|
4546
|
-
if (imageSet !== void 0) next.supportsImages =
|
|
4768
|
+
if (imageSet !== void 0) next.supportsImages = imageSet.has(model.id);
|
|
4547
4769
|
const budget = budgets?.[model.id];
|
|
4548
4770
|
if (budget !== void 0 && budget > 0 && budget < next.contextWindow) next.contextWindow = budget;
|
|
4549
4771
|
return next;
|
|
@@ -4648,8 +4870,13 @@ function catalogFromUpstream(models, region = "cn") {
|
|
|
4648
4870
|
if (models.length === 0) return fallbackModelsFor(region);
|
|
4649
4871
|
return models.map(toModelInfo);
|
|
4650
4872
|
}
|
|
4651
|
-
|
|
4652
|
-
|
|
4873
|
+
Object.freeze([
|
|
4874
|
+
"priority",
|
|
4875
|
+
"round-robin",
|
|
4876
|
+
"balanced",
|
|
4877
|
+
"sticky",
|
|
4878
|
+
"expiry"
|
|
4879
|
+
]);
|
|
4653
4880
|
/**
|
|
4654
4881
|
* The schedule every automation job falls back to.
|
|
4655
4882
|
*
|
|
@@ -9608,6 +9835,24 @@ const PROVIDERS = Object.freeze([
|
|
|
9608
9835
|
"website": "https://openai.com/",
|
|
9609
9836
|
"local": false,
|
|
9610
9837
|
"source": "fcc"
|
|
9838
|
+
},
|
|
9839
|
+
{
|
|
9840
|
+
"id": "custom_openai",
|
|
9841
|
+
"name": "Custom OpenAI-compatible",
|
|
9842
|
+
"category": "apikey",
|
|
9843
|
+
"auth": {
|
|
9844
|
+
"kind": "apiKey",
|
|
9845
|
+
"env": "CUSTOM_OPENAI_API_KEY",
|
|
9846
|
+
"url": null
|
|
9847
|
+
},
|
|
9848
|
+
"baseUrl": "",
|
|
9849
|
+
"format": "openai-chat",
|
|
9850
|
+
"models": [],
|
|
9851
|
+
"notes": "Any OpenAI-compatible endpoint — vLLM, LiteLLM, one-api, LM Studio, a self-hosted gateway. Paste the base URL (ending in /v1) and the key on this card, then press Fetch models.",
|
|
9852
|
+
"website": null,
|
|
9853
|
+
"local": false,
|
|
9854
|
+
"baseUrlOverrideEnv": "CUSTOM_OPENAI_BASE_URL",
|
|
9855
|
+
"custom": true
|
|
9611
9856
|
}
|
|
9612
9857
|
].map((entry) => Object.freeze({
|
|
9613
9858
|
...entry,
|
|
@@ -9967,6 +10212,71 @@ function appearanceChain(themeKey = APPEARANCE_THEME_KEY) {
|
|
|
9967
10212
|
};
|
|
9968
10213
|
}
|
|
9969
10214
|
/**
|
|
10215
|
+
* Pull the `expertId` out of a task's `jump_url`.
|
|
10216
|
+
*
|
|
10217
|
+
* The "体验XX专家" tasks (#98) name an expert that is NOT in the marketplace
|
|
10218
|
+
* listing — the reporter翻遍了 marketExpertList in both `agent` and `team`
|
|
10219
|
+
* types and got zero hits — so the only place the id appears is the deeplink
|
|
10220
|
+
* embedded in the task itself:
|
|
10221
|
+
*
|
|
10222
|
+
* https://www.workbuddy.cn/work/launch/?deeplink=workbuddy%3A%2F%2Fexperts%3FexpertId%3Dex_GDl31WahyIH0&buddy_type=workbuddy
|
|
10223
|
+
*
|
|
10224
|
+
* The deeplink is percent-encoded, and it has to be: the `?` and `&` inside it
|
|
10225
|
+
* would otherwise end the outer query. So the parse is two steps — decode the
|
|
10226
|
+
* `deeplink` parameter, then read `expertId` off the scheme it decodes to — and
|
|
10227
|
+
* NOT a regex over the whole URL, which would also match the outer `expertId`
|
|
10228
|
+
* spelling in a future URL shape and silently pick the wrong one.
|
|
10229
|
+
*
|
|
10230
|
+
* Returns undefined for the tasks whose deeplink carries no id at all
|
|
10231
|
+
* (`WorkBuddy_Valorant` points at `workbuddy://expert` with no query), which is
|
|
10232
|
+
* the honest answer: those need a different source and are not guessed here.
|
|
10233
|
+
*/
|
|
10234
|
+
function expertIdFromJumpUrl(jumpUrl) {
|
|
10235
|
+
if (jumpUrl === "") return void 0;
|
|
10236
|
+
/** Read `expertId` off any URL-ish string, decoded or not. */
|
|
10237
|
+
const readId = (value) => {
|
|
10238
|
+
let query = value;
|
|
10239
|
+
try {
|
|
10240
|
+
const parsed = new URL(value);
|
|
10241
|
+
query = parsed.search === "" ? value : parsed.search;
|
|
10242
|
+
} catch {
|
|
10243
|
+
query = value;
|
|
10244
|
+
}
|
|
10245
|
+
const marker = query.indexOf("expertId=");
|
|
10246
|
+
if (marker === -1) return void 0;
|
|
10247
|
+
const rest = query.slice(marker + 9);
|
|
10248
|
+
const end = rest.search(/[&#]/u);
|
|
10249
|
+
const id = (end === -1 ? rest : rest.slice(0, end)).trim();
|
|
10250
|
+
return id === "" ? void 0 : id;
|
|
10251
|
+
};
|
|
10252
|
+
let source = jumpUrl;
|
|
10253
|
+
try {
|
|
10254
|
+
const raw = new URL(jumpUrl).searchParams.get("deeplink");
|
|
10255
|
+
if (raw !== null && raw !== "") source = raw;
|
|
10256
|
+
} catch {
|
|
10257
|
+
source = jumpUrl;
|
|
10258
|
+
}
|
|
10259
|
+
return readId(source);
|
|
10260
|
+
}
|
|
10261
|
+
/**
|
|
10262
|
+
* Build a summon record for an expert the marketplace does not list.
|
|
10263
|
+
*
|
|
10264
|
+
* Everything the chains need beyond the id is cosmetic — the scorer keys on
|
|
10265
|
+
* `expertId` — so the title from the task is reused as the display name rather
|
|
10266
|
+
* than inventing one. `version` is left empty, which every consumer already
|
|
10267
|
+
* defaults to `1.0.0`.
|
|
10268
|
+
*/
|
|
10269
|
+
function expertFromTask(expertId, title) {
|
|
10270
|
+
return {
|
|
10271
|
+
expertId,
|
|
10272
|
+
expertType: "agent",
|
|
10273
|
+
displayName: title,
|
|
10274
|
+
profession: title,
|
|
10275
|
+
version: "",
|
|
10276
|
+
categories: []
|
|
10277
|
+
};
|
|
10278
|
+
}
|
|
10279
|
+
/**
|
|
9970
10280
|
* The three "summon an expert" events (`expert_summon_click` and friends).
|
|
9971
10281
|
*
|
|
9972
10282
|
* Paid before the conversation, in the order the app emits them.
|
|
@@ -10080,7 +10390,12 @@ const EVENT_CHAIN_BUILDERS = {
|
|
|
10080
10390
|
skill_1: true,
|
|
10081
10391
|
expert_5: true,
|
|
10082
10392
|
Expert_team_use_3: true,
|
|
10083
|
-
Expert_lighthouse: true
|
|
10393
|
+
Expert_lighthouse: true,
|
|
10394
|
+
expert_use_maidehao: true,
|
|
10395
|
+
expert_use_douyinyin: true,
|
|
10396
|
+
expert_use_shangjia_xuanchuan: true,
|
|
10397
|
+
expert_use_bowangwang: true,
|
|
10398
|
+
WorkBuddy_Valorant: true
|
|
10084
10399
|
};
|
|
10085
10400
|
const AUTOMATION_TICK_MS = 6e4;
|
|
10086
10401
|
const JOB_KINDS = [
|
|
@@ -10195,10 +10510,35 @@ function slotKey(date, timeZone = AUTOMATION_TIME_ZONE) {
|
|
|
10195
10510
|
const { hour } = zonedParts(date, timeZone);
|
|
10196
10511
|
return `${dayKey(date, timeZone)}T${hour}`;
|
|
10197
10512
|
}
|
|
10198
|
-
/**
|
|
10199
|
-
|
|
10513
|
+
/**
|
|
10514
|
+
* Which account classes each job may touch.
|
|
10515
|
+
*
|
|
10516
|
+
* This replaced a single `global → false`, which was right about the growth
|
|
10517
|
+
* system and wrong as a blanket: it excluded a global account from EVERY job,
|
|
10518
|
+
* including the ones that are idempotent, cheap, and pay credits regardless of
|
|
10519
|
+
* region. The blanket also made the rule unfalsifiable — "global does nothing"
|
|
10520
|
+
* cannot be checked, whereas "the checkin job tries global accounts and here is
|
|
10521
|
+
* what came back" can.
|
|
10522
|
+
*
|
|
10523
|
+
* So the scope is per job, and the default for a job that is not named is `cn`:
|
|
10524
|
+
* a new job must opt in explicitly rather than inherit an accident.
|
|
10525
|
+
*/
|
|
10526
|
+
const JOB_REGION_SCOPE = {
|
|
10527
|
+
report: ["cn"],
|
|
10528
|
+
tasks: ["cn"],
|
|
10529
|
+
streak: ["cn"],
|
|
10530
|
+
travel: ["cn"],
|
|
10531
|
+
compensation: ["cn"],
|
|
10532
|
+
checkin: ["cn", "global"]
|
|
10533
|
+
};
|
|
10534
|
+
/** Whether `job` may run against `region`; unnamed jobs stay CN. */
|
|
10535
|
+
function jobAllowsRegion(job, region) {
|
|
10536
|
+
return (JOB_REGION_SCOPE[job] ?? ["cn"]).includes(region);
|
|
10537
|
+
}
|
|
10538
|
+
/** Whether this account may be used for a job: not cooling, and right region. */
|
|
10539
|
+
function eligible(account, now, job) {
|
|
10200
10540
|
if (account.cooldownUntilMs > now) return false;
|
|
10201
|
-
if (regionOf(account.credential.domain)
|
|
10541
|
+
if (job !== void 0 && !jobAllowsRegion(job, regionOf(account.credential.domain))) return false;
|
|
10202
10542
|
return true;
|
|
10203
10543
|
}
|
|
10204
10544
|
const sleep = (ms) => new Promise((resolve) => {
|
|
@@ -10269,6 +10609,16 @@ var WorkBuddyScheduler = class {
|
|
|
10269
10609
|
/** Host hooks that persist the ledger across restarts. */
|
|
10270
10610
|
loadEarnings;
|
|
10271
10611
|
saveEarningsFn;
|
|
10612
|
+
/**
|
|
10613
|
+
* The most recent ledger write that may still be in flight.
|
|
10614
|
+
*
|
|
10615
|
+
* `persistEarnings` is synchronous from its callers' point of view, but the
|
|
10616
|
+
* host's settings write is not: it is an async document update. Keeping the
|
|
10617
|
+
* promise here is what lets {@link flushEarnings} wait for the write that
|
|
10618
|
+
* matters — the last one before a restart — without making every counter
|
|
10619
|
+
* update await disk.
|
|
10620
|
+
*/
|
|
10621
|
+
pendingEarningsWrite = Promise.resolve();
|
|
10272
10622
|
constructor(pool, client, options = {}) {
|
|
10273
10623
|
this.pool = pool;
|
|
10274
10624
|
this.client = client;
|
|
@@ -10305,6 +10655,18 @@ var WorkBuddyScheduler = class {
|
|
|
10305
10655
|
this.saveEarningsFn = save;
|
|
10306
10656
|
}
|
|
10307
10657
|
/**
|
|
10658
|
+
* Wait for the ledger write that is (or was last) in flight.
|
|
10659
|
+
*
|
|
10660
|
+
* Called at the END of a task pass, right after the last claim, so the rewards
|
|
10661
|
+
* that were just collected are on disk before a restart can lose them. It is
|
|
10662
|
+
* deliberately NOT awaited on every counter bump: the settings document write
|
|
10663
|
+
* is the slow part, and paying it per account would slow the pass for no
|
|
10664
|
+
* benefit while the process is still alive.
|
|
10665
|
+
*/
|
|
10666
|
+
async flushEarnings() {
|
|
10667
|
+
await this.pendingEarningsWrite;
|
|
10668
|
+
}
|
|
10669
|
+
/**
|
|
10308
10670
|
* Fold a previously persisted ledger back in, when it belongs to today.
|
|
10309
10671
|
*
|
|
10310
10672
|
* Used after the settings document becomes readable, which happens after
|
|
@@ -10574,7 +10936,7 @@ var WorkBuddyScheduler = class {
|
|
|
10574
10936
|
compensationCredit: (existing?.compensationCredit ?? 0) + compensationCredit,
|
|
10575
10937
|
date: today
|
|
10576
10938
|
});
|
|
10577
|
-
this.persistEarnings();
|
|
10939
|
+
this.pendingEarningsWrite = this.persistEarnings();
|
|
10578
10940
|
}
|
|
10579
10941
|
/** Clear the per-account counters when the local day changes. */
|
|
10580
10942
|
rollEarnings(today) {
|
|
@@ -10590,9 +10952,18 @@ var WorkBuddyScheduler = class {
|
|
|
10590
10952
|
* Best effort on purpose: a failed save must never abort a run that has
|
|
10591
10953
|
* already collected rewards, and the in-memory ledger keeps serving the card
|
|
10592
10954
|
* for the rest of the session either way.
|
|
10955
|
+
*
|
|
10956
|
+
* The returned promise settles when the write has landed (or failed). Callers
|
|
10957
|
+
* that are about to END a run await it: reported in #98, a task pass that
|
|
10958
|
+
* claimed 21 tasks had its process restarted before the volatile settings
|
|
10959
|
+
* write reached disk, so `automationEarnings` still read `credit: 0` while
|
|
10960
|
+
* the upstream had really paid out. Fire-and-forget is fine for a counter the
|
|
10961
|
+
* card will re-read in this process; it is not fine for the last write before
|
|
10962
|
+
* exit.
|
|
10593
10963
|
*/
|
|
10594
10964
|
persistEarnings() {
|
|
10595
|
-
|
|
10965
|
+
const save = this.saveEarningsFn;
|
|
10966
|
+
if (save === void 0) return Promise.resolve();
|
|
10596
10967
|
const accounts = {};
|
|
10597
10968
|
for (const [id, entry] of this.earnings) accounts[id] = entry;
|
|
10598
10969
|
const ledger = {
|
|
@@ -10600,12 +10971,14 @@ var WorkBuddyScheduler = class {
|
|
|
10600
10971
|
accounts
|
|
10601
10972
|
};
|
|
10602
10973
|
try {
|
|
10603
|
-
const saved =
|
|
10604
|
-
if (saved !== void 0 && typeof saved.then === "function") saved.then(void 0, (error) => {
|
|
10974
|
+
const saved = save(ledger);
|
|
10975
|
+
if (saved !== void 0 && typeof saved.then === "function") return Promise.resolve(saved).then(void 0, (error) => {
|
|
10605
10976
|
this.logger.warn?.("dsh-xdpool: could not persist automation earnings:", error);
|
|
10606
10977
|
});
|
|
10978
|
+
return Promise.resolve();
|
|
10607
10979
|
} catch (error) {
|
|
10608
10980
|
this.logger.warn?.("dsh-xdpool: could not persist automation earnings:", error);
|
|
10981
|
+
return Promise.resolve();
|
|
10609
10982
|
}
|
|
10610
10983
|
}
|
|
10611
10984
|
/**
|
|
@@ -10632,8 +11005,22 @@ var WorkBuddyScheduler = class {
|
|
|
10632
11005
|
let energy = 0;
|
|
10633
11006
|
let claimed = 0;
|
|
10634
11007
|
const detail = [];
|
|
11008
|
+
/**
|
|
11009
|
+
* Every item this run considered, with a status attached.
|
|
11010
|
+
*
|
|
11011
|
+
* `detail` stays as the human-readable log line; this is the same work in a
|
|
11012
|
+
* shape the card can GROUP and COUNT, so "3 tasks unclaimed — locked" is
|
|
11013
|
+
* distinguishable from "0 tasks, everything already collected".
|
|
11014
|
+
*/
|
|
11015
|
+
const items = [];
|
|
10635
11016
|
let progressNote;
|
|
10636
|
-
|
|
11017
|
+
try {
|
|
11018
|
+
const revived = await this.pool.reviveBenched((credential) => this.client.fetchCredits(credential));
|
|
11019
|
+
for (const account of revived) detail.push(`复活 ${account.label}`);
|
|
11020
|
+
} catch (error) {
|
|
11021
|
+
this.logger.warn?.("automation: could not probe benched accounts:", error);
|
|
11022
|
+
}
|
|
11023
|
+
const accounts = this.accountsInOrder(kind);
|
|
10637
11024
|
if (kind === "tasks") await this.sendEventChains(accounts);
|
|
10638
11025
|
for (let index = 0; index < accounts.length; index++) {
|
|
10639
11026
|
if (this.stopped) break;
|
|
@@ -10641,12 +11028,22 @@ var WorkBuddyScheduler = class {
|
|
|
10641
11028
|
if (account === void 0) continue;
|
|
10642
11029
|
try {
|
|
10643
11030
|
switch (kind) {
|
|
10644
|
-
case "checkin":
|
|
11031
|
+
case "checkin": {
|
|
11032
|
+
let alreadyDone = false;
|
|
11033
|
+
try {
|
|
11034
|
+
alreadyDone = (await this.client.fetchCheckinStatus(account.credential)).todayCheckedIn === true;
|
|
11035
|
+
} catch (error) {
|
|
11036
|
+
this.logger.warn?.(`automation checkin ${account.label}: status read failed, claiming anyway:`, error);
|
|
11037
|
+
}
|
|
11038
|
+
if (alreadyDone) {
|
|
11039
|
+
this.logger.info?.(`automation checkin ${account.label}: already done today (read, not claimed)`);
|
|
11040
|
+
break;
|
|
11041
|
+
}
|
|
10645
11042
|
try {
|
|
10646
11043
|
const claim = await this.client.claimDailyCheckin(account.credential);
|
|
10647
11044
|
this.recordEarnings(account.id, today, { checkinCredit: claim.credit });
|
|
10648
11045
|
if (claim.credit > 0) {
|
|
10649
|
-
|
|
11046
|
+
detail.push(`签到 +${claim.credit}`);
|
|
10650
11047
|
this.logger.info?.(`automation checkin ${account.label}: +${claim.credit} credit`);
|
|
10651
11048
|
}
|
|
10652
11049
|
} catch (error) {
|
|
@@ -10654,6 +11051,7 @@ var WorkBuddyScheduler = class {
|
|
|
10654
11051
|
this.logger.info?.(`automation checkin ${account.label}: already done today`);
|
|
10655
11052
|
}
|
|
10656
11053
|
break;
|
|
11054
|
+
}
|
|
10657
11055
|
case "report": {
|
|
10658
11056
|
const days = await this.reportOne(account);
|
|
10659
11057
|
this.logger.info?.(`automation report ${account.label}: streak days=${days}`);
|
|
@@ -10665,6 +11063,7 @@ var WorkBuddyScheduler = class {
|
|
|
10665
11063
|
energy += result.energy;
|
|
10666
11064
|
claimed += result.claimed;
|
|
10667
11065
|
detail.push(...result.titles);
|
|
11066
|
+
items.push(...result.items);
|
|
10668
11067
|
this.claimableSeen += result.claimableCount;
|
|
10669
11068
|
this.recordEarnings(account.id, today, {
|
|
10670
11069
|
credit: result.credit,
|
|
@@ -10712,8 +11111,11 @@ var WorkBuddyScheduler = class {
|
|
|
10712
11111
|
state.claimed = claimed;
|
|
10713
11112
|
state.message = this.summarise(kind, ok, failed, claimed, credit, energy);
|
|
10714
11113
|
state.detail = detail;
|
|
10715
|
-
state.
|
|
11114
|
+
state.items = items;
|
|
11115
|
+
if (progressNote === void 0) delete state.progress;
|
|
11116
|
+
else state.progress = progressNote;
|
|
10716
11117
|
this.logger.info?.(`automation ${accountWord}: ${state.message}`);
|
|
11118
|
+
if (kind === "tasks" || kind === "checkin" || kind === "streak" || kind === "travel" || kind === "compensation") await this.flushEarnings();
|
|
10717
11119
|
}
|
|
10718
11120
|
/** Compose the one-line summary shown on the card. */
|
|
10719
11121
|
summarise(kind, ok, failed, claimed, credit, energy) {
|
|
@@ -10725,12 +11127,16 @@ var WorkBuddyScheduler = class {
|
|
|
10725
11127
|
*
|
|
10726
11128
|
* Disabled accounts are excluded here rather than filtered by the caller so a
|
|
10727
11129
|
* card switch takes effect on the next pass without any event plumbing.
|
|
11130
|
+
*
|
|
11131
|
+
* `job` narrows the set by region scope: the growth jobs are CN-only while
|
|
11132
|
+
* check-in runs on both gateways, so eligibility cannot be decided without
|
|
11133
|
+
* knowing which job is about to run.
|
|
10728
11134
|
*/
|
|
10729
|
-
accountsInOrder() {
|
|
11135
|
+
accountsInOrder(job) {
|
|
10730
11136
|
const now = Date.now();
|
|
10731
11137
|
return this.pool.list().filter((account) => {
|
|
10732
11138
|
if (this.pool.isDisabled(account.id)) return false;
|
|
10733
|
-
return eligible(account, now);
|
|
11139
|
+
return eligible(account, now, job);
|
|
10734
11140
|
});
|
|
10735
11141
|
}
|
|
10736
11142
|
/**
|
|
@@ -10836,10 +11242,88 @@ var WorkBuddyScheduler = class {
|
|
|
10836
11242
|
case "expert_5": return this.expertChains(credential, "agent", task);
|
|
10837
11243
|
case "Expert_team_use_3": return this.expertChains(credential, "team", task);
|
|
10838
11244
|
case "Expert_lighthouse": return this.lighthouseChains(credential);
|
|
11245
|
+
case "expert_use_maidehao":
|
|
11246
|
+
case "expert_use_douyinyin":
|
|
11247
|
+
case "expert_use_shangjia_xuanchuan":
|
|
11248
|
+
case "expert_use_bowangwang":
|
|
11249
|
+
case "WorkBuddy_Valorant": return this.taskExpertChains(credential, task);
|
|
10839
11250
|
default: return [];
|
|
10840
11251
|
}
|
|
10841
11252
|
}
|
|
10842
11253
|
/**
|
|
11254
|
+
* Summon-and-use one expert NAMED BY THE TASK.
|
|
11255
|
+
*
|
|
11256
|
+
* Structurally `expertChains` minus the marketplace: the expert id is read
|
|
11257
|
+
* from the task's deeplink instead of by browsing `marketExpertList`, because
|
|
11258
|
+
* this family's experts are not listed there at all (#98 measured 500 entries
|
|
11259
|
+
* across both `agent` and `team`, zero hits).
|
|
11260
|
+
*
|
|
11261
|
+
* Returns `[]` — not a throw — when the task carries no usable id: the caller
|
|
11262
|
+
* logs "could not be built" only for exceptions, and a task shape this code
|
|
11263
|
+
* does not understand is information, not a fault.
|
|
11264
|
+
*/
|
|
11265
|
+
/**
|
|
11266
|
+
* Find the expert a task is about, by matching its title against the
|
|
11267
|
+
* marketplace.
|
|
11268
|
+
*
|
|
11269
|
+
* The marketplace listing is ~500 entries across `agent` and `team`, and this
|
|
11270
|
+
* only runs for a task whose deeplink carried no id — at most a handful per
|
|
11271
|
+
* run — so the cost is acceptable. It is not cached because a freshly
|
|
11272
|
+
* published expert must become findable without a restart.
|
|
11273
|
+
*
|
|
11274
|
+
* Matching rules, all of them strict on purpose:
|
|
11275
|
+
* - the task's title must appear IN the expert's name or profession, or the
|
|
11276
|
+
* expert's display name must appear in the title (both directions, since
|
|
11277
|
+
* the board and the marketplace word things differently);
|
|
11278
|
+
* - comparison ignores case, whitespace and the punctuation vendors sprinkle
|
|
11279
|
+
* through names;
|
|
11280
|
+
* - several matches means NO answer. Summoning the wrong agent scores 0, so a
|
|
11281
|
+
* guess costs an upstream call and still fails — worse than declining.
|
|
11282
|
+
*/
|
|
11283
|
+
async expertIdByKeyword(credential, task) {
|
|
11284
|
+
const normalise = (value) => value.toLowerCase().replace(/[\s_\-·::,,.。()()【】[\]「」]/gu, "");
|
|
11285
|
+
const wanted = normalise(task.title);
|
|
11286
|
+
if (wanted === "") return void 0;
|
|
11287
|
+
let candidates;
|
|
11288
|
+
try {
|
|
11289
|
+
const [agents, teams] = await Promise.all([this.client.marketExpertList(credential, "agent"), this.client.marketExpertList(credential, "team")]);
|
|
11290
|
+
candidates = [...agents, ...teams];
|
|
11291
|
+
} catch (error) {
|
|
11292
|
+
this.logger.warn?.(`automation events ${task.taskCode}: marketplace lookup failed:`, error);
|
|
11293
|
+
return;
|
|
11294
|
+
}
|
|
11295
|
+
const matches = candidates.filter((candidate) => {
|
|
11296
|
+
const name = normalise(candidate.displayName ?? "");
|
|
11297
|
+
const profession = normalise(candidate.profession ?? "");
|
|
11298
|
+
if (name === "" && profession === "") return false;
|
|
11299
|
+
return name !== "" && (name.includes(wanted) || wanted.includes(name)) || profession !== "" && (profession.includes(wanted) || wanted.includes(profession));
|
|
11300
|
+
});
|
|
11301
|
+
if (matches.length !== 1) {
|
|
11302
|
+
if (matches.length > 1) this.logger.info?.(`automation events ${task.taskCode}: ${String(matches.length)} marketplace experts match "${task.title}", not guessing`);
|
|
11303
|
+
return;
|
|
11304
|
+
}
|
|
11305
|
+
const found = matches[0];
|
|
11306
|
+
if (found === void 0) return void 0;
|
|
11307
|
+
this.logger.info?.(`automation events ${task.taskCode}: resolved expertId ${found.expertId} by keyword "${task.title}"`);
|
|
11308
|
+
return found.expertId;
|
|
11309
|
+
}
|
|
11310
|
+
async taskExpertChains(credential, task) {
|
|
11311
|
+
const expertId = expertIdFromJumpUrl(task.jumpUrl) ?? await this.expertIdByKeyword(credential, task);
|
|
11312
|
+
if (expertId === void 0) {
|
|
11313
|
+
this.logger.warn?.(`automation events ${task.taskCode}: no expertId in jump_url and no marketplace match, skipping`);
|
|
11314
|
+
return [];
|
|
11315
|
+
}
|
|
11316
|
+
const expert = expertFromTask(expertId, task.title);
|
|
11317
|
+
await this.client.reportDesktopEvents(credential, expertSummonEvents(expert));
|
|
11318
|
+
if (this.delayMs > 0 && !this.stopped) await sleep(this.delayMs);
|
|
11319
|
+
const chat = await this.client.openConversation(credential, expert.expertId);
|
|
11320
|
+
if (chat === void 0) throw new Error(`${task.taskCode}: no server conversation`);
|
|
11321
|
+
return [{
|
|
11322
|
+
transport: "desktop",
|
|
11323
|
+
events: [...expertChatEvents(expert, chat.conversationId, chat.requestId), expertActualUseEvent(expert, chat.conversationId, chat.requestId)]
|
|
11324
|
+
}];
|
|
11325
|
+
}
|
|
11326
|
+
/**
|
|
10843
11327
|
* The summon-and-use chains for the expert tasks.
|
|
10844
11328
|
*
|
|
10845
11329
|
* Two steps per expert, and both are load-bearing: the summon events alone are
|
|
@@ -10911,21 +11395,41 @@ var WorkBuddyScheduler = class {
|
|
|
10911
11395
|
const credential = account.credential;
|
|
10912
11396
|
const open = (await this.client.listTasks(credential)).filter((task) => task.acceptStatus === "not_accepted" && !task.locked).map((task) => task.taskCode);
|
|
10913
11397
|
if (open.length > 0) await this.client.acceptTasks(credential, open);
|
|
10914
|
-
const
|
|
11398
|
+
const refreshed = await this.client.listTasks(credential);
|
|
11399
|
+
const claimable = refreshed.filter((task) => task.claimable && !task.locked);
|
|
10915
11400
|
let credit = 0;
|
|
10916
11401
|
let energy = 0;
|
|
10917
11402
|
let claimed = 0;
|
|
10918
11403
|
const titles = [];
|
|
11404
|
+
const items = [];
|
|
11405
|
+
for (const task of refreshed) if (task.locked === true || task.claimable !== true) items.push({
|
|
11406
|
+
title: task.title === "" ? task.taskCode : task.title,
|
|
11407
|
+
status: "skipped",
|
|
11408
|
+
note: task.locked === true ? "locked" : task.acceptStatus === "not_accepted" ? "not accepted" : "not scored yet"
|
|
11409
|
+
});
|
|
10919
11410
|
for (const task of claimable) {
|
|
10920
11411
|
if (this.stopped) break;
|
|
10921
11412
|
const reward = await this.client.claimTaskReward(credential, task.taskCode);
|
|
10922
11413
|
credit += reward.credit;
|
|
10923
11414
|
energy += reward.energy;
|
|
11415
|
+
const label = task.title === "" ? task.taskCode : task.title;
|
|
10924
11416
|
if (reward.credit > 0 || reward.energy > 0) {
|
|
10925
11417
|
claimed++;
|
|
10926
11418
|
titles.push(task.title);
|
|
11419
|
+
items.push({
|
|
11420
|
+
title: label,
|
|
11421
|
+
status: "claimed",
|
|
11422
|
+
...reward.credit > 0 ? { credit: reward.credit } : {},
|
|
11423
|
+
...reward.energy > 0 ? { energy: reward.energy } : {}
|
|
11424
|
+
});
|
|
10927
11425
|
this.logger.info?.(`automation claim ${account.label}: ${task.title} +${reward.credit}c +${reward.energy}e`);
|
|
10928
|
-
} else
|
|
11426
|
+
} else {
|
|
11427
|
+
items.push({
|
|
11428
|
+
title: label,
|
|
11429
|
+
status: "already"
|
|
11430
|
+
});
|
|
11431
|
+
this.logger.info?.(`automation claim ${account.label}: ${task.taskCode} already claimed`);
|
|
11432
|
+
}
|
|
10929
11433
|
if (this.delayMs > 0 && !this.stopped) await sleep(this.delayMs);
|
|
10930
11434
|
}
|
|
10931
11435
|
return {
|
|
@@ -10933,7 +11437,8 @@ var WorkBuddyScheduler = class {
|
|
|
10933
11437
|
credit,
|
|
10934
11438
|
energy,
|
|
10935
11439
|
claimableCount: claimable.length,
|
|
10936
|
-
titles
|
|
11440
|
+
titles,
|
|
11441
|
+
items
|
|
10937
11442
|
};
|
|
10938
11443
|
}
|
|
10939
11444
|
/**
|
|
@@ -11075,6 +11580,7 @@ Object.freeze({
|
|
|
11075
11580
|
"kimi_code": "Kimi Code 订阅,模型 id 取自 9router 的 kimi 注册表",
|
|
11076
11581
|
"lightning": "Lightning AI 的 Model APIs 网关",
|
|
11077
11582
|
"llamacpp": "本地服务器,模型列表取决于用户已加载的模型",
|
|
11583
|
+
"custom_openai": "自定义 OpenAI 兼容端点:vLLM / LiteLLM / one-api / 自建网关,填 base URL 和 key 后拉取模型",
|
|
11078
11584
|
"llm7": "default/fast/pro 别名被固定,只留流式工具模型",
|
|
11079
11585
|
"lmstudio": "本地服务器,模型列表取决于用户已加载的模型",
|
|
11080
11586
|
"minimax": "推理内容从正文流中单独拆出,语音与图像 id 已剔除",
|
|
@@ -11220,7 +11726,7 @@ async function readPreferredId(path = preferredIdPath()) {
|
|
|
11220
11726
|
* free of documents that describe the absence of a decision.
|
|
11221
11727
|
*/
|
|
11222
11728
|
async function writePreferredId(accountId, path = preferredIdPath()) {
|
|
11223
|
-
await
|
|
11729
|
+
await ensureSecretDir(dirname(path));
|
|
11224
11730
|
if (accountId === void 0) {
|
|
11225
11731
|
await rm(path, { force: true });
|
|
11226
11732
|
return;
|
|
@@ -11230,7 +11736,7 @@ async function writePreferredId(accountId, path = preferredIdPath()) {
|
|
|
11230
11736
|
accountId: accountId.trim()
|
|
11231
11737
|
}, null, 2);
|
|
11232
11738
|
const temp = `${path}.tmp`;
|
|
11233
|
-
await
|
|
11739
|
+
await writeSecretFile(temp, `${payload}\n`);
|
|
11234
11740
|
await rename(temp, path);
|
|
11235
11741
|
}
|
|
11236
11742
|
fileURLToPath(new URL("..", import.meta.url));
|
|
@@ -11307,7 +11813,18 @@ z.object({
|
|
|
11307
11813
|
modelSelectionGlobal: asVolatile(modelSelectionSchema.description("Model selection for the international gateway")),
|
|
11308
11814
|
automation: asVolatile(automationSchema.description("Daily points automation (activity report, task claiming, check-in)")),
|
|
11309
11815
|
automationEarnings: asVolatile(z.any().description("Automation earnings ledger (written by the scheduler)")),
|
|
11310
|
-
sidebarRow: asVolatile(z.boolean().default(true).description("Show the XD Pool entry in the left sidebar (the Settings page keeps working either way)"))
|
|
11816
|
+
sidebarRow: asVolatile(z.boolean().default(true).description("Show the XD Pool entry in the left sidebar (the Settings page keeps working either way)")),
|
|
11817
|
+
/**
|
|
11818
|
+
* The 免密 free lane (`free:` models).
|
|
11819
|
+
*
|
|
11820
|
+
* OFF unless set, and it is the only lane that works that way. It is a shared
|
|
11821
|
+
* third-party endpoint with no credential of its own, so there is nothing for
|
|
11822
|
+
* a user to "configure" — it simply appears in the picker or does not. On by
|
|
11823
|
+
* default it added a group nobody asked for that the aggregate switch does not
|
|
11824
|
+
* govern (it is not an aggregate provider), leaving no way to remove it
|
|
11825
|
+
* without editing a file. Reported as "关不掉,应该默认是关闭的".
|
|
11826
|
+
*/
|
|
11827
|
+
freeLane: asVolatile(z.boolean().default(false).description("Offer the shared 免密 free models (the \"XD Pool · 免费模型\" group). Off by default: it is a third-party endpoint with no key of your own."))
|
|
11311
11828
|
});
|
|
11312
11829
|
/** Assemble the runtime objects without registering anything. */
|
|
11313
11830
|
/**
|