dsh-xdpool 2.5.1 → 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 +169 -0
- package/README.en.md +10 -5
- package/README.md +10 -5
- package/assets/community-qr-qq.jpg +0 -0
- package/assets/community-qr-wechat.jpg +0 -0
- package/lib/bin.js +427 -74
- package/lib/client.js +1060 -235
- package/lib/index.d.ts +376 -61
- package/lib/index.js +2097 -937
- package/package.json +1 -1
- package/vendor/channel-pack/pack.js +346 -24
- package/assets/community-qr.png +0 -0
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";
|
|
@@ -1355,14 +1388,17 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1355
1388
|
if (typeof raw !== "object" || raw === null) continue;
|
|
1356
1389
|
const account = raw;
|
|
1357
1390
|
const num = (key) => typeof account[key] === "number" ? account[key] : 0;
|
|
1358
|
-
const
|
|
1359
|
-
const
|
|
1360
|
-
const
|
|
1391
|
+
const capacityType = num("CapacityType");
|
|
1392
|
+
const monthly = capacityType === 4;
|
|
1393
|
+
const periodic = monthly || capacityType === 2;
|
|
1394
|
+
const size = periodic ? num("CycleCapacitySize") : num("CapacitySize");
|
|
1395
|
+
const remain = periodic ? num("CycleCapacityRemain") : num("CapacityRemain");
|
|
1361
1396
|
const capped = remain < 0 ? 0 : remain;
|
|
1362
1397
|
const cycleEndMs = parseDate(account["CycleEndTime"]);
|
|
1363
|
-
const expiresAtMs = monthly ? void 0 : parseDate(account["ExpiredTime"]) ?? cycleEndMs;
|
|
1398
|
+
const expiresAtMs = monthly ? void 0 : capacityType === 2 ? parseDate(account["DeductionEndTime"]) : parseDate(account["ExpiredTime"]) ?? cycleEndMs;
|
|
1364
1399
|
const refreshAtMs = monthly ? cycleEndMs === void 0 ? void 0 : cycleEndMs + 1e3 : void 0;
|
|
1365
|
-
if (
|
|
1400
|
+
if (expiresAtMs !== void 0 && expiresAtMs <= Date.now()) continue;
|
|
1401
|
+
if (!periodic && capped <= 0) continue;
|
|
1366
1402
|
total += capped;
|
|
1367
1403
|
if (expiresAtMs !== void 0) {
|
|
1368
1404
|
if (nearestExpiryMs === void 0 || expiresAtMs < nearestExpiryMs) nearestExpiryMs = expiresAtMs;
|
|
@@ -2093,6 +2129,92 @@ var WorkBuddyUpstreamClient = class {
|
|
|
2093
2129
|
}
|
|
2094
2130
|
};
|
|
2095
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
|
|
2096
2218
|
//#region src/ignored.ts
|
|
2097
2219
|
/**
|
|
2098
2220
|
* The permanent ignore list: accounts the user has thrown out of the pool.
|
|
@@ -2193,9 +2315,9 @@ async function writeIgnoredAccounts(accounts, path = ignoredIdsPath()) {
|
|
|
2193
2315
|
version: 1,
|
|
2194
2316
|
accounts
|
|
2195
2317
|
}, null, 2);
|
|
2196
|
-
await
|
|
2318
|
+
await ensureSecretDir(dirname(path));
|
|
2197
2319
|
const temp = `${path}.tmp`;
|
|
2198
|
-
await
|
|
2320
|
+
await writeSecretFile(temp, `${payload}\n`);
|
|
2199
2321
|
await rename(temp, path);
|
|
2200
2322
|
}
|
|
2201
2323
|
/**
|
|
@@ -2321,9 +2443,9 @@ async function saveStoredAccounts(path, accounts) {
|
|
|
2321
2443
|
version: 1,
|
|
2322
2444
|
accounts: stored
|
|
2323
2445
|
}, null, 2) + "\n";
|
|
2324
|
-
await
|
|
2446
|
+
await ensureSecretDir(join(path, ".."));
|
|
2325
2447
|
const tmp = `${path}.tmp`;
|
|
2326
|
-
await
|
|
2448
|
+
await writeSecretFile(tmp, document);
|
|
2327
2449
|
await rename(tmp, path);
|
|
2328
2450
|
}
|
|
2329
2451
|
//#endregion
|
|
@@ -3087,16 +3209,20 @@ function parseWorkBuddyAuth(text, sourcePath, decrypt) {
|
|
|
3087
3209
|
const refreshExpiresAtMs = typeof auth["refreshExpiresAt"] === "number" ? expiryToMs(auth["refreshExpiresAt"]) : void 0;
|
|
3088
3210
|
if (refreshExpiresAtMs !== void 0 && refreshExpiresAtMs > 0 && refreshExpiresAtMs < Date.now()) return;
|
|
3089
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"]);
|
|
3090
3216
|
return {
|
|
3091
3217
|
accessToken,
|
|
3092
3218
|
refreshToken: decryptableString(auth["refreshToken"], decrypt).value,
|
|
3093
3219
|
expiresAtMs: typeof auth["expiresAt"] === "number" ? expiryToMs(auth["expiresAt"]) : 0,
|
|
3094
3220
|
...refreshExpiresAtMs === void 0 ? {} : { refreshExpiresAtMs },
|
|
3095
3221
|
...lastRefreshAtMs === void 0 ? {} : { lastRefreshAtMs },
|
|
3096
|
-
...
|
|
3097
|
-
...
|
|
3098
|
-
...
|
|
3099
|
-
...
|
|
3222
|
+
...nickname === void 0 ? {} : { nickname },
|
|
3223
|
+
...uin === void 0 ? {} : { uin },
|
|
3224
|
+
...uid === void 0 ? {} : { uid },
|
|
3225
|
+
...enterpriseId === void 0 ? {} : { enterpriseId },
|
|
3100
3226
|
domain: typeof auth["domain"] === "string" ? auth["domain"] : "",
|
|
3101
3227
|
sourcePath
|
|
3102
3228
|
};
|
|
@@ -3441,6 +3567,16 @@ var WorkBuddyAccountPool = class {
|
|
|
3441
3567
|
*/
|
|
3442
3568
|
disabledIds = /* @__PURE__ */ new Set();
|
|
3443
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
|
+
/**
|
|
3444
3580
|
* Account ids the user threw out of the pool for good.
|
|
3445
3581
|
*
|
|
3446
3582
|
* Enforced BEFORE the credential is parsed: `scan()` skips a file whose
|
|
@@ -3517,6 +3653,11 @@ var WorkBuddyAccountPool = class {
|
|
|
3517
3653
|
* A function rather than a list because that pool is mounted asynchronously
|
|
3518
3654
|
* and can be re-read on demand; `undefined` means "no such source", which is
|
|
3519
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.
|
|
3520
3661
|
*/
|
|
3521
3662
|
externalSource;
|
|
3522
3663
|
constructor(options = {}) {
|
|
@@ -3622,9 +3763,10 @@ var WorkBuddyAccountPool = class {
|
|
|
3622
3763
|
}
|
|
3623
3764
|
const byId = /* @__PURE__ */ new Map();
|
|
3624
3765
|
for (const account of this.accounts) byId.set(account.id, account);
|
|
3625
|
-
if (this.persistent
|
|
3766
|
+
if (this.persistent) {
|
|
3626
3767
|
const stored = await loadStoredAccounts(this.storePath);
|
|
3627
3768
|
for (const entry of stored) {
|
|
3769
|
+
if (this.ignoredIds.size > 0 && this.ignoredIds.has(entry.id)) continue;
|
|
3628
3770
|
if (byId.has(entry.id)) continue;
|
|
3629
3771
|
byId.set(entry.id, {
|
|
3630
3772
|
id: entry.id,
|
|
@@ -3674,7 +3816,10 @@ var WorkBuddyAccountPool = class {
|
|
|
3674
3816
|
this.accounts = ordered;
|
|
3675
3817
|
this.lastScanAtMs = Date.now();
|
|
3676
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.`);
|
|
3677
3821
|
await saveStoredAccounts(this.storePath, this.accounts);
|
|
3822
|
+
this.lastPersistedIds = new Set(this.accounts.map((account) => account.id));
|
|
3678
3823
|
} catch (error) {
|
|
3679
3824
|
this.logger?.warn?.("dsh-xdpool: could not persist the account store", error);
|
|
3680
3825
|
}
|
|
@@ -4190,6 +4335,43 @@ var WorkBuddyAccountPool = class {
|
|
|
4190
4335
|
return this.accounts.filter((account) => (account.credentialDeadUntilMs ?? 0) > now);
|
|
4191
4336
|
}
|
|
4192
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
|
+
/**
|
|
4193
4375
|
* Refresh the account's access token when it is within the margin (or already
|
|
4194
4376
|
* expired), in-flight de-duped per account. A failed refresh keeps the
|
|
4195
4377
|
* existing token when it has not yet expired, so an unreachable refresh
|
|
@@ -4336,8 +4518,23 @@ ${new Date(until).toISOString()}
|
|
|
4336
4518
|
* (`kimi-k3-1`). A stale fallback is worse than a short one — it looks
|
|
4337
4519
|
* authoritative while being wrong.
|
|
4338
4520
|
*
|
|
4339
|
-
*
|
|
4340
|
-
*
|
|
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.
|
|
4341
4538
|
*/
|
|
4342
4539
|
const FALLBACK_WORKBUDDY_MODELS = [
|
|
4343
4540
|
{
|
|
@@ -4354,8 +4551,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
4354
4551
|
contextWindow: 96e4,
|
|
4355
4552
|
maxOutputTokens: 64e3,
|
|
4356
4553
|
supportsImages: true,
|
|
4357
|
-
|
|
4358
|
-
tags: ["craft", "badge:夜间免费:#FF0000"]
|
|
4554
|
+
tags: ["craft"]
|
|
4359
4555
|
},
|
|
4360
4556
|
{
|
|
4361
4557
|
id: "hy3",
|
|
@@ -4363,40 +4559,35 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
4363
4559
|
contextWindow: 192e3,
|
|
4364
4560
|
maxOutputTokens: 64e3,
|
|
4365
4561
|
supportsImages: true,
|
|
4366
|
-
|
|
4367
|
-
tags: ["craft", "badge:限时免费:#FF0000"]
|
|
4562
|
+
tags: ["craft"]
|
|
4368
4563
|
},
|
|
4369
4564
|
{
|
|
4370
4565
|
id: "hy3-x",
|
|
4371
4566
|
name: "Hy3",
|
|
4372
4567
|
contextWindow: 192e3,
|
|
4373
4568
|
maxOutputTokens: 64e3,
|
|
4374
|
-
supportsImages: true
|
|
4375
|
-
multiplier: .05
|
|
4569
|
+
supportsImages: true
|
|
4376
4570
|
},
|
|
4377
4571
|
{
|
|
4378
4572
|
id: "space-bunny",
|
|
4379
4573
|
name: "Space-Bunny",
|
|
4380
4574
|
contextWindow: 1e6,
|
|
4381
4575
|
maxOutputTokens: 64e3,
|
|
4382
|
-
supportsImages: true
|
|
4383
|
-
multiplier: .08
|
|
4576
|
+
supportsImages: true
|
|
4384
4577
|
},
|
|
4385
4578
|
{
|
|
4386
4579
|
id: "deepseek-v4.1-flash",
|
|
4387
4580
|
name: "Deepseek-V4.1-Flash",
|
|
4388
4581
|
contextWindow: 1e6,
|
|
4389
4582
|
maxOutputTokens: 128e3,
|
|
4390
|
-
supportsImages: true
|
|
4391
|
-
multiplier: .11
|
|
4583
|
+
supportsImages: true
|
|
4392
4584
|
},
|
|
4393
4585
|
{
|
|
4394
4586
|
id: "deepseek-v4-pro",
|
|
4395
4587
|
name: "Deepseek-V4-Pro",
|
|
4396
4588
|
contextWindow: 1e6,
|
|
4397
4589
|
maxOutputTokens: 128e3,
|
|
4398
|
-
supportsImages: true
|
|
4399
|
-
multiplier: .51
|
|
4590
|
+
supportsImages: true
|
|
4400
4591
|
},
|
|
4401
4592
|
{
|
|
4402
4593
|
id: "deepseek-v4-flash",
|
|
@@ -4410,16 +4601,14 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
4410
4601
|
name: "GLM-5.3",
|
|
4411
4602
|
contextWindow: 1e6,
|
|
4412
4603
|
maxOutputTokens: 64e3,
|
|
4413
|
-
supportsImages: true
|
|
4414
|
-
multiplier: .79
|
|
4604
|
+
supportsImages: true
|
|
4415
4605
|
},
|
|
4416
4606
|
{
|
|
4417
4607
|
id: "glm-5.3-flash",
|
|
4418
4608
|
name: "GLM-5.3-Flash",
|
|
4419
4609
|
contextWindow: 1e6,
|
|
4420
4610
|
maxOutputTokens: 131072,
|
|
4421
|
-
supportsImages: true
|
|
4422
|
-
multiplier: .06
|
|
4611
|
+
supportsImages: true
|
|
4423
4612
|
},
|
|
4424
4613
|
{
|
|
4425
4614
|
id: "glm-5.2",
|
|
@@ -4427,64 +4616,56 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
4427
4616
|
contextWindow: 1e6,
|
|
4428
4617
|
maxOutputTokens: 64e3,
|
|
4429
4618
|
supportsImages: true,
|
|
4430
|
-
|
|
4431
|
-
tags: ["craft", "badge:夜间折扣:#1E90FF"]
|
|
4619
|
+
tags: ["craft"]
|
|
4432
4620
|
},
|
|
4433
4621
|
{
|
|
4434
4622
|
id: "glm-5.1",
|
|
4435
4623
|
name: "GLM-5.1",
|
|
4436
4624
|
contextWindow: 2e5,
|
|
4437
4625
|
maxOutputTokens: 48e3,
|
|
4438
|
-
supportsImages: false
|
|
4439
|
-
multiplier: .79
|
|
4626
|
+
supportsImages: false
|
|
4440
4627
|
},
|
|
4441
4628
|
{
|
|
4442
4629
|
id: "glm-5v-turbo",
|
|
4443
4630
|
name: "GLM-5v-Turbo",
|
|
4444
4631
|
contextWindow: 2e5,
|
|
4445
4632
|
maxOutputTokens: 64e3,
|
|
4446
|
-
supportsImages: true
|
|
4447
|
-
multiplier: .71
|
|
4633
|
+
supportsImages: true
|
|
4448
4634
|
},
|
|
4449
4635
|
{
|
|
4450
4636
|
id: "kimi-k3-1",
|
|
4451
4637
|
name: "Kimi-K3",
|
|
4452
4638
|
contextWindow: 1e6,
|
|
4453
4639
|
maxOutputTokens: 32e3,
|
|
4454
|
-
supportsImages: true
|
|
4455
|
-
multiplier: 1.62
|
|
4640
|
+
supportsImages: true
|
|
4456
4641
|
},
|
|
4457
4642
|
{
|
|
4458
4643
|
id: "kimi-k2.8-preview",
|
|
4459
4644
|
name: "Kimi-K2.8-Preview",
|
|
4460
4645
|
contextWindow: 1e6,
|
|
4461
4646
|
maxOutputTokens: 64e3,
|
|
4462
|
-
supportsImages: true
|
|
4463
|
-
multiplier: .77
|
|
4647
|
+
supportsImages: true
|
|
4464
4648
|
},
|
|
4465
4649
|
{
|
|
4466
4650
|
id: "kimi-k2.7",
|
|
4467
4651
|
name: "Kimi-K2.7-Code",
|
|
4468
4652
|
contextWindow: 256e3,
|
|
4469
4653
|
maxOutputTokens: 32e3,
|
|
4470
|
-
supportsImages: true
|
|
4471
|
-
multiplier: .57
|
|
4654
|
+
supportsImages: true
|
|
4472
4655
|
},
|
|
4473
4656
|
{
|
|
4474
4657
|
id: "kimi-k2.6",
|
|
4475
4658
|
name: "Kimi-K2.6",
|
|
4476
4659
|
contextWindow: 256e3,
|
|
4477
4660
|
maxOutputTokens: 32e3,
|
|
4478
|
-
supportsImages: true
|
|
4479
|
-
multiplier: .52
|
|
4661
|
+
supportsImages: true
|
|
4480
4662
|
},
|
|
4481
4663
|
{
|
|
4482
4664
|
id: "minimax-m3",
|
|
4483
4665
|
name: "MiniMax-M3",
|
|
4484
4666
|
contextWindow: 512e3,
|
|
4485
4667
|
maxOutputTokens: 64e3,
|
|
4486
|
-
supportsImages: true
|
|
4487
|
-
multiplier: .25
|
|
4668
|
+
supportsImages: true
|
|
4488
4669
|
}
|
|
4489
4670
|
];
|
|
4490
4671
|
/**
|
|
@@ -4557,11 +4738,24 @@ var WorkBuddyCatalog = class {
|
|
|
4557
4738
|
}
|
|
4558
4739
|
/**
|
|
4559
4740
|
* The models DSH should actually offer, after applying the user's selection:
|
|
4560
|
-
* disabled models are dropped, an explicit image list
|
|
4561
|
-
*
|
|
4741
|
+
* disabled models are dropped, an explicit image list decides image support,
|
|
4742
|
+
* and a per-model budget caps the advertised window.
|
|
4562
4743
|
*
|
|
4563
4744
|
* An absent `enabledModelIds` means "everything" — a fresh install with no
|
|
4564
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".
|
|
4565
4759
|
*/
|
|
4566
4760
|
visible() {
|
|
4567
4761
|
const enabled = this.selection.enabledModelIds;
|
|
@@ -4571,7 +4765,7 @@ var WorkBuddyCatalog = class {
|
|
|
4571
4765
|
const budgets = this.selection.contextBudgets;
|
|
4572
4766
|
return this.models.filter((model) => allow === void 0 || allow.has(model.id)).map((model) => {
|
|
4573
4767
|
const next = { ...model };
|
|
4574
|
-
if (imageSet !== void 0) next.supportsImages =
|
|
4768
|
+
if (imageSet !== void 0) next.supportsImages = imageSet.has(model.id);
|
|
4575
4769
|
const budget = budgets?.[model.id];
|
|
4576
4770
|
if (budget !== void 0 && budget > 0 && budget < next.contextWindow) next.contextWindow = budget;
|
|
4577
4771
|
return next;
|
|
@@ -4676,8 +4870,13 @@ function catalogFromUpstream(models, region = "cn") {
|
|
|
4676
4870
|
if (models.length === 0) return fallbackModelsFor(region);
|
|
4677
4871
|
return models.map(toModelInfo);
|
|
4678
4872
|
}
|
|
4679
|
-
|
|
4680
|
-
|
|
4873
|
+
Object.freeze([
|
|
4874
|
+
"priority",
|
|
4875
|
+
"round-robin",
|
|
4876
|
+
"balanced",
|
|
4877
|
+
"sticky",
|
|
4878
|
+
"expiry"
|
|
4879
|
+
]);
|
|
4681
4880
|
/**
|
|
4682
4881
|
* The schedule every automation job falls back to.
|
|
4683
4882
|
*
|
|
@@ -9636,6 +9835,24 @@ const PROVIDERS = Object.freeze([
|
|
|
9636
9835
|
"website": "https://openai.com/",
|
|
9637
9836
|
"local": false,
|
|
9638
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
|
|
9639
9856
|
}
|
|
9640
9857
|
].map((entry) => Object.freeze({
|
|
9641
9858
|
...entry,
|
|
@@ -10293,10 +10510,35 @@ function slotKey(date, timeZone = AUTOMATION_TIME_ZONE) {
|
|
|
10293
10510
|
const { hour } = zonedParts(date, timeZone);
|
|
10294
10511
|
return `${dayKey(date, timeZone)}T${hour}`;
|
|
10295
10512
|
}
|
|
10296
|
-
/**
|
|
10297
|
-
|
|
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) {
|
|
10298
10540
|
if (account.cooldownUntilMs > now) return false;
|
|
10299
|
-
if (regionOf(account.credential.domain)
|
|
10541
|
+
if (job !== void 0 && !jobAllowsRegion(job, regionOf(account.credential.domain))) return false;
|
|
10300
10542
|
return true;
|
|
10301
10543
|
}
|
|
10302
10544
|
const sleep = (ms) => new Promise((resolve) => {
|
|
@@ -10763,8 +11005,22 @@ var WorkBuddyScheduler = class {
|
|
|
10763
11005
|
let energy = 0;
|
|
10764
11006
|
let claimed = 0;
|
|
10765
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 = [];
|
|
10766
11016
|
let progressNote;
|
|
10767
|
-
|
|
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);
|
|
10768
11024
|
if (kind === "tasks") await this.sendEventChains(accounts);
|
|
10769
11025
|
for (let index = 0; index < accounts.length; index++) {
|
|
10770
11026
|
if (this.stopped) break;
|
|
@@ -10772,12 +11028,22 @@ var WorkBuddyScheduler = class {
|
|
|
10772
11028
|
if (account === void 0) continue;
|
|
10773
11029
|
try {
|
|
10774
11030
|
switch (kind) {
|
|
10775
|
-
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
|
+
}
|
|
10776
11042
|
try {
|
|
10777
11043
|
const claim = await this.client.claimDailyCheckin(account.credential);
|
|
10778
11044
|
this.recordEarnings(account.id, today, { checkinCredit: claim.credit });
|
|
10779
11045
|
if (claim.credit > 0) {
|
|
10780
|
-
|
|
11046
|
+
detail.push(`签到 +${claim.credit}`);
|
|
10781
11047
|
this.logger.info?.(`automation checkin ${account.label}: +${claim.credit} credit`);
|
|
10782
11048
|
}
|
|
10783
11049
|
} catch (error) {
|
|
@@ -10785,6 +11051,7 @@ var WorkBuddyScheduler = class {
|
|
|
10785
11051
|
this.logger.info?.(`automation checkin ${account.label}: already done today`);
|
|
10786
11052
|
}
|
|
10787
11053
|
break;
|
|
11054
|
+
}
|
|
10788
11055
|
case "report": {
|
|
10789
11056
|
const days = await this.reportOne(account);
|
|
10790
11057
|
this.logger.info?.(`automation report ${account.label}: streak days=${days}`);
|
|
@@ -10796,6 +11063,7 @@ var WorkBuddyScheduler = class {
|
|
|
10796
11063
|
energy += result.energy;
|
|
10797
11064
|
claimed += result.claimed;
|
|
10798
11065
|
detail.push(...result.titles);
|
|
11066
|
+
items.push(...result.items);
|
|
10799
11067
|
this.claimableSeen += result.claimableCount;
|
|
10800
11068
|
this.recordEarnings(account.id, today, {
|
|
10801
11069
|
credit: result.credit,
|
|
@@ -10843,7 +11111,9 @@ var WorkBuddyScheduler = class {
|
|
|
10843
11111
|
state.claimed = claimed;
|
|
10844
11112
|
state.message = this.summarise(kind, ok, failed, claimed, credit, energy);
|
|
10845
11113
|
state.detail = detail;
|
|
10846
|
-
state.
|
|
11114
|
+
state.items = items;
|
|
11115
|
+
if (progressNote === void 0) delete state.progress;
|
|
11116
|
+
else state.progress = progressNote;
|
|
10847
11117
|
this.logger.info?.(`automation ${accountWord}: ${state.message}`);
|
|
10848
11118
|
if (kind === "tasks" || kind === "checkin" || kind === "streak" || kind === "travel" || kind === "compensation") await this.flushEarnings();
|
|
10849
11119
|
}
|
|
@@ -10857,12 +11127,16 @@ var WorkBuddyScheduler = class {
|
|
|
10857
11127
|
*
|
|
10858
11128
|
* Disabled accounts are excluded here rather than filtered by the caller so a
|
|
10859
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.
|
|
10860
11134
|
*/
|
|
10861
|
-
accountsInOrder() {
|
|
11135
|
+
accountsInOrder(job) {
|
|
10862
11136
|
const now = Date.now();
|
|
10863
11137
|
return this.pool.list().filter((account) => {
|
|
10864
11138
|
if (this.pool.isDisabled(account.id)) return false;
|
|
10865
|
-
return eligible(account, now);
|
|
11139
|
+
return eligible(account, now, job);
|
|
10866
11140
|
});
|
|
10867
11141
|
}
|
|
10868
11142
|
/**
|
|
@@ -10988,10 +11262,55 @@ var WorkBuddyScheduler = class {
|
|
|
10988
11262
|
* logs "could not be built" only for exceptions, and a task shape this code
|
|
10989
11263
|
* does not understand is information, not a fault.
|
|
10990
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
|
+
}
|
|
10991
11310
|
async taskExpertChains(credential, task) {
|
|
10992
|
-
const expertId = expertIdFromJumpUrl(task.jumpUrl);
|
|
11311
|
+
const expertId = expertIdFromJumpUrl(task.jumpUrl) ?? await this.expertIdByKeyword(credential, task);
|
|
10993
11312
|
if (expertId === void 0) {
|
|
10994
|
-
this.logger.warn?.(`automation events ${task.taskCode}: no expertId in jump_url, skipping`);
|
|
11313
|
+
this.logger.warn?.(`automation events ${task.taskCode}: no expertId in jump_url and no marketplace match, skipping`);
|
|
10995
11314
|
return [];
|
|
10996
11315
|
}
|
|
10997
11316
|
const expert = expertFromTask(expertId, task.title);
|
|
@@ -11076,21 +11395,41 @@ var WorkBuddyScheduler = class {
|
|
|
11076
11395
|
const credential = account.credential;
|
|
11077
11396
|
const open = (await this.client.listTasks(credential)).filter((task) => task.acceptStatus === "not_accepted" && !task.locked).map((task) => task.taskCode);
|
|
11078
11397
|
if (open.length > 0) await this.client.acceptTasks(credential, open);
|
|
11079
|
-
const
|
|
11398
|
+
const refreshed = await this.client.listTasks(credential);
|
|
11399
|
+
const claimable = refreshed.filter((task) => task.claimable && !task.locked);
|
|
11080
11400
|
let credit = 0;
|
|
11081
11401
|
let energy = 0;
|
|
11082
11402
|
let claimed = 0;
|
|
11083
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
|
+
});
|
|
11084
11410
|
for (const task of claimable) {
|
|
11085
11411
|
if (this.stopped) break;
|
|
11086
11412
|
const reward = await this.client.claimTaskReward(credential, task.taskCode);
|
|
11087
11413
|
credit += reward.credit;
|
|
11088
11414
|
energy += reward.energy;
|
|
11415
|
+
const label = task.title === "" ? task.taskCode : task.title;
|
|
11089
11416
|
if (reward.credit > 0 || reward.energy > 0) {
|
|
11090
11417
|
claimed++;
|
|
11091
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
|
+
});
|
|
11092
11425
|
this.logger.info?.(`automation claim ${account.label}: ${task.title} +${reward.credit}c +${reward.energy}e`);
|
|
11093
|
-
} 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
|
+
}
|
|
11094
11433
|
if (this.delayMs > 0 && !this.stopped) await sleep(this.delayMs);
|
|
11095
11434
|
}
|
|
11096
11435
|
return {
|
|
@@ -11098,7 +11437,8 @@ var WorkBuddyScheduler = class {
|
|
|
11098
11437
|
credit,
|
|
11099
11438
|
energy,
|
|
11100
11439
|
claimableCount: claimable.length,
|
|
11101
|
-
titles
|
|
11440
|
+
titles,
|
|
11441
|
+
items
|
|
11102
11442
|
};
|
|
11103
11443
|
}
|
|
11104
11444
|
/**
|
|
@@ -11240,6 +11580,7 @@ Object.freeze({
|
|
|
11240
11580
|
"kimi_code": "Kimi Code 订阅,模型 id 取自 9router 的 kimi 注册表",
|
|
11241
11581
|
"lightning": "Lightning AI 的 Model APIs 网关",
|
|
11242
11582
|
"llamacpp": "本地服务器,模型列表取决于用户已加载的模型",
|
|
11583
|
+
"custom_openai": "自定义 OpenAI 兼容端点:vLLM / LiteLLM / one-api / 自建网关,填 base URL 和 key 后拉取模型",
|
|
11243
11584
|
"llm7": "default/fast/pro 别名被固定,只留流式工具模型",
|
|
11244
11585
|
"lmstudio": "本地服务器,模型列表取决于用户已加载的模型",
|
|
11245
11586
|
"minimax": "推理内容从正文流中单独拆出,语音与图像 id 已剔除",
|
|
@@ -11385,7 +11726,7 @@ async function readPreferredId(path = preferredIdPath()) {
|
|
|
11385
11726
|
* free of documents that describe the absence of a decision.
|
|
11386
11727
|
*/
|
|
11387
11728
|
async function writePreferredId(accountId, path = preferredIdPath()) {
|
|
11388
|
-
await
|
|
11729
|
+
await ensureSecretDir(dirname(path));
|
|
11389
11730
|
if (accountId === void 0) {
|
|
11390
11731
|
await rm(path, { force: true });
|
|
11391
11732
|
return;
|
|
@@ -11395,7 +11736,7 @@ async function writePreferredId(accountId, path = preferredIdPath()) {
|
|
|
11395
11736
|
accountId: accountId.trim()
|
|
11396
11737
|
}, null, 2);
|
|
11397
11738
|
const temp = `${path}.tmp`;
|
|
11398
|
-
await
|
|
11739
|
+
await writeSecretFile(temp, `${payload}\n`);
|
|
11399
11740
|
await rename(temp, path);
|
|
11400
11741
|
}
|
|
11401
11742
|
fileURLToPath(new URL("..", import.meta.url));
|
|
@@ -11471,7 +11812,19 @@ z.object({
|
|
|
11471
11812
|
modelSelectionCn: asVolatile(modelSelectionSchema.description("Model selection for the domestic gateway")),
|
|
11472
11813
|
modelSelectionGlobal: asVolatile(modelSelectionSchema.description("Model selection for the international gateway")),
|
|
11473
11814
|
automation: asVolatile(automationSchema.description("Daily points automation (activity report, task claiming, check-in)")),
|
|
11474
|
-
automationEarnings: asVolatile(z.any().description("Automation earnings ledger (written by the scheduler)"))
|
|
11815
|
+
automationEarnings: asVolatile(z.any().description("Automation earnings ledger (written by the scheduler)")),
|
|
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."))
|
|
11475
11828
|
});
|
|
11476
11829
|
/** Assemble the runtime objects without registering anything. */
|
|
11477
11830
|
/**
|