dsh-workbuddy-xdpool 1.6.1 → 1.7.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 +140 -0
- package/lib/bin.js +49 -20
- package/lib/client.js +597 -416
- package/lib/index.d.ts +9 -1
- package/lib/index.js +138 -26
- package/package.json +3 -3
package/lib/index.d.ts
CHANGED
|
@@ -1413,7 +1413,7 @@ export declare class WorkBuddyScheduler {
|
|
|
1413
1413
|
* long before the settings section exists; a ledger written before that point
|
|
1414
1414
|
* would have nowhere to go.
|
|
1415
1415
|
*/
|
|
1416
|
-
setEarningsPersistence(save: (ledger: AutomationLedger) => void): void;
|
|
1416
|
+
setEarningsPersistence(save: (ledger: AutomationLedger) => void | Promise<void>): void;
|
|
1417
1417
|
/**
|
|
1418
1418
|
* Fold a previously persisted ledger back in, when it belongs to today.
|
|
1419
1419
|
*
|
|
@@ -2158,6 +2158,14 @@ export declare const modelSelectionKeyFor: (region: 'cn' | 'global') => string;
|
|
|
2158
2158
|
* also why `contextBudgets` is a real dictionary (`z.dict`) - an open object
|
|
2159
2159
|
* schema reads as "an object with no fields" and the fold then throws while
|
|
2160
2160
|
* the provider row is rendered.
|
|
2161
|
+
*
|
|
2162
|
+
* Every field is wrapped in {@link asVolatile}: on the 0.1.7 line the settings
|
|
2163
|
+
* write gate refuses an entry whose schema declares no volatile field at all
|
|
2164
|
+
* ("Plugin entry ... has no volatile fields") and `describe()` skips such an
|
|
2165
|
+
* entry — so an unmarked schema means the card can neither render nor save. On
|
|
2166
|
+
* the 0.1.5 line the wrapper degrades to an identity no-op (see its JSDoc), and
|
|
2167
|
+
* the value the running instance reads is a plain value either way once
|
|
2168
|
+
* unwrapped.
|
|
2161
2169
|
*/
|
|
2162
2170
|
export declare const Config: z<Config>;
|
|
2163
2171
|
/** Everything the CLI needs from a live plugin instance. */
|
package/lib/index.js
CHANGED
|
@@ -1747,8 +1747,20 @@ const AAD_DOMAIN = Buffer.from("WB-AAD\0", "ascii");
|
|
|
1747
1747
|
const SYMMETRIC_SCHEME = "sym-v1";
|
|
1748
1748
|
/** Env override pointing at the WorkBuddy desktop executable. */
|
|
1749
1749
|
const WORKBUDDY_APP_EXECUTABLE_ENV = "WORKBUDDY_APP_EXECUTABLE";
|
|
1750
|
-
/**
|
|
1751
|
-
|
|
1750
|
+
/**
|
|
1751
|
+
* How long the app is given to answer with its key payload.
|
|
1752
|
+
*
|
|
1753
|
+
* 30s, not 10s: the child is the WorkBuddy Electron binary running as plain
|
|
1754
|
+
* Node, and its FIRST spawn on a cold machine costs several seconds on its own
|
|
1755
|
+
* (measured 4.5s here) before the endpoint security stack has warmed its scan
|
|
1756
|
+
* cache. Under load — a concurrent `pnpm install` from the market, a running
|
|
1757
|
+
* full-disk scan — that first spawn crosses a 10s budget, the fetch rejects,
|
|
1758
|
+
* `readAtRestKey` returns undefined, and every encrypted credential then reads
|
|
1759
|
+
* as `WorkBuddyEncryptedCredentialError` until the 60s negative cache expires.
|
|
1760
|
+
* A successful fetch is cached for the process lifetime, so the longer budget
|
|
1761
|
+
* is only ever paid once per process, and only when the app is present but slow.
|
|
1762
|
+
*/
|
|
1763
|
+
const KEY_FETCH_TIMEOUT_MS = 3e4;
|
|
1752
1764
|
/**
|
|
1753
1765
|
* Executable file names the desktop app ships under, in probe order.
|
|
1754
1766
|
*
|
|
@@ -2003,6 +2015,7 @@ function windowsFallbackAppPaths(env) {
|
|
|
2003
2015
|
}
|
|
2004
2016
|
roots.add(join(drive, "Program Files"));
|
|
2005
2017
|
roots.add(join(drive, "Program Files (x86)"));
|
|
2018
|
+
roots.add(drive);
|
|
2006
2019
|
}
|
|
2007
2020
|
for (const root of roots) for (const name of APP_EXECUTABLE_NAMES) {
|
|
2008
2021
|
out.push(join(root, "WorkBuddy", name));
|
|
@@ -4160,12 +4173,16 @@ var WorkBuddyScheduler = class {
|
|
|
4160
4173
|
*/
|
|
4161
4174
|
persistEarnings() {
|
|
4162
4175
|
if (this.saveEarningsFn === void 0) return;
|
|
4176
|
+
const accounts = {};
|
|
4177
|
+
for (const [id, entry] of this.earnings) accounts[id] = entry;
|
|
4178
|
+
const ledger = {
|
|
4179
|
+
date: this.earningsDate,
|
|
4180
|
+
accounts
|
|
4181
|
+
};
|
|
4163
4182
|
try {
|
|
4164
|
-
const
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
date: this.earningsDate,
|
|
4168
|
-
accounts
|
|
4183
|
+
const saved = this.saveEarningsFn(ledger);
|
|
4184
|
+
if (saved !== void 0 && typeof saved.then === "function") saved.then(void 0, (error) => {
|
|
4185
|
+
this.logger.warn?.("dsh-workbuddy-xdpool: could not persist automation earnings:", error);
|
|
4169
4186
|
});
|
|
4170
4187
|
} catch (error) {
|
|
4171
4188
|
this.logger.warn?.("dsh-workbuddy-xdpool: could not persist automation earnings:", error);
|
|
@@ -5962,6 +5979,56 @@ function automationOptions(automation) {
|
|
|
5962
5979
|
* region that has never been saved stays absent so `applyConfigFromSource` can
|
|
5963
5980
|
* fall back to the legacy flat keys.
|
|
5964
5981
|
*/
|
|
5982
|
+
function asVolatile(schema) {
|
|
5983
|
+
const candidate = schema;
|
|
5984
|
+
return typeof candidate.volatile === "function" ? candidate.volatile() : schema;
|
|
5985
|
+
}
|
|
5986
|
+
/**
|
|
5987
|
+
* Peel one live volatile reference.
|
|
5988
|
+
*
|
|
5989
|
+
* On 0.1.7 a volatile field is handed back as `{ get(): T }` rather than a
|
|
5990
|
+
* plain value, so a running instance observes a settings edit without being
|
|
5991
|
+
* remounted. Every read of a marked field therefore has to unwrap: passing the
|
|
5992
|
+
* reference onward compares an object against a string and reports the field as
|
|
5993
|
+
* unset. On 0.1.5 the field is already a plain value, so this is a no-op.
|
|
5994
|
+
*
|
|
5995
|
+
* The test is duck-typed on purpose — importing `isVolatile` would add a
|
|
5996
|
+
* dependency the 0.1.5 line does not carry.
|
|
5997
|
+
*/
|
|
5998
|
+
function unwrapVolatile(value) {
|
|
5999
|
+
if (value !== null && typeof value === "object" && typeof value.get === "function") return value.get();
|
|
6000
|
+
return value;
|
|
6001
|
+
}
|
|
6002
|
+
/**
|
|
6003
|
+
* Deep copy of a config value with every live `{get(): T}` reference replaced
|
|
6004
|
+
* by the value it resolves to.
|
|
6005
|
+
*
|
|
6006
|
+
* {@link unwrapVolatile} peels only the one level an ordinary read needs. A
|
|
6007
|
+
* settings service is different: it validates and `structuredClone`s the WHOLE
|
|
6008
|
+
* object, so a reference surviving anywhere inside it fails schema validation
|
|
6009
|
+
* with a message that names the field but not the cause —
|
|
6010
|
+
* `$.authFile expected string but got [object Object]`. That is what makes the
|
|
6011
|
+
* namespace fail to register and the card silently disappear, which is why the
|
|
6012
|
+
* object handed to `installSection` goes through this first.
|
|
6013
|
+
*/
|
|
6014
|
+
function unwrapVolatileDeep(value) {
|
|
6015
|
+
const peeled = unwrapVolatile(value);
|
|
6016
|
+
if (Array.isArray(peeled)) return peeled.map((entry) => unwrapVolatileDeep(entry));
|
|
6017
|
+
if (peeled !== null && typeof peeled === "object") {
|
|
6018
|
+
const out = {};
|
|
6019
|
+
for (const [key, entry] of Object.entries(peeled)) out[key] = unwrapVolatileDeep(entry);
|
|
6020
|
+
return out;
|
|
6021
|
+
}
|
|
6022
|
+
return peeled;
|
|
6023
|
+
}
|
|
6024
|
+
/**
|
|
6025
|
+
* One region's model-selection schema.
|
|
6026
|
+
*
|
|
6027
|
+
* Every field is optional on purpose: an absent field keeps its documented
|
|
6028
|
+
* meaning ("all enabled" / "follow the upstream image flag" / "no cap"), and a
|
|
6029
|
+
* region that has never been saved stays absent so `applyConfigFromSource` can
|
|
6030
|
+
* fall back to the legacy flat keys.
|
|
6031
|
+
*/
|
|
5965
6032
|
const modelSelectionSchema = z.object({
|
|
5966
6033
|
enabledModelIds: z.array(z.string()).description("Model ids enabled in this region's picker (absent = all)"),
|
|
5967
6034
|
imageModelIds: z.array(z.string()).description("Model ids accepting image input in this region (absent = follow upstream)"),
|
|
@@ -5999,24 +6066,32 @@ const modelSelectionKeyFor = (region) => region === "cn" ? "modelSelectionCn" :
|
|
|
5999
6066
|
* also why `contextBudgets` is a real dictionary (`z.dict`) - an open object
|
|
6000
6067
|
* schema reads as "an object with no fields" and the fold then throws while
|
|
6001
6068
|
* the provider row is rendered.
|
|
6069
|
+
*
|
|
6070
|
+
* Every field is wrapped in {@link asVolatile}: on the 0.1.7 line the settings
|
|
6071
|
+
* write gate refuses an entry whose schema declares no volatile field at all
|
|
6072
|
+
* ("Plugin entry ... has no volatile fields") and `describe()` skips such an
|
|
6073
|
+
* entry — so an unmarked schema means the card can neither render nor save. On
|
|
6074
|
+
* the 0.1.5 line the wrapper degrades to an identity no-op (see its JSDoc), and
|
|
6075
|
+
* the value the running instance reads is a plain value either way once
|
|
6076
|
+
* unwrapped.
|
|
6002
6077
|
*/
|
|
6003
6078
|
const Config = z.object({
|
|
6004
|
-
authFile: z.string().description("WorkBuddy desktop auth file (defaults to the app own location)"),
|
|
6005
|
-
cooldownMs: z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds"),
|
|
6006
|
-
distribution: z.union([
|
|
6079
|
+
authFile: asVolatile(z.string().description("WorkBuddy desktop auth file (defaults to the app own location)")),
|
|
6080
|
+
cooldownMs: asVolatile(z.number().step(1).min(1e3).default(6e4).description("Rate-limit cooldown per account, in milliseconds")),
|
|
6081
|
+
distribution: asVolatile(z.union([
|
|
6007
6082
|
"priority",
|
|
6008
6083
|
"round-robin",
|
|
6009
6084
|
"balanced"
|
|
6010
|
-
]).default("priority").description("How requests are spread: priority (drain one), round-robin (in order), or balanced (idle-weighted random)"),
|
|
6011
|
-
disabledAccountIds: z.array(z.string()).default([]).description("Account ids excluded from the pool (empty = every discovered account participates)"),
|
|
6012
|
-
creditReserves: z.dict(z.number().step(1).min(0)).default({}).description("Per-account credit floor: stop using an account once its balance reaches this value"),
|
|
6013
|
-
enabledModelIds: z.array(z.string()).default([]).description("Legacy shared model-id list; used by a region that has no per-region selection yet"),
|
|
6014
|
-
imageModelIds: z.array(z.string()).default([]).description("Legacy shared image-id list; used by a region that has no per-region selection yet"),
|
|
6015
|
-
contextBudgets: z.dict(z.number().step(1).min(1)).default({}).description("Legacy shared context budgets; used by a region with no per-region selection yet"),
|
|
6016
|
-
modelSelectionCn: modelSelectionSchema.description("Model selection for the domestic gateway"),
|
|
6017
|
-
modelSelectionGlobal: modelSelectionSchema.description("Model selection for the international gateway"),
|
|
6018
|
-
automation: automationSchema.description("Daily points automation (activity report, task claiming, check-in)"),
|
|
6019
|
-
automationEarnings: z.any().description("Automation earnings ledger (written by the scheduler)")
|
|
6085
|
+
]).default("priority").description("How requests are spread: priority (drain one), round-robin (in order), or balanced (idle-weighted random)")),
|
|
6086
|
+
disabledAccountIds: asVolatile(z.array(z.string()).default([]).description("Account ids excluded from the pool (empty = every discovered account participates)")),
|
|
6087
|
+
creditReserves: asVolatile(z.dict(z.number().step(1).min(0)).default({}).description("Per-account credit floor: stop using an account once its balance reaches this value")),
|
|
6088
|
+
enabledModelIds: asVolatile(z.array(z.string()).default([]).description("Legacy shared model-id list; used by a region that has no per-region selection yet")),
|
|
6089
|
+
imageModelIds: asVolatile(z.array(z.string()).default([]).description("Legacy shared image-id list; used by a region that has no per-region selection yet")),
|
|
6090
|
+
contextBudgets: asVolatile(z.dict(z.number().step(1).min(1)).default({}).description("Legacy shared context budgets; used by a region with no per-region selection yet")),
|
|
6091
|
+
modelSelectionCn: asVolatile(modelSelectionSchema.description("Model selection for the domestic gateway")),
|
|
6092
|
+
modelSelectionGlobal: asVolatile(modelSelectionSchema.description("Model selection for the international gateway")),
|
|
6093
|
+
automation: asVolatile(automationSchema.description("Daily points automation (activity report, task claiming, check-in)")),
|
|
6094
|
+
automationEarnings: asVolatile(z.any().description("Automation earnings ledger (written by the scheduler)"))
|
|
6020
6095
|
});
|
|
6021
6096
|
/** Live API, published for the CLI. */
|
|
6022
6097
|
let api;
|
|
@@ -6073,10 +6148,21 @@ function apply(ctx, config = {}) {
|
|
|
6073
6148
|
/** The domestic adapter, published for the CLI after registration. */
|
|
6074
6149
|
let builtAdapter;
|
|
6075
6150
|
let invalidateCatalog = () => {};
|
|
6076
|
-
|
|
6151
|
+
/**
|
|
6152
|
+
* The effective config with every live volatile reference peeled.
|
|
6153
|
+
*
|
|
6154
|
+
* On 0.1.7 a field marked volatile is handed to the plugin as `{get(): T}` so
|
|
6155
|
+
* a settings edit is observed without a remount, and the loader keeps the
|
|
6156
|
+
* reference live. Every read below therefore goes through this view: a raw
|
|
6157
|
+
* reference compared against a string reports the field as unset, which is
|
|
6158
|
+
* how a saved reserve would read back as "0" while the file holds the value.
|
|
6159
|
+
* On 0.1.5 there is nothing to peel and this is the identity.
|
|
6160
|
+
*/
|
|
6161
|
+
let rawCurrent = () => config;
|
|
6162
|
+
const current = () => unwrapVolatileDeep(rawCurrent());
|
|
6077
6163
|
const sectionHooks = {
|
|
6078
6164
|
setSource(source) {
|
|
6079
|
-
|
|
6165
|
+
rawCurrent = source;
|
|
6080
6166
|
},
|
|
6081
6167
|
onChange() {
|
|
6082
6168
|
applyConfigFromSource();
|
|
@@ -6102,8 +6188,12 @@ function apply(ctx, config = {}) {
|
|
|
6102
6188
|
core.scheduler.applyConfig(automationOptions(automation));
|
|
6103
6189
|
};
|
|
6104
6190
|
const settingsService = ctx.settings;
|
|
6105
|
-
if (typeof settingsService.installSection === "function") settingsService.installSection(ctx, WORKBUDDY_POOL_SETTINGS_NS, Config, config, sectionHooks);
|
|
6106
|
-
|
|
6191
|
+
if (typeof settingsService.installSection === "function") settingsService.installSection(ctx, WORKBUDDY_POOL_SETTINGS_NS, Config, unwrapVolatileDeep(config), sectionHooks);
|
|
6192
|
+
if (typeof settingsService.configure === "function") ctx.effect(() => settingsService.configure?.({ auto: true }, ctx.fiber) ?? (() => {}));
|
|
6193
|
+
if (typeof settingsService.installSection !== "function" && typeof settingsService.configure !== "function") ctx.logger.warn?.("dsh-workbuddy-xdpool: settings service exposes neither installSection nor configure; the card will not mount");
|
|
6194
|
+
ctx.on("loader/volatile-update", () => {
|
|
6195
|
+
applyConfigFromSource();
|
|
6196
|
+
});
|
|
6107
6197
|
/**
|
|
6108
6198
|
* Write one key of the plugin's own settings section. Only ever called with
|
|
6109
6199
|
* the three model-selection keys, so the settings file cannot be steered from
|
|
@@ -6139,11 +6229,31 @@ function apply(ctx, config = {}) {
|
|
|
6139
6229
|
if (typeof value === "object" && value !== null) return `{${Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
6140
6230
|
return JSON.stringify(value);
|
|
6141
6231
|
}
|
|
6232
|
+
/**
|
|
6233
|
+
* The namespace `settings.update` expects on the running line.
|
|
6234
|
+
*
|
|
6235
|
+
* 0.1.5 resolves writers by the NAMESPACE the plugin registered through
|
|
6236
|
+
* `installSection`, so the plugin picks it. 0.1.7 resolves them by the HOST
|
|
6237
|
+
* PLUGIN ENTRY ID instead (`configEditor.entries().find(row => row.options.id
|
|
6238
|
+
* === ns)`), and the profile patch chooses that id — the live Desktop host
|
|
6239
|
+
* mounts this plugin as `llm-workbuddy-xdpool`, but a marketplace install can
|
|
6240
|
+
* wrap it (`mkt-...`) or mount it through an `include` row. Asking the fiber
|
|
6241
|
+
* for its own entry id is therefore the only correct answer there; guessing
|
|
6242
|
+
* our own namespace makes every save throw `No configurable plugin entry`.
|
|
6243
|
+
*/
|
|
6244
|
+
const settingsWriteNs = typeof settingsService.installSection === "function" ? WORKBUDDY_POOL_SETTINGS_NS : (() => {
|
|
6245
|
+
try {
|
|
6246
|
+
const entryId = ctx.fiber?.entry?.options?.id;
|
|
6247
|
+
return entryId === void 0 || entryId === "" ? WORKBUDDY_POOL_SETTINGS_NS : entryId;
|
|
6248
|
+
} catch {
|
|
6249
|
+
return WORKBUDDY_POOL_SETTINGS_NS;
|
|
6250
|
+
}
|
|
6251
|
+
})();
|
|
6142
6252
|
const setSetting = async (key, value, expected) => {
|
|
6143
6253
|
if (value === void 0) return;
|
|
6144
6254
|
const update = settingsService?.update;
|
|
6145
6255
|
if (update === void 0) throw new Error(`settings service has no update(); ${key} was not saved`);
|
|
6146
|
-
await update.call(settingsService,
|
|
6256
|
+
await update.call(settingsService, settingsWriteNs, { [key]: value });
|
|
6147
6257
|
if (expected !== void 0) {
|
|
6148
6258
|
const stored = current()[key];
|
|
6149
6259
|
if (!stableJsonEqual(stored, expected)) throw new Error(`settings field "${key}" was not persisted`);
|
|
@@ -6339,7 +6449,9 @@ function apply(ctx, config = {}) {
|
|
|
6339
6449
|
ctx.logger.warn(`dsh-workbuddy-xdpool: ${region} model catalog unavailable; using static fallback`, error);
|
|
6340
6450
|
}
|
|
6341
6451
|
invalidateCatalog();
|
|
6342
|
-
})()
|
|
6452
|
+
})().catch((error) => {
|
|
6453
|
+
ctx.logger.warn("dsh-workbuddy-xdpool: account catalog seed failed", error);
|
|
6454
|
+
});
|
|
6343
6455
|
}, (error) => {
|
|
6344
6456
|
ctx.logger.error("dsh-workbuddy-xdpool: shim failed to listen", error);
|
|
6345
6457
|
});
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-workbuddy-xdpool",
|
|
3
3
|
"displayName": "DSH WorkBuddy XD Pool",
|
|
4
4
|
"description": "Merge every locally signed-in WorkBuddy account into DeepSeek Harness as one auto-failing-over model pool (multi-account rotation, live credits, daily check-in and model catalog).",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.7.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "XDTrees",
|
|
8
8
|
"homepage": "https://github.com/XDTrees/dsh-workbuddy-xdpool#readme",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
],
|
|
25
25
|
"type": "module",
|
|
26
26
|
"engines": {
|
|
27
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
27
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
28
|
+
"dsh": ">=0.1.5-rc.1 <0.2.0"
|
|
28
29
|
},
|
|
29
30
|
"main": "lib/index.js",
|
|
30
31
|
"types": "lib/index.d.ts",
|
|
@@ -63,7 +64,6 @@
|
|
|
63
64
|
},
|
|
64
65
|
"client": {
|
|
65
66
|
"inject": [
|
|
66
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
67
67
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
68
68
|
"@deepseek-ai/dsh-client-ui-slots",
|
|
69
69
|
"@deepseek-ai/dsh-client-ui-settings-plugins",
|