dsh-workbuddy-xdpool 1.7.0 → 1.7.2
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 -84
- package/lib/bin.js +444 -42
- package/lib/client.js +168 -10
- package/lib/index.d.ts +270 -4
- package/lib/index.js +485 -68
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { basename, dirname, join, resolve } from "node:path";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { createDecipheriv, createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
|
-
import { readFile, readdir } from "node:fs/promises";
|
|
4
|
+
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { execFile, execFileSync } from "node:child_process";
|
|
7
7
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
@@ -443,6 +443,7 @@ function parseUpstreamModel(value) {
|
|
|
443
443
|
const reasoning = parseReasoning(raw["reasoning"]);
|
|
444
444
|
const supportsToolCall = typeof raw["supportsToolCall"] === "boolean" ? raw["supportsToolCall"] : void 0;
|
|
445
445
|
const supportsImages = typeof raw["supportsImages"] === "boolean" ? raw["supportsImages"] : void 0;
|
|
446
|
+
const tags = Array.isArray(raw["tags"]) ? raw["tags"].filter((tag) => typeof tag === "string" && tag !== "") : void 0;
|
|
446
447
|
return {
|
|
447
448
|
id,
|
|
448
449
|
name,
|
|
@@ -453,7 +454,8 @@ function parseUpstreamModel(value) {
|
|
|
453
454
|
...descriptionZh === void 0 ? {} : { descriptionZh },
|
|
454
455
|
...descriptionEn === void 0 ? {} : { descriptionEn },
|
|
455
456
|
...supportsToolCall === void 0 ? {} : { supportsToolCall },
|
|
456
|
-
...supportsImages === void 0 ? {} : { supportsImages }
|
|
457
|
+
...supportsImages === void 0 ? {} : { supportsImages },
|
|
458
|
+
...tags === void 0 || tags.length === 0 ? {} : { tags }
|
|
457
459
|
};
|
|
458
460
|
}
|
|
459
461
|
/**
|
|
@@ -2428,6 +2430,41 @@ async function readCredential(path) {
|
|
|
2428
2430
|
}
|
|
2429
2431
|
}
|
|
2430
2432
|
/**
|
|
2433
|
+
* The pool id a document would receive, WITHOUT decrypting anything.
|
|
2434
|
+
*
|
|
2435
|
+
* Used to honour the ignore list before the at-rest key lookup runs: `uin` and
|
|
2436
|
+
* `uid` are read as plain strings by {@link parseWorkBuddyAuth}, while
|
|
2437
|
+
* `nickname` is commonly encrypted — so this returns undefined for an account
|
|
2438
|
+
* whose identity lives only in the encrypted nickname, and the caller falls
|
|
2439
|
+
* back to the full parse for those.
|
|
2440
|
+
*/
|
|
2441
|
+
function cheapIdentityId(text) {
|
|
2442
|
+
let parsed;
|
|
2443
|
+
try {
|
|
2444
|
+
parsed = JSON.parse(text);
|
|
2445
|
+
} catch {
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
2449
|
+
const document = parsed;
|
|
2450
|
+
const identity = typeof document["account"] === "object" && document["account"] !== null ? document["account"] : document;
|
|
2451
|
+
const uin = typeof identity["uin"] === "string" && identity["uin"] !== "" ? identity["uin"] : void 0;
|
|
2452
|
+
const uid = typeof identity["uid"] === "string" && identity["uid"] !== "" ? identity["uid"] : void 0;
|
|
2453
|
+
if (uin === void 0 && uid === void 0) return void 0;
|
|
2454
|
+
return workbuddyAccountId({
|
|
2455
|
+
...uin === void 0 ? {} : { uin },
|
|
2456
|
+
...uid === void 0 ? {} : { uid }
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
/** {@link cheapIdentityId} for a file path; undefined when it cannot be read. */
|
|
2460
|
+
async function cheapIdentityIdFromFile(path) {
|
|
2461
|
+
try {
|
|
2462
|
+
return cheapIdentityId(await readFile(path, "utf8"));
|
|
2463
|
+
} catch {
|
|
2464
|
+
return;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2431
2468
|
* Build the field opener, or undefined when the app cannot supply its key.
|
|
2432
2469
|
*
|
|
2433
2470
|
* Split out so the key lookup is testable without a real desktop install, and so
|
|
@@ -2506,6 +2543,21 @@ var WorkBuddyAccountPool = class {
|
|
|
2506
2543
|
*/
|
|
2507
2544
|
disabledIds = /* @__PURE__ */ new Set();
|
|
2508
2545
|
/**
|
|
2546
|
+
* Account ids the user threw out of the pool for good.
|
|
2547
|
+
*
|
|
2548
|
+
* Enforced BEFORE the credential is parsed: `scan()` skips a file whose
|
|
2549
|
+
* identity is already ignored, so an ignored account costs no at-rest key
|
|
2550
|
+
* lookup (which spawns the desktop app on 5.6.0+) and cannot re-enter the pool
|
|
2551
|
+
* when the app writes a fresh sign-in for it. That is the difference from
|
|
2552
|
+
* {@link disabledIds}, which only filters at pick time and leaves the account
|
|
2553
|
+
* listed, readable and re-discoverable.
|
|
2554
|
+
*
|
|
2555
|
+
* The set is supplied by the host from the plugin's own ignore file, and is
|
|
2556
|
+
* replaced wholesale on every {@link applyIgnored} so removing an entry takes
|
|
2557
|
+
* effect on the next scan without a restart.
|
|
2558
|
+
*/
|
|
2559
|
+
ignoredIds = /* @__PURE__ */ new Set();
|
|
2560
|
+
/**
|
|
2509
2561
|
* Per-account credit floor, keyed by account id. 0 (or absent) means "spend
|
|
2510
2562
|
* it all".
|
|
2511
2563
|
*
|
|
@@ -2557,12 +2609,39 @@ var WorkBuddyAccountPool = class {
|
|
|
2557
2609
|
if (options.disabledAccountIds !== void 0) this.disabledIds = new Set(options.disabledAccountIds);
|
|
2558
2610
|
if (options.creditReserves !== void 0) this.setCreditReserves(options.creditReserves);
|
|
2559
2611
|
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Replace the permanent ignore list.
|
|
2614
|
+
*
|
|
2615
|
+
* Also drops any already-discovered account that is now ignored, so the change
|
|
2616
|
+
* is visible without waiting for the next scan: the card refreshes its status
|
|
2617
|
+
* document right after the write, and an account still sitting in `accounts`
|
|
2618
|
+
* would keep showing up there.
|
|
2619
|
+
*/
|
|
2620
|
+
applyIgnored(ids) {
|
|
2621
|
+
this.ignoredIds = new Set(ids);
|
|
2622
|
+
if (this.ignoredIds.size === 0) return;
|
|
2623
|
+
this.accounts = this.accounts.filter((account) => !this.ignoredIds.has(account.id));
|
|
2624
|
+
}
|
|
2625
|
+
/** Whether this account has been thrown out of the pool for good. */
|
|
2626
|
+
isIgnored(accountId) {
|
|
2627
|
+
return this.ignoredIds.has(accountId);
|
|
2628
|
+
}
|
|
2629
|
+
/** Every ignored id currently in force, in insertion order. */
|
|
2630
|
+
ignoredIdsInOrder() {
|
|
2631
|
+
return [...this.ignoredIds];
|
|
2632
|
+
}
|
|
2560
2633
|
/** Rescan the auth directories and merge newly discovered accounts. */
|
|
2561
2634
|
async scan() {
|
|
2562
2635
|
const found = [];
|
|
2563
2636
|
for (const dir of this.authDirs) for (const file of await authFilesIn(dir)) {
|
|
2637
|
+
if (this.ignoredIds.size > 0) {
|
|
2638
|
+
const cheapId = await cheapIdentityIdFromFile(file);
|
|
2639
|
+
if (cheapId !== void 0 && this.ignoredIds.has(cheapId)) continue;
|
|
2640
|
+
}
|
|
2564
2641
|
const credential = await readCredential(file);
|
|
2565
|
-
if (credential
|
|
2642
|
+
if (credential === void 0) continue;
|
|
2643
|
+
if (this.ignoredIds.size > 0 && this.ignoredIds.has(workbuddyAccountId(credential))) continue;
|
|
2644
|
+
found.push(credential);
|
|
2566
2645
|
}
|
|
2567
2646
|
const byId = /* @__PURE__ */ new Map();
|
|
2568
2647
|
for (const account of this.accounts) byId.set(account.id, account);
|
|
@@ -2903,28 +2982,44 @@ ${new Date(until).toISOString()}
|
|
|
2903
2982
|
};
|
|
2904
2983
|
//#endregion
|
|
2905
2984
|
//#region src/catalog.ts
|
|
2906
|
-
/**
|
|
2985
|
+
/**
|
|
2986
|
+
* Static fallback used before the first live catalog fetch, and whenever the
|
|
2987
|
+
* upstream cannot be reached.
|
|
2988
|
+
*
|
|
2989
|
+
* The multipliers are carried on purpose. Without them the provider's model
|
|
2990
|
+
* picker silently loses every rate and every free badge the moment the live
|
|
2991
|
+
* fetch fails — which reads to the user as "the plugin broke my model list"
|
|
2992
|
+
* rather than "the upstream is unreachable". The values are the ones the two
|
|
2993
|
+
* gateways actually advertise for these ids (`credits: "x0.79 credits"` and so
|
|
2994
|
+
* on), so a fallback row looks the same as a live one.
|
|
2995
|
+
*
|
|
2996
|
+
* `multiplier: 0` is the gateways' own spelling of "free" (`credits: "x0.00"`),
|
|
2997
|
+
* which is what turns on the free badge.
|
|
2998
|
+
*/
|
|
2907
2999
|
const FALLBACK_WORKBUDDY_MODELS = [
|
|
2908
3000
|
{
|
|
2909
3001
|
id: "glm-5.3",
|
|
2910
3002
|
name: "GLM-5.3",
|
|
2911
3003
|
contextWindow: 2e5,
|
|
2912
3004
|
maxOutputTokens: 128e3,
|
|
2913
|
-
supportsImages: true
|
|
3005
|
+
supportsImages: true,
|
|
3006
|
+
multiplier: .79
|
|
2914
3007
|
},
|
|
2915
3008
|
{
|
|
2916
3009
|
id: "glm-5.3-flash",
|
|
2917
3010
|
name: "GLM-5.3-Flash",
|
|
2918
3011
|
contextWindow: 2e5,
|
|
2919
3012
|
maxOutputTokens: 128e3,
|
|
2920
|
-
supportsImages: true
|
|
3013
|
+
supportsImages: true,
|
|
3014
|
+
multiplier: .06
|
|
2921
3015
|
},
|
|
2922
3016
|
{
|
|
2923
3017
|
id: "glm-5.2",
|
|
2924
3018
|
name: "GLM-5.2",
|
|
2925
3019
|
contextWindow: 2e5,
|
|
2926
3020
|
maxOutputTokens: 128e3,
|
|
2927
|
-
supportsImages: true
|
|
3021
|
+
supportsImages: true,
|
|
3022
|
+
multiplier: .79
|
|
2928
3023
|
},
|
|
2929
3024
|
{
|
|
2930
3025
|
id: "glm-5.1",
|
|
@@ -2952,7 +3047,8 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
2952
3047
|
name: "Kimi-K3",
|
|
2953
3048
|
contextWindow: 2e5,
|
|
2954
3049
|
maxOutputTokens: 128e3,
|
|
2955
|
-
supportsImages: true
|
|
3050
|
+
supportsImages: true,
|
|
3051
|
+
multiplier: 1.62
|
|
2956
3052
|
},
|
|
2957
3053
|
{
|
|
2958
3054
|
id: "minimax-m3",
|
|
@@ -2966,14 +3062,16 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
2966
3062
|
name: "Hy3",
|
|
2967
3063
|
contextWindow: 32e3,
|
|
2968
3064
|
maxOutputTokens: 8e3,
|
|
2969
|
-
supportsImages: true
|
|
3065
|
+
supportsImages: true,
|
|
3066
|
+
multiplier: 0
|
|
2970
3067
|
},
|
|
2971
3068
|
{
|
|
2972
3069
|
id: "hy4-preview",
|
|
2973
3070
|
name: "Hy4-Preview",
|
|
2974
3071
|
contextWindow: 1e6,
|
|
2975
3072
|
maxOutputTokens: 128e3,
|
|
2976
|
-
supportsImages: true
|
|
3073
|
+
supportsImages: true,
|
|
3074
|
+
multiplier: .29
|
|
2977
3075
|
}
|
|
2978
3076
|
];
|
|
2979
3077
|
/** Live catalog with a static fallback behind it. */
|
|
@@ -3051,7 +3149,8 @@ function toModelInfo(model) {
|
|
|
3051
3149
|
maxOutputTokens: model.maxTokens,
|
|
3052
3150
|
supportsImages: model.supportsImages ?? false,
|
|
3053
3151
|
...model.creditMultiplier === void 0 ? {} : { multiplier: model.creditMultiplier },
|
|
3054
|
-
...model.reasoning?.supportedEfforts === void 0 ? {} : { supportedEfforts: model.reasoning.supportedEfforts }
|
|
3152
|
+
...model.reasoning?.supportedEfforts === void 0 ? {} : { supportedEfforts: model.reasoning.supportedEfforts },
|
|
3153
|
+
...model.tags === void 0 || model.tags.length === 0 ? {} : { tags: model.tags }
|
|
3055
3154
|
};
|
|
3056
3155
|
}
|
|
3057
3156
|
/** Map the live upstream list, falling back to the static list when empty. */
|
|
@@ -3161,7 +3260,8 @@ const TAG_LABEL = {
|
|
|
3161
3260
|
* so we don't tack a trailing separator on a plain model. */
|
|
3162
3261
|
function displaySuffix(info) {
|
|
3163
3262
|
const parts = [];
|
|
3164
|
-
if (
|
|
3263
|
+
if (info.multiplier === 0) parts.push("免费");
|
|
3264
|
+
else if (typeof info.multiplier === "number" && Number.isFinite(info.multiplier)) parts.push(`x${info.multiplier.toFixed(2)}`);
|
|
3165
3265
|
for (const tag of info.tags ?? []) {
|
|
3166
3266
|
const label = TAG_LABEL[tag];
|
|
3167
3267
|
if (label !== void 0 && !parts.includes(label)) parts.push(label);
|
|
@@ -3660,6 +3760,65 @@ function expertChatEvents(expert, conversationId, requestId) {
|
|
|
3660
3760
|
expert_industry_id: ""
|
|
3661
3761
|
} : event);
|
|
3662
3762
|
}
|
|
3763
|
+
//#endregion
|
|
3764
|
+
//#region src/status-paths.ts
|
|
3765
|
+
/**
|
|
3766
|
+
* Node-free constants and types shared by the Host and browser halves of the
|
|
3767
|
+
* WorkBuddy XD Pool settings card.
|
|
3768
|
+
*
|
|
3769
|
+
* Pool's runtime state already lives in `src/status.ts` (`buildStatus` /
|
|
3770
|
+
* `WorkBuddyStatus`); this module only carves the cross-domain (Host→browser)
|
|
3771
|
+
* JSON document into a shape that stays token-free and matches what the
|
|
3772
|
+
* browser card renders. Route paths are plugin-owned and mounted on the Host's
|
|
3773
|
+
* same-origin web server (see `src/web-status.ts`).
|
|
3774
|
+
*
|
|
3775
|
+
* @module dsh-workbuddy-xdpool/status-paths
|
|
3776
|
+
*/
|
|
3777
|
+
/** Plugin-owned read-only pool status endpoint (account rows + models + shim). */
|
|
3778
|
+
const POOL_STATUS_PATH = "/plugins/dsh-workbuddy-xdpool/status";
|
|
3779
|
+
/** Plugin-owned local account rescan endpoint (re-read desktop snapshots). */
|
|
3780
|
+
const POOL_RESCAN_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/rescan";
|
|
3781
|
+
/** Plugin-owned cooldown reset endpoint (clear all 429 cooldowns). */
|
|
3782
|
+
const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-workbuddy-xdpool/cooldowns/reset";
|
|
3783
|
+
/** Plugin-owned daily check-in action endpoint (claim today's reward). */
|
|
3784
|
+
const POOL_CHECKIN_PATH = "/plugins/dsh-workbuddy-xdpool/checkin";
|
|
3785
|
+
/** Plugin-owned model-selection save endpoint (writes the settings section). */
|
|
3786
|
+
const POOL_MODELS_SAVE_PATH = "/plugins/dsh-workbuddy-xdpool/models/save";
|
|
3787
|
+
/** Switch one account in or out of the pool (card toggle). */
|
|
3788
|
+
const POOL_ACCOUNT_DISABLE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/disabled";
|
|
3789
|
+
/**
|
|
3790
|
+
* Throw one account out of the pool for good, or take it back.
|
|
3791
|
+
*
|
|
3792
|
+
* Separate from the disable route because the semantics differ: disabling is a
|
|
3793
|
+
* rotation preference the account survives, ignoring survives the account.
|
|
3794
|
+
*/
|
|
3795
|
+
const POOL_ACCOUNT_IGNORE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/ignored";
|
|
3796
|
+
/** Run one automation job immediately, so the card can verify it on demand. */
|
|
3797
|
+
const POOL_AUTOMATION_RUN_PATH = "/plugins/dsh-workbuddy-xdpool/automation/run";
|
|
3798
|
+
/** Set or clear one account's reserved-credit floor. */
|
|
3799
|
+
const POOL_CREDIT_RESERVE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/credit-reserve";
|
|
3800
|
+
/**
|
|
3801
|
+
* The schedule every automation job falls back to.
|
|
3802
|
+
*
|
|
3803
|
+
* Shared by both halves on purpose. The host uses it when a configured hour
|
|
3804
|
+
* list arrives empty (the settings schema materializes "never configured" into
|
|
3805
|
+
* `[]`), and the card uses it when it writes the `automation` block back, so a
|
|
3806
|
+
* document that already holds an empty list is healed instead of being saved
|
|
3807
|
+
* back as an unrunnable schedule.
|
|
3808
|
+
*
|
|
3809
|
+
* This lives here rather than in `scheduler.ts` because the browser half cannot
|
|
3810
|
+
* import the host module: `scheduler.ts` pulls in `node:crypto` and the whole
|
|
3811
|
+
* upstream client, none of which exists in the browser bundle. Two hand-written
|
|
3812
|
+
* copies would drift, and the drift is invisible — the card would write a
|
|
3813
|
+
* schedule the scheduler does not run.
|
|
3814
|
+
*/
|
|
3815
|
+
const DEFAULT_AUTOMATION_HOURS = {
|
|
3816
|
+
checkin: [9],
|
|
3817
|
+
report: [10],
|
|
3818
|
+
tasks: [11],
|
|
3819
|
+
streak: [12],
|
|
3820
|
+
travel: [9, 21]
|
|
3821
|
+
};
|
|
3663
3822
|
/** How often the loop wakes to look for a due job. */
|
|
3664
3823
|
/**
|
|
3665
3824
|
* How long to wait for event scoring to land before re-reading the task list.
|
|
@@ -3718,6 +3877,23 @@ function isAutomationJobKind(value) {
|
|
|
3718
3877
|
return typeof value === "string" && AUTOMATION_JOB_KINDS.includes(value);
|
|
3719
3878
|
}
|
|
3720
3879
|
const JOB_KINDS = AUTOMATION_JOB_KINDS;
|
|
3880
|
+
/**
|
|
3881
|
+
* Pick the hour list to run on: the configured one, or the default.
|
|
3882
|
+
*
|
|
3883
|
+
* An EMPTY list is treated as "not configured" rather than as "never run".
|
|
3884
|
+
* That distinction is the whole point: the settings schema materializes an
|
|
3885
|
+
* absent list into `[]` (a bare `z.array` with no `.default()`), so a config
|
|
3886
|
+
* that never mentioned the schedule arrives here looking exactly like one
|
|
3887
|
+
* deliberately set to nothing — and honouring the empty reading silently
|
|
3888
|
+
* disabled every job while the card still showed the switch as ON.
|
|
3889
|
+
*
|
|
3890
|
+
* A user who genuinely wants a job skipped can leave it out of the schedule
|
|
3891
|
+
* they save; they cannot express "off" with an empty array through the card
|
|
3892
|
+
* either way, because the card always writes a non-empty list.
|
|
3893
|
+
*/
|
|
3894
|
+
function hoursOrDefault(configured, fallback) {
|
|
3895
|
+
return configured !== void 0 && configured.length > 0 ? configured : fallback;
|
|
3896
|
+
}
|
|
3721
3897
|
const EMPTY_JOB_STATE = {
|
|
3722
3898
|
ok: 0,
|
|
3723
3899
|
failed: 0,
|
|
@@ -3906,11 +4082,11 @@ var WorkBuddyScheduler = class {
|
|
|
3906
4082
|
this.earningsDate = today;
|
|
3907
4083
|
}
|
|
3908
4084
|
this.enabled = options.enabled ?? false;
|
|
3909
|
-
this.checkinHours = options.checkinHours
|
|
3910
|
-
this.reportHours = options.reportHours
|
|
3911
|
-
this.taskHours = options.taskHours
|
|
3912
|
-
this.streakHours = options.streakHours
|
|
3913
|
-
this.travelHours = options.travelHours
|
|
4085
|
+
this.checkinHours = hoursOrDefault(options.checkinHours, DEFAULT_AUTOMATION_HOURS.checkin);
|
|
4086
|
+
this.reportHours = hoursOrDefault(options.reportHours, DEFAULT_AUTOMATION_HOURS.report);
|
|
4087
|
+
this.taskHours = hoursOrDefault(options.taskHours, DEFAULT_AUTOMATION_HOURS.tasks);
|
|
4088
|
+
this.streakHours = hoursOrDefault(options.streakHours, DEFAULT_AUTOMATION_HOURS.streak);
|
|
4089
|
+
this.travelHours = hoursOrDefault(options.travelHours, DEFAULT_AUTOMATION_HOURS.travel);
|
|
3914
4090
|
}
|
|
3915
4091
|
/** Apply a new configuration; safe to call while running. */
|
|
3916
4092
|
/**
|
|
@@ -3938,11 +4114,11 @@ var WorkBuddyScheduler = class {
|
|
|
3938
4114
|
}
|
|
3939
4115
|
applyConfig(options) {
|
|
3940
4116
|
if (options.enabled !== void 0) this.enabled = options.enabled;
|
|
3941
|
-
if (options.checkinHours !== void 0) this.checkinHours = options.checkinHours;
|
|
3942
|
-
if (options.reportHours !== void 0) this.reportHours = options.reportHours;
|
|
3943
|
-
if (options.taskHours !== void 0) this.taskHours = options.taskHours;
|
|
3944
|
-
if (options.streakHours !== void 0) this.streakHours = options.streakHours;
|
|
3945
|
-
if (options.travelHours !== void 0) this.travelHours = options.travelHours;
|
|
4117
|
+
if (options.checkinHours !== void 0) this.checkinHours = hoursOrDefault(options.checkinHours, DEFAULT_AUTOMATION_HOURS.checkin);
|
|
4118
|
+
if (options.reportHours !== void 0) this.reportHours = hoursOrDefault(options.reportHours, DEFAULT_AUTOMATION_HOURS.report);
|
|
4119
|
+
if (options.taskHours !== void 0) this.taskHours = hoursOrDefault(options.taskHours, DEFAULT_AUTOMATION_HOURS.tasks);
|
|
4120
|
+
if (options.streakHours !== void 0) this.streakHours = hoursOrDefault(options.streakHours, DEFAULT_AUTOMATION_HOURS.streak);
|
|
4121
|
+
if (options.travelHours !== void 0) this.travelHours = hoursOrDefault(options.travelHours, DEFAULT_AUTOMATION_HOURS.travel);
|
|
3946
4122
|
}
|
|
3947
4123
|
/** Hours for one job, used by the loop and the status document. */
|
|
3948
4124
|
hoursOf(kind) {
|
|
@@ -3988,7 +4164,7 @@ var WorkBuddyScheduler = class {
|
|
|
3988
4164
|
async runNow(kind, force = false) {
|
|
3989
4165
|
const today = dayKey(this.now());
|
|
3990
4166
|
const state = this.states[kind];
|
|
3991
|
-
if (!force && state.
|
|
4167
|
+
if (!force && state.lastFiredHour === slotKey(this.now())) return { ...state };
|
|
3992
4168
|
this.busy = true;
|
|
3993
4169
|
try {
|
|
3994
4170
|
await this.runJob(kind, today);
|
|
@@ -4086,13 +4262,46 @@ var WorkBuddyScheduler = class {
|
|
|
4086
4262
|
* two hours still runs twice a day — but a job whose hour passed while DSH was
|
|
4087
4263
|
* closed runs immediately on the next tick instead of waiting for tomorrow.
|
|
4088
4264
|
*/
|
|
4089
|
-
|
|
4265
|
+
/**
|
|
4266
|
+
* The configured slot `kind` should consume at `now`, or undefined when none.
|
|
4267
|
+
*
|
|
4268
|
+
* Returns the SLOT STRING (not a boolean) because the caller must record the
|
|
4269
|
+
* same value it acted on. Returning a boolean was the original bug's enabler:
|
|
4270
|
+
* the tick asked "is it due", then `runJob` independently wrote "what time is
|
|
4271
|
+
* it now" — and those two answers were almost never equal.
|
|
4272
|
+
*
|
|
4273
|
+
* CATCH-UP semantics are deliberate: a candidate fires once its hour has
|
|
4274
|
+
* PASSED and its slot is still unconsumed, so a laptop that slept through
|
|
4275
|
+
* 10:00 still runs the job when it wakes, on the same day.
|
|
4276
|
+
*
|
|
4277
|
+
* The EARLIEST unconsumed candidate wins, which is what keeps a two-hour job
|
|
4278
|
+
* (`travelHours: [9, 21]`) whole: consuming the earliest due slot leaves the
|
|
4279
|
+
* later one for its own hour.
|
|
4280
|
+
*/
|
|
4281
|
+
dueSlot(kind, now) {
|
|
4090
4282
|
const hours = this.hoursOf(kind);
|
|
4091
|
-
if (hours.length === 0) return
|
|
4092
|
-
const
|
|
4093
|
-
const current = Number(hour);
|
|
4283
|
+
if (hours.length === 0) return void 0;
|
|
4284
|
+
const current = Number(zonedParts(now).hour);
|
|
4094
4285
|
const today = dayKey(now);
|
|
4095
|
-
|
|
4286
|
+
const fired = this.firedSlotsOf(kind);
|
|
4287
|
+
for (const candidate of [...hours].sort((a, b) => a - b)) {
|
|
4288
|
+
if (candidate > current) break;
|
|
4289
|
+
const slot = `${today}T${String(candidate).padStart(2, "0")}`;
|
|
4290
|
+
if (!fired.has(slot)) return slot;
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
/** Today's consumed slots for one job, as a set. */
|
|
4294
|
+
firedSlotsOf(kind) {
|
|
4295
|
+
const stored = this.states[kind].firedSlots;
|
|
4296
|
+
if (stored === void 0) return /* @__PURE__ */ new Set();
|
|
4297
|
+
const today = dayKey(this.now());
|
|
4298
|
+
return new Set(stored.filter((slot) => slot.startsWith(`${today}T`)));
|
|
4299
|
+
}
|
|
4300
|
+
/** Record one consumed slot on a job's state. */
|
|
4301
|
+
consumeSlot(kind, slot) {
|
|
4302
|
+
const state = this.states[kind];
|
|
4303
|
+
const today = dayKey(this.now());
|
|
4304
|
+
state.firedSlots = [.../* @__PURE__ */ new Set([...this.firedSlotsOf(kind), slot])].filter((entry) => entry.startsWith(`${today}T`)).sort();
|
|
4096
4305
|
}
|
|
4097
4306
|
async tick() {
|
|
4098
4307
|
if (!this.enabled || this.stopped || this.busy) return;
|
|
@@ -4103,9 +4312,10 @@ var WorkBuddyScheduler = class {
|
|
|
4103
4312
|
const today = dayKey(now);
|
|
4104
4313
|
for (const kind of JOB_KINDS) {
|
|
4105
4314
|
if (this.stopped) return;
|
|
4106
|
-
if (this.states[kind].
|
|
4107
|
-
|
|
4108
|
-
|
|
4315
|
+
if (this.states[kind].lastFiredHour === slot) continue;
|
|
4316
|
+
const consumed = this.dueSlot(kind, now);
|
|
4317
|
+
if (consumed === void 0) continue;
|
|
4318
|
+
await this.runJob(kind, today, consumed);
|
|
4109
4319
|
}
|
|
4110
4320
|
} catch (error) {
|
|
4111
4321
|
this.logger.warn?.("dsh-workbuddy-xdpool automation tick failed:", error);
|
|
@@ -4191,6 +4401,12 @@ var WorkBuddyScheduler = class {
|
|
|
4191
4401
|
/**
|
|
4192
4402
|
* Run one job against every eligible account and record the outcome.
|
|
4193
4403
|
*
|
|
4404
|
+
* `consumedSlot` is the configured slot this run spends (`dueSlot`'s answer).
|
|
4405
|
+
* It is passed IN rather than recomputed here so the value recorded is exactly
|
|
4406
|
+
* the value the schedule decided on — the defect this replaces wrote
|
|
4407
|
+
* `slotKey(now)` instead, i.e. the clock time the run finished at, which
|
|
4408
|
+
* almost never equals the configured candidate the gate had compared against.
|
|
4409
|
+
*
|
|
4194
4410
|
* The task job runs in TWO passes. The first sends the event chains that light
|
|
4195
4411
|
* up client-scored tasks; the second collects rewards. They are separate
|
|
4196
4412
|
* because scoring lands asynchronously — a chain sent and claimed within the
|
|
@@ -4198,7 +4414,7 @@ var WorkBuddyScheduler = class {
|
|
|
4198
4414
|
* while claiming wants the whole pool to have been lit up first. Splitting
|
|
4199
4415
|
* them costs one shared wait instead of one wait per account.
|
|
4200
4416
|
*/
|
|
4201
|
-
async runJob(kind, today) {
|
|
4417
|
+
async runJob(kind, today, consumedSlot) {
|
|
4202
4418
|
const accountWord = kind === "report" ? "report" : kind;
|
|
4203
4419
|
let ok = 0;
|
|
4204
4420
|
let failed = 0;
|
|
@@ -4263,7 +4479,8 @@ var WorkBuddyScheduler = class {
|
|
|
4263
4479
|
if (index < accounts.length - 1 && this.delayMs > 0 && !this.stopped) await sleep(this.delayMs);
|
|
4264
4480
|
}
|
|
4265
4481
|
const state = this.states[kind];
|
|
4266
|
-
|
|
4482
|
+
if (consumedSlot !== void 0) this.consumeSlot(kind, consumedSlot);
|
|
4483
|
+
state.lastFiredHour = slotKey(this.now());
|
|
4267
4484
|
state.lastRunDate = today;
|
|
4268
4485
|
state.lastRunAtMs = this.now().getTime();
|
|
4269
4486
|
state.ok = ok;
|
|
@@ -5415,36 +5632,6 @@ function formatRates(status) {
|
|
|
5415
5632
|
return lines.join("\n");
|
|
5416
5633
|
}
|
|
5417
5634
|
//#endregion
|
|
5418
|
-
//#region src/status-paths.ts
|
|
5419
|
-
/**
|
|
5420
|
-
* Node-free constants and types shared by the Host and browser halves of the
|
|
5421
|
-
* WorkBuddy XD Pool settings card.
|
|
5422
|
-
*
|
|
5423
|
-
* Pool's runtime state already lives in `src/status.ts` (`buildStatus` /
|
|
5424
|
-
* `WorkBuddyStatus`); this module only carves the cross-domain (Host→browser)
|
|
5425
|
-
* JSON document into a shape that stays token-free and matches what the
|
|
5426
|
-
* browser card renders. Route paths are plugin-owned and mounted on the Host's
|
|
5427
|
-
* same-origin web server (see `src/web-status.ts`).
|
|
5428
|
-
*
|
|
5429
|
-
* @module dsh-workbuddy-xdpool/status-paths
|
|
5430
|
-
*/
|
|
5431
|
-
/** Plugin-owned read-only pool status endpoint (account rows + models + shim). */
|
|
5432
|
-
const POOL_STATUS_PATH = "/plugins/dsh-workbuddy-xdpool/status";
|
|
5433
|
-
/** Plugin-owned local account rescan endpoint (re-read desktop snapshots). */
|
|
5434
|
-
const POOL_RESCAN_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/rescan";
|
|
5435
|
-
/** Plugin-owned cooldown reset endpoint (clear all 429 cooldowns). */
|
|
5436
|
-
const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-workbuddy-xdpool/cooldowns/reset";
|
|
5437
|
-
/** Plugin-owned daily check-in action endpoint (claim today's reward). */
|
|
5438
|
-
const POOL_CHECKIN_PATH = "/plugins/dsh-workbuddy-xdpool/checkin";
|
|
5439
|
-
/** Plugin-owned model-selection save endpoint (writes the settings section). */
|
|
5440
|
-
const POOL_MODELS_SAVE_PATH = "/plugins/dsh-workbuddy-xdpool/models/save";
|
|
5441
|
-
/** Switch one account in or out of the pool (card toggle). */
|
|
5442
|
-
const POOL_ACCOUNT_DISABLE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/disabled";
|
|
5443
|
-
/** Run one automation job immediately, so the card can verify it on demand. */
|
|
5444
|
-
const POOL_AUTOMATION_RUN_PATH = "/plugins/dsh-workbuddy-xdpool/automation/run";
|
|
5445
|
-
/** Set or clear one account's reserved-credit floor. */
|
|
5446
|
-
const POOL_CREDIT_RESERVE_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/credit-reserve";
|
|
5447
|
-
//#endregion
|
|
5448
5635
|
//#region src/web-status.ts
|
|
5449
5636
|
/** Redact token-like content before it crosses to the browser. */
|
|
5450
5637
|
/**
|
|
@@ -5569,6 +5756,27 @@ function parseAccountToggle(body, known) {
|
|
|
5569
5756
|
};
|
|
5570
5757
|
}
|
|
5571
5758
|
/**
|
|
5759
|
+
* Validate an ignore/unignore request.
|
|
5760
|
+
*
|
|
5761
|
+
* Two different notions of "known" apply, which is why the caller passes both:
|
|
5762
|
+
*
|
|
5763
|
+
* - ignoring requires the account to be IN THE POOL, because the card can only
|
|
5764
|
+
* name an account it was just shown;
|
|
5765
|
+
* - un-ignoring requires the account to be ON THE IGNORE LIST instead — by
|
|
5766
|
+
* definition it is not in the pool any more, so checking the pool would make
|
|
5767
|
+
* the undo button impossible to use.
|
|
5768
|
+
*/
|
|
5769
|
+
function parseAccountIgnore(body, knownInPool, knownIgnored) {
|
|
5770
|
+
const accountId = typeof body["accountId"] === "string" ? body["accountId"].trim() : "";
|
|
5771
|
+
const ignored = body["ignored"];
|
|
5772
|
+
if (accountId === "" || typeof ignored !== "boolean") return void 0;
|
|
5773
|
+
if (ignored ? !knownInPool(accountId) : !knownIgnored(accountId)) return void 0;
|
|
5774
|
+
return {
|
|
5775
|
+
accountId,
|
|
5776
|
+
ignored
|
|
5777
|
+
};
|
|
5778
|
+
}
|
|
5779
|
+
/**
|
|
5572
5780
|
* Parse a manual-run request.
|
|
5573
5781
|
*
|
|
5574
5782
|
* The job name is checked against the real job list rather than passed through:
|
|
@@ -5740,7 +5948,8 @@ async function poolWebStatus(deps, region = "cn") {
|
|
|
5740
5948
|
regions,
|
|
5741
5949
|
shim,
|
|
5742
5950
|
automation,
|
|
5743
|
-
creditReserves: deps.pool.creditReservesInOrder()
|
|
5951
|
+
creditReserves: deps.pool.creditReservesInOrder(),
|
|
5952
|
+
ignored: deps.ignoredAccounts?.() ?? []
|
|
5744
5953
|
};
|
|
5745
5954
|
}
|
|
5746
5955
|
/**
|
|
@@ -5864,6 +6073,42 @@ function registerPoolStatusRoute(ctx, deps) {
|
|
|
5864
6073
|
}
|
|
5865
6074
|
});
|
|
5866
6075
|
/**
|
|
6076
|
+
* Throw one account out of the pool for good, or take it back.
|
|
6077
|
+
*
|
|
6078
|
+
* Same three guards as the disable route — POST only, loopback origin only,
|
|
6079
|
+
* and an explicit `accountId` — plus the semantic split in
|
|
6080
|
+
* {@link parseAccountIgnore}: ignoring checks the pool, un-ignoring checks
|
|
6081
|
+
* the ignore list.
|
|
6082
|
+
*
|
|
6083
|
+
* The credential FILE is deliberately left alone. Deleting it would sign the
|
|
6084
|
+
* desktop app out (the live file IS the app's current session) and would not
|
|
6085
|
+
* even stick, since the app rewrites it on the next sign-in. Ignoring is
|
|
6086
|
+
* enforced on the read side instead, which is both reversible and durable.
|
|
6087
|
+
*/
|
|
6088
|
+
const disposeAccountIgnore = ctx.webServer.register({
|
|
6089
|
+
kind: "exact",
|
|
6090
|
+
path: POOL_ACCOUNT_IGNORE_PATH,
|
|
6091
|
+
handler: async (req, res) => {
|
|
6092
|
+
if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
6093
|
+
if (!loopbackOrigin(req)) return json(res, 403, { error: "origin-not-trusted" });
|
|
6094
|
+
if (deps.setAccountIgnored === void 0) return json(res, 503, { error: "settings service unavailable; the ignore list cannot be saved" });
|
|
6095
|
+
try {
|
|
6096
|
+
const body = await readJsonBody(req);
|
|
6097
|
+
const knownInPool = new Set(deps.pool.list().map((account) => account.id));
|
|
6098
|
+
const knownIgnored = new Set((deps.ignoredAccounts?.() ?? []).map((entry) => entry.id));
|
|
6099
|
+
const parsed = parseAccountIgnore(body, (id) => knownInPool.has(id), (id) => knownIgnored.has(id));
|
|
6100
|
+
if (parsed === void 0) return json(res, 400, { error: "invalid account ignore payload" });
|
|
6101
|
+
await deps.setAccountIgnored(parsed.accountId, parsed.ignored);
|
|
6102
|
+
json(res, 200, {
|
|
6103
|
+
ok: true,
|
|
6104
|
+
...parsed
|
|
6105
|
+
});
|
|
6106
|
+
} catch (error) {
|
|
6107
|
+
json(res, 500, { error: safeMessage(error) });
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
});
|
|
6111
|
+
/**
|
|
5867
6112
|
* Run one automation job on demand.
|
|
5868
6113
|
*
|
|
5869
6114
|
* POST only, loopback origin only, and the job name must be one of the four
|
|
@@ -5923,6 +6168,7 @@ function registerPoolStatusRoute(ctx, deps) {
|
|
|
5923
6168
|
return () => {
|
|
5924
6169
|
disposeAutomationRun();
|
|
5925
6170
|
disposeCreditReserve();
|
|
6171
|
+
disposeAccountIgnore();
|
|
5926
6172
|
disposeCheckin();
|
|
5927
6173
|
disposeAccountDisable();
|
|
5928
6174
|
disposeModelsSave();
|
|
@@ -5933,6 +6179,153 @@ function registerPoolStatusRoute(ctx, deps) {
|
|
|
5933
6179
|
}, "dsh-workbuddy-xdpool: Web status route");
|
|
5934
6180
|
}
|
|
5935
6181
|
//#endregion
|
|
6182
|
+
//#region src/ignored.ts
|
|
6183
|
+
/**
|
|
6184
|
+
* The permanent ignore list: accounts the user has thrown out of the pool.
|
|
6185
|
+
*
|
|
6186
|
+
* Distinct from `disabledAccountIds`, which is a ROTATION preference. A disabled
|
|
6187
|
+
* account stays on the card, can be switched back on, and its credential keeps
|
|
6188
|
+
* being read on every scan. Ignoring is a statement about the ACCOUNT — "this
|
|
6189
|
+
* one is not mine any more" — so an ignored account is skipped BEFORE its
|
|
6190
|
+
* credential is decrypted, never appears among the pool's accounts, and does not
|
|
6191
|
+
* come back when the desktop app writes a fresh `.info` file for it.
|
|
6192
|
+
*
|
|
6193
|
+
* It lives in a plugin-owned file rather than in the settings document for one
|
|
6194
|
+
* practical reason: the CLI has no settings service. `dsh-workbuddy-xdpool
|
|
6195
|
+
* ignore <id>` runs without a host, so a settings-only list could be written by
|
|
6196
|
+
* the card and never by the CLI. One file, read by both halves, keeps the two
|
|
6197
|
+
* views of "which accounts are ignored" identical by construction.
|
|
6198
|
+
*
|
|
6199
|
+
* @module dsh-workbuddy-xdpool/ignored
|
|
6200
|
+
*/
|
|
6201
|
+
/** Directory holding this plugin's own state (imported snapshots, ignore list). */
|
|
6202
|
+
const PLUGIN_DATA_DIR_NAME = ".workbuddy-xdpool";
|
|
6203
|
+
/** File holding the permanent ignore list, inside {@link pluginDataDir}. */
|
|
6204
|
+
const IGNORED_FILE_NAME = "ignored.json";
|
|
6205
|
+
/**
|
|
6206
|
+
* The DSH home directory, honouring the same override the host uses.
|
|
6207
|
+
*
|
|
6208
|
+
* Shared by the CLI and the host so both halves resolve the same file: an
|
|
6209
|
+
* `ignore` written from the terminal has to be visible to the running plugin,
|
|
6210
|
+
* which is only true if they agree on where "home" is.
|
|
6211
|
+
*/
|
|
6212
|
+
function dshHome(env = process.env) {
|
|
6213
|
+
const fromEnv = env["DSH_HOME"];
|
|
6214
|
+
if (typeof fromEnv === "string" && fromEnv.trim() !== "") return fromEnv.trim();
|
|
6215
|
+
return join(homedir(), ".dsh");
|
|
6216
|
+
}
|
|
6217
|
+
/** This plugin's own state directory. */
|
|
6218
|
+
function pluginDataDir(env = process.env) {
|
|
6219
|
+
return join(dshHome(env), PLUGIN_DATA_DIR_NAME);
|
|
6220
|
+
}
|
|
6221
|
+
/** Absolute path of the ignore list. */
|
|
6222
|
+
function ignoredIdsPath(env = process.env) {
|
|
6223
|
+
return join(pluginDataDir(env), IGNORED_FILE_NAME);
|
|
6224
|
+
}
|
|
6225
|
+
/**
|
|
6226
|
+
* Normalize whatever the file holds into a clean list.
|
|
6227
|
+
*
|
|
6228
|
+
* Tolerant on purpose: this file is hand-editable and a malformed entry must not
|
|
6229
|
+
* take the pool down. An entry without a usable id is dropped; a missing label
|
|
6230
|
+
* falls back to the id so the card always has something to print.
|
|
6231
|
+
*/
|
|
6232
|
+
function normalize(raw) {
|
|
6233
|
+
if (typeof raw !== "object" || raw === null) return [];
|
|
6234
|
+
const entries = raw.accounts;
|
|
6235
|
+
if (!Array.isArray(entries)) return [];
|
|
6236
|
+
const out = [];
|
|
6237
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6238
|
+
for (const entry of entries) {
|
|
6239
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
6240
|
+
const record = entry;
|
|
6241
|
+
const id = typeof record["id"] === "string" ? record["id"].trim() : "";
|
|
6242
|
+
if (id === "" || seen.has(id)) continue;
|
|
6243
|
+
seen.add(id);
|
|
6244
|
+
const label = typeof record["label"] === "string" && record["label"].trim() !== "" ? record["label"] : id;
|
|
6245
|
+
const ignoredAt = typeof record["ignoredAt"] === "string" ? record["ignoredAt"] : "";
|
|
6246
|
+
out.push({
|
|
6247
|
+
id,
|
|
6248
|
+
label,
|
|
6249
|
+
ignoredAt
|
|
6250
|
+
});
|
|
6251
|
+
}
|
|
6252
|
+
return out;
|
|
6253
|
+
}
|
|
6254
|
+
/**
|
|
6255
|
+
* Read the ignore list, tolerating every "no list yet" shape.
|
|
6256
|
+
*
|
|
6257
|
+
* A missing file, unreadable file, or invalid JSON all mean the same thing to
|
|
6258
|
+
* the caller — nothing is ignored — so none of them throws. The pool must be
|
|
6259
|
+
* able to start on a machine that has never ignored anything.
|
|
6260
|
+
*/
|
|
6261
|
+
async function readIgnoredAccounts(path = ignoredIdsPath()) {
|
|
6262
|
+
try {
|
|
6263
|
+
return normalize(JSON.parse(await readFile(path, "utf8")));
|
|
6264
|
+
} catch {
|
|
6265
|
+
return [];
|
|
6266
|
+
}
|
|
6267
|
+
}
|
|
6268
|
+
/**
|
|
6269
|
+
* Synchronous read, for startup.
|
|
6270
|
+
*
|
|
6271
|
+
* The host applies the ignore list from inside `apply()`, which is synchronous,
|
|
6272
|
+
* and doing it there removes a startup race: an async load could resolve AFTER
|
|
6273
|
+
* the first account scan, which would let an ignored account slip into the pool
|
|
6274
|
+
* once per boot. The file is a few hundred bytes, so a blocking read at startup
|
|
6275
|
+
* costs nothing measurable.
|
|
6276
|
+
*/
|
|
6277
|
+
function readIgnoredAccountsSync(path = ignoredIdsPath()) {
|
|
6278
|
+
try {
|
|
6279
|
+
return normalize(JSON.parse(readFileSync(path, "utf8")));
|
|
6280
|
+
} catch {
|
|
6281
|
+
return [];
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6284
|
+
/**
|
|
6285
|
+
* Replace the ignore list, atomically.
|
|
6286
|
+
*
|
|
6287
|
+
* Written to a sibling temp file and renamed over the target so a crash (or a
|
|
6288
|
+
* concurrent reader) can never observe a half-written document — the ignore
|
|
6289
|
+
* list is the only thing standing between a dead account and the rotation, and
|
|
6290
|
+
* a truncated file reads as "nothing is ignored", which would quietly put every
|
|
6291
|
+
* discarded account back in the pool.
|
|
6292
|
+
*/
|
|
6293
|
+
async function writeIgnoredAccounts(accounts, path = ignoredIdsPath()) {
|
|
6294
|
+
const payload = JSON.stringify({
|
|
6295
|
+
version: 1,
|
|
6296
|
+
accounts
|
|
6297
|
+
}, null, 2);
|
|
6298
|
+
await mkdir(dirname(path), { recursive: true });
|
|
6299
|
+
const temp = `${path}.tmp`;
|
|
6300
|
+
await writeFile(temp, `${payload}\n`, "utf8");
|
|
6301
|
+
await rename(temp, path);
|
|
6302
|
+
}
|
|
6303
|
+
/**
|
|
6304
|
+
* Add one account to the ignore list, preserving the rest.
|
|
6305
|
+
*
|
|
6306
|
+
* A read-modify-write rather than a wholesale replace: the card and the CLI can
|
|
6307
|
+
* both be open, and each request names exactly one account, so re-writing the
|
|
6308
|
+
* whole list from a stale view would drop the other side's edits.
|
|
6309
|
+
*/
|
|
6310
|
+
async function ignoreAccount(account, path = ignoredIdsPath()) {
|
|
6311
|
+
const current = await readIgnoredAccounts(path);
|
|
6312
|
+
if (current.some((entry) => entry.id === account.id)) return current;
|
|
6313
|
+
const next = [...current, {
|
|
6314
|
+
id: account.id,
|
|
6315
|
+
label: account.label ?? account.id,
|
|
6316
|
+
ignoredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6317
|
+
}];
|
|
6318
|
+
await writeIgnoredAccounts(next, path);
|
|
6319
|
+
return next;
|
|
6320
|
+
}
|
|
6321
|
+
/** Drop one account from the ignore list. Returns the resulting list. */
|
|
6322
|
+
async function unignoreAccount(accountId, path = ignoredIdsPath()) {
|
|
6323
|
+
const current = await readIgnoredAccounts(path);
|
|
6324
|
+
const next = current.filter((entry) => entry.id !== accountId);
|
|
6325
|
+
if (next.length !== current.length) await writeIgnoredAccounts(next, path);
|
|
6326
|
+
return next;
|
|
6327
|
+
}
|
|
6328
|
+
//#endregion
|
|
5936
6329
|
//#region src/index.ts
|
|
5937
6330
|
/**
|
|
5938
6331
|
* Host-side plugin entry. Registers the `workbuddy-xdpool` provider into the
|
|
@@ -6135,6 +6528,14 @@ function createCore(logger) {
|
|
|
6135
6528
|
*/
|
|
6136
6529
|
function apply(ctx, config = {}) {
|
|
6137
6530
|
const core = createCore(ctx.logger);
|
|
6531
|
+
const ignoredPath = ignoredIdsPath();
|
|
6532
|
+
let ignoredAccounts = readIgnoredAccountsSync(ignoredPath);
|
|
6533
|
+
core.pool.applyIgnored(ignoredAccounts.map((entry) => entry.id));
|
|
6534
|
+
/** Re-read the ignore file and push it into the pool. */
|
|
6535
|
+
const refreshIgnored = () => {
|
|
6536
|
+
ignoredAccounts = readIgnoredAccountsSync(ignoredPath);
|
|
6537
|
+
core.pool.applyIgnored(ignoredAccounts.map((entry) => entry.id));
|
|
6538
|
+
};
|
|
6138
6539
|
/**
|
|
6139
6540
|
* Invalidate the provider snapshot so the picker re-reads the catalog.
|
|
6140
6541
|
*
|
|
@@ -6183,8 +6584,11 @@ function apply(ctx, config = {}) {
|
|
|
6183
6584
|
...imageModelIds === void 0 ? {} : { imageModelIds },
|
|
6184
6585
|
...contextBudgets === void 0 ? {} : { contextBudgets }
|
|
6185
6586
|
};
|
|
6186
|
-
|
|
6187
|
-
|
|
6587
|
+
const fallbackSelection = (region) => {
|
|
6588
|
+
return legacySelection.enabledModelIds === void 0 && legacySelection.imageModelIds === void 0 && legacySelection.contextBudgets === void 0 ? core.catalogs[region].currentSelection() : legacySelection;
|
|
6589
|
+
};
|
|
6590
|
+
core.catalogs.cn.applySelection(modelSelectionCn ?? fallbackSelection("cn"));
|
|
6591
|
+
core.catalogs.global.applySelection(modelSelectionGlobal ?? fallbackSelection("global"));
|
|
6188
6592
|
core.scheduler.applyConfig(automationOptions(automation));
|
|
6189
6593
|
};
|
|
6190
6594
|
const settingsService = ctx.settings;
|
|
@@ -6194,6 +6598,7 @@ function apply(ctx, config = {}) {
|
|
|
6194
6598
|
ctx.on("loader/volatile-update", () => {
|
|
6195
6599
|
applyConfigFromSource();
|
|
6196
6600
|
});
|
|
6601
|
+
applyConfigFromSource();
|
|
6197
6602
|
/**
|
|
6198
6603
|
* Write one key of the plugin's own settings section. Only ever called with
|
|
6199
6604
|
* the three model-selection keys, so the settings file cannot be steered from
|
|
@@ -6324,7 +6729,19 @@ function apply(ctx, config = {}) {
|
|
|
6324
6729
|
if (reserve > 0) next[accountId] = reserve;
|
|
6325
6730
|
else delete next[accountId];
|
|
6326
6731
|
await setSetting("creditReserves", next, next);
|
|
6327
|
-
}
|
|
6732
|
+
},
|
|
6733
|
+
setAccountIgnored: async (accountId, ignored) => {
|
|
6734
|
+
if (ignored) {
|
|
6735
|
+
const known = core.pool.list().find((account) => account.id === accountId);
|
|
6736
|
+
await ignoreAccount({
|
|
6737
|
+
id: accountId,
|
|
6738
|
+
...known === void 0 ? {} : { label: known.label }
|
|
6739
|
+
}, ignoredPath);
|
|
6740
|
+
} else await unignoreAccount(accountId, ignoredPath);
|
|
6741
|
+
refreshIgnored();
|
|
6742
|
+
if (!ignored) await core.pool.scan();
|
|
6743
|
+
},
|
|
6744
|
+
ignoredAccounts: () => ignoredAccounts
|
|
6328
6745
|
}));
|
|
6329
6746
|
api = {
|
|
6330
6747
|
...core,
|
|
@@ -6457,4 +6874,4 @@ function apply(ctx, config = {}) {
|
|
|
6457
6874
|
});
|
|
6458
6875
|
}
|
|
6459
6876
|
//#endregion
|
|
6460
|
-
export { APPEARANCE_THEME_KEY, AUTOMATION_JOB_KINDS, AUTOMATION_TICK_MS, BUDDY_APP_ID, BUDDY_APP_NAME, Config, DEFAULT_CONTEXT_BUDGET, EVENT_SCORE_WAIT_MS, FALLBACK_WORKBUDDY_MODELS, LIBRARY_DOC_URL, LIGHTHOUSE_EXPERT_ID, PLAYBOOK_CASE_ID, PLAYBOOK_CASE_NAME, POOL_AUTOMATION_RUN_PATH, POOL_CHECKIN_PATH, POOL_CREDIT_RESERVE_PATH, POOL_MODELS_SAVE_PATH, POOL_RESCAN_PATH, POOL_RESET_COOLDOWN_PATH, POOL_STATUS_PATH, SKILL_ID, SKILL_NAME, TEMPLATE_PRESETS, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_LIVE_FILENAME, WORKBUDDY_POOL_PROVIDER, WORKBUDDY_POOL_SETTINGS_NS, WorkBuddyAccountPool, WorkBuddyCatalog, WorkBuddyScheduler, WorkBuddyUpstreamClient, appearanceChain, apply, automationChain, automationOptions, buddyAppChain, buddyAppEvents, buildStatus, candidateAuthDirs, canvasChain, chatChain, classifyUpstreamError, createCore, createWorkBuddyAdapter, createWorkBuddyShim, currentApi, dayKey, defaultDesktopAuthDirs, desktopAutomationCreatedEvent, desktopCanvasEvents, desktopChatEvents, expertActualUseEvent, expertChatEvents, expertSummonEvents, formatRates, formatStatus, inject, isAutomationJobKind, isFireHour, libraryReadChain, modelSelectionKeyFor, name, parseRateLimitReset, parseWorkBuddyAuth, playbookChain, poolWebStatus, registerPoolStatusRoute, setApi, skillChain, templateChain, templateChains, workbuddyAccountId };
|
|
6877
|
+
export { APPEARANCE_THEME_KEY, AUTOMATION_JOB_KINDS, AUTOMATION_TICK_MS, BUDDY_APP_ID, BUDDY_APP_NAME, Config, DEFAULT_AUTOMATION_HOURS, DEFAULT_CONTEXT_BUDGET, EVENT_SCORE_WAIT_MS, FALLBACK_WORKBUDDY_MODELS, IGNORED_FILE_NAME, LIBRARY_DOC_URL, LIGHTHOUSE_EXPERT_ID, PLAYBOOK_CASE_ID, PLAYBOOK_CASE_NAME, PLUGIN_DATA_DIR_NAME, POOL_ACCOUNT_IGNORE_PATH, POOL_AUTOMATION_RUN_PATH, POOL_CHECKIN_PATH, POOL_CREDIT_RESERVE_PATH, POOL_MODELS_SAVE_PATH, POOL_RESCAN_PATH, POOL_RESET_COOLDOWN_PATH, POOL_STATUS_PATH, SKILL_ID, SKILL_NAME, TEMPLATE_PRESETS, WORKBUDDY_AUTH_FILE_ENV, WORKBUDDY_LIVE_FILENAME, WORKBUDDY_POOL_PROVIDER, WORKBUDDY_POOL_SETTINGS_NS, WorkBuddyAccountPool, WorkBuddyCatalog, WorkBuddyScheduler, WorkBuddyUpstreamClient, appearanceChain, apply, automationChain, automationOptions, buddyAppChain, buddyAppEvents, buildStatus, candidateAuthDirs, canvasChain, chatChain, classifyUpstreamError, createCore, createWorkBuddyAdapter, createWorkBuddyShim, currentApi, dayKey, defaultDesktopAuthDirs, desktopAutomationCreatedEvent, desktopCanvasEvents, desktopChatEvents, dshHome, expertActualUseEvent, expertChatEvents, expertSummonEvents, formatRates, formatStatus, ignoreAccount, ignoredIdsPath, inject, isAutomationJobKind, isFireHour, libraryReadChain, modelSelectionKeyFor, name, parseRateLimitReset, parseWorkBuddyAuth, playbookChain, pluginDataDir, poolWebStatus, readIgnoredAccounts, readIgnoredAccountsSync, registerPoolStatusRoute, setApi, skillChain, templateChain, templateChains, unignoreAccount, workbuddyAccountId, writeIgnoredAccounts };
|