dsh-workbuddy-xdpool 1.7.1 → 1.7.3
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 +146 -169
- package/lib/bin.js +286 -54
- package/lib/client.js +90 -16
- package/lib/index.d.ts +726 -550
- package/lib/index.js +384 -69
- package/package.json +1 -1
package/lib/bin.js
CHANGED
|
@@ -10,6 +10,39 @@ import "@earendil-works/pi-ai/api/openai-completions.lazy";
|
|
|
10
10
|
import "@deepseek-ai/dsh-llm";
|
|
11
11
|
import "@deepseek-ai/dsh-llm-pi-ai";
|
|
12
12
|
//#region src/upstream.ts
|
|
13
|
+
/**
|
|
14
|
+
* Backoff between catalog-fetch attempts.
|
|
15
|
+
*
|
|
16
|
+
* Three retries after the first try, so a startup network hiccup does not cost
|
|
17
|
+
* the user their model list for the whole session. The window covers the case
|
|
18
|
+
* that actually bit: several independent components reported `fetch failed`
|
|
19
|
+
* within the same second while the machine was still bringing its network up,
|
|
20
|
+
* and the very next request succeeded 0.7s later.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately short and bounded. This runs during plugin startup, so a long
|
|
23
|
+
* ladder would delay the provider appearing at all; the total added latency
|
|
24
|
+
* here is ~12s in the worst case, and only when the network is genuinely down.
|
|
25
|
+
*/
|
|
26
|
+
const CATALOG_RETRY_BACKOFF_MS = [
|
|
27
|
+
1e3,
|
|
28
|
+
3e3,
|
|
29
|
+
8e3
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Sleep helper for the retry ladder.
|
|
33
|
+
*
|
|
34
|
+
* Deliberately NOT unref'd. An unref'd timer does not keep the event loop
|
|
35
|
+
* alive, so awaiting one can hang forever when nothing else is pending — the
|
|
36
|
+
* retry would simply never resume. That is exactly what the first version of
|
|
37
|
+
* this did, and it wedged the very first end-to-end run: `fetchModels` never
|
|
38
|
+
* settled. The wait is at most a few seconds and only happens on failure, so
|
|
39
|
+
* holding the loop open is the correct trade.
|
|
40
|
+
*/
|
|
41
|
+
function delay(ms) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
setTimeout(resolve, ms);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
13
46
|
/** CN chat base per dingminhua's on-machine probe (HTTP 200 for chat/models). */
|
|
14
47
|
const CN_CHAT_BASE = "https://copilot.tencent.com";
|
|
15
48
|
/** Billing/console base — `www.codebuddy.cn` is the billing origin. */
|
|
@@ -31,6 +64,13 @@ const MODELS_CATALOG_PATH = "/v2/enterprises/personal/models";
|
|
|
31
64
|
const GLOBAL_CONFIG_PATH = "/v3/config";
|
|
32
65
|
const JSON_TIMEOUT_MS = 3e4;
|
|
33
66
|
const ERROR_BODY_LIMIT = 4096;
|
|
67
|
+
/** True for a request the CALLER aborted, which must never be retried. */
|
|
68
|
+
function isAbortError(error) {
|
|
69
|
+
if (typeof error !== "object" || error === null) return false;
|
|
70
|
+
if (error.name === "AbortError") return true;
|
|
71
|
+
const cause = error.cause;
|
|
72
|
+
return typeof cause === "object" && cause !== null && cause.name === "AbortError";
|
|
73
|
+
}
|
|
34
74
|
/** Cap on the reassembled compaction reply, guarding against a runaway stream. */
|
|
35
75
|
const COMPLETION_TEXT_LIMIT = 65536;
|
|
36
76
|
/** Insufficient-credit markers, ASCII lowercase plus the original Chinese. */
|
|
@@ -638,9 +678,20 @@ function parseTask(value) {
|
|
|
638
678
|
var WorkBuddyUpstreamClient = class {
|
|
639
679
|
fetchImpl;
|
|
640
680
|
clientVersion;
|
|
681
|
+
/** Backoff ladder for `fetchModels`; empty means a single attempt. */
|
|
682
|
+
catalogRetryBackoffMs;
|
|
683
|
+
/**
|
|
684
|
+
* Optional logger for retry notices.
|
|
685
|
+
*
|
|
686
|
+
* Set by the host so a retry is visible in the log with its attempt count —
|
|
687
|
+
* without it, a retry that eventually succeeds is invisible, and an operator
|
|
688
|
+
* debugging "why was the catalog slow" has nothing to look at.
|
|
689
|
+
*/
|
|
690
|
+
logger;
|
|
641
691
|
constructor(options = {}) {
|
|
642
692
|
this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
643
693
|
this.clientVersion = options.clientVersion ?? "2.0.4";
|
|
694
|
+
this.catalogRetryBackoffMs = options.catalogRetryBackoffMs ?? CATALOG_RETRY_BACKOFF_MS;
|
|
644
695
|
}
|
|
645
696
|
/**
|
|
646
697
|
* Normalize an OpenAI chat-completions body for the WorkBuddy upstream:
|
|
@@ -797,6 +848,30 @@ var WorkBuddyUpstreamClient = class {
|
|
|
797
848
|
* below is common to the two branches.
|
|
798
849
|
*/
|
|
799
850
|
async fetchModels(credential, signal) {
|
|
851
|
+
const attempts = this.catalogRetryBackoffMs.length + 1;
|
|
852
|
+
let lastError;
|
|
853
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) try {
|
|
854
|
+
return await this.fetchModelsOnce(credential, signal);
|
|
855
|
+
} catch (error) {
|
|
856
|
+
lastError = error;
|
|
857
|
+
if (isAbortError(error)) throw error;
|
|
858
|
+
const wait = this.catalogRetryBackoffMs[attempt];
|
|
859
|
+
if (wait === void 0) break;
|
|
860
|
+
this.logger?.warn?.(`dsh-workbuddy-xdpool: model catalog fetch failed (attempt ${attempt + 1}/${attempts}), retrying in ${wait}ms`, error);
|
|
861
|
+
await delay(wait);
|
|
862
|
+
}
|
|
863
|
+
throw lastError;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* One catalog attempt, without retries.
|
|
867
|
+
*
|
|
868
|
+
* Split out so {@link fetchModels} can retry it: the failure this guards
|
|
869
|
+
* against is a startup network hiccup, where several independent components
|
|
870
|
+
* see `fetch failed` inside the same second and the very next request
|
|
871
|
+
* succeeds — exactly the case a single attempt turns into "the user's model
|
|
872
|
+
* list is missing half its entries for the rest of the session".
|
|
873
|
+
*/
|
|
874
|
+
async fetchModelsOnce(credential, signal) {
|
|
800
875
|
const global = regionOf(credential.domain) === "global";
|
|
801
876
|
const url = global ? `${globalBase(credential)}${GLOBAL_CONFIG_PATH}` : `${chatBase(credential)}${MODELS_CATALOG_PATH}`;
|
|
802
877
|
const headers = global ? {
|
|
@@ -2879,38 +2954,97 @@ ${new Date(until).toISOString()}
|
|
|
2879
2954
|
* Static fallback used before the first live catalog fetch, and whenever the
|
|
2880
2955
|
* upstream cannot be reached.
|
|
2881
2956
|
*
|
|
2882
|
-
*
|
|
2883
|
-
*
|
|
2884
|
-
*
|
|
2885
|
-
*
|
|
2886
|
-
*
|
|
2887
|
-
*
|
|
2957
|
+
* This table is the DOMESTIC (CN) roster, and it is kept in step with what
|
|
2958
|
+
* `copilot.tencent.com` actually advertises. It matters more than a "just in
|
|
2959
|
+
* case" list usually would: a failed startup fetch falls back to it, and the
|
|
2960
|
+
* user sees that as "half my models were deleted" — `deepseek-v4.1-flash`
|
|
2961
|
+
* and friends simply vanishing from the picker, with no visible explanation.
|
|
2962
|
+
*
|
|
2963
|
+
* Every row below was captured from the live endpoint, INCLUDING the window
|
|
2964
|
+
* sizes: the previous version carried a 32K window for `hy3` when the gateway
|
|
2965
|
+
* says 192K, and listed `kimi-k3` under an id the gateway no longer uses
|
|
2966
|
+
* (`kimi-k3-1`). A stale fallback is worse than a short one — it looks
|
|
2967
|
+
* authoritative while being wrong.
|
|
2888
2968
|
*
|
|
2889
2969
|
* `multiplier: 0` is the gateways' own spelling of "free" (`credits: "x0.00"`),
|
|
2890
2970
|
* which is what turns on the free badge.
|
|
2891
2971
|
*/
|
|
2892
2972
|
const FALLBACK_WORKBUDDY_MODELS = [
|
|
2973
|
+
{
|
|
2974
|
+
id: "auto",
|
|
2975
|
+
name: "Auto",
|
|
2976
|
+
contextWindow: 256e3,
|
|
2977
|
+
maxOutputTokens: 32e3,
|
|
2978
|
+
supportsImages: true
|
|
2979
|
+
},
|
|
2980
|
+
{
|
|
2981
|
+
id: "hy4-preview",
|
|
2982
|
+
name: "Hy4 preview",
|
|
2983
|
+
contextWindow: 1e6,
|
|
2984
|
+
maxOutputTokens: 64e3,
|
|
2985
|
+
supportsImages: true,
|
|
2986
|
+
multiplier: .29
|
|
2987
|
+
},
|
|
2988
|
+
{
|
|
2989
|
+
id: "hy3",
|
|
2990
|
+
name: "Hy3",
|
|
2991
|
+
contextWindow: 192e3,
|
|
2992
|
+
maxOutputTokens: 64e3,
|
|
2993
|
+
supportsImages: true,
|
|
2994
|
+
multiplier: 0
|
|
2995
|
+
},
|
|
2996
|
+
{
|
|
2997
|
+
id: "hy3-x",
|
|
2998
|
+
name: "Hy3",
|
|
2999
|
+
contextWindow: 192e3,
|
|
3000
|
+
maxOutputTokens: 64e3,
|
|
3001
|
+
supportsImages: true,
|
|
3002
|
+
multiplier: .05
|
|
3003
|
+
},
|
|
3004
|
+
{
|
|
3005
|
+
id: "deepseek-v4.1-flash",
|
|
3006
|
+
name: "Deepseek-V4.1-Flash",
|
|
3007
|
+
contextWindow: 1e6,
|
|
3008
|
+
maxOutputTokens: 128e3,
|
|
3009
|
+
supportsImages: true,
|
|
3010
|
+
multiplier: .11
|
|
3011
|
+
},
|
|
3012
|
+
{
|
|
3013
|
+
id: "deepseek-v4-pro",
|
|
3014
|
+
name: "Deepseek-V4-Pro",
|
|
3015
|
+
contextWindow: 1e6,
|
|
3016
|
+
maxOutputTokens: 128e3,
|
|
3017
|
+
supportsImages: true,
|
|
3018
|
+
multiplier: .51
|
|
3019
|
+
},
|
|
3020
|
+
{
|
|
3021
|
+
id: "deepseek-v4-flash",
|
|
3022
|
+
name: "DeepSeek-V4-Flash",
|
|
3023
|
+
contextWindow: 1e6,
|
|
3024
|
+
maxOutputTokens: 128e3,
|
|
3025
|
+
supportsImages: true
|
|
3026
|
+
},
|
|
2893
3027
|
{
|
|
2894
3028
|
id: "glm-5.3",
|
|
2895
3029
|
name: "GLM-5.3",
|
|
2896
|
-
contextWindow:
|
|
2897
|
-
maxOutputTokens:
|
|
3030
|
+
contextWindow: 1e6,
|
|
3031
|
+
maxOutputTokens: 64e3,
|
|
2898
3032
|
supportsImages: true,
|
|
2899
3033
|
multiplier: .79
|
|
2900
3034
|
},
|
|
2901
3035
|
{
|
|
2902
3036
|
id: "glm-5.3-flash",
|
|
2903
3037
|
name: "GLM-5.3-Flash",
|
|
2904
|
-
contextWindow:
|
|
2905
|
-
maxOutputTokens:
|
|
3038
|
+
contextWindow: 1e6,
|
|
3039
|
+
maxOutputTokens: 131072,
|
|
2906
3040
|
supportsImages: true,
|
|
2907
3041
|
multiplier: .06
|
|
2908
3042
|
},
|
|
2909
3043
|
{
|
|
2910
3044
|
id: "glm-5.2",
|
|
2911
3045
|
name: "GLM-5.2",
|
|
2912
|
-
contextWindow:
|
|
2913
|
-
maxOutputTokens:
|
|
3046
|
+
contextWindow: 1e6,
|
|
3047
|
+
maxOutputTokens: 64e3,
|
|
2914
3048
|
supportsImages: true,
|
|
2915
3049
|
multiplier: .79
|
|
2916
3050
|
},
|
|
@@ -2918,53 +3052,57 @@ const FALLBACK_WORKBUDDY_MODELS = [
|
|
|
2918
3052
|
id: "glm-5.1",
|
|
2919
3053
|
name: "GLM-5.1",
|
|
2920
3054
|
contextWindow: 2e5,
|
|
2921
|
-
maxOutputTokens:
|
|
2922
|
-
supportsImages: false
|
|
3055
|
+
maxOutputTokens: 48e3,
|
|
3056
|
+
supportsImages: false,
|
|
3057
|
+
multiplier: .79
|
|
2923
3058
|
},
|
|
2924
3059
|
{
|
|
2925
|
-
id: "
|
|
2926
|
-
name: "
|
|
3060
|
+
id: "glm-5v-turbo",
|
|
3061
|
+
name: "GLM-5v-Turbo",
|
|
2927
3062
|
contextWindow: 2e5,
|
|
2928
|
-
maxOutputTokens:
|
|
2929
|
-
supportsImages: true
|
|
2930
|
-
|
|
2931
|
-
{
|
|
2932
|
-
id: "deepseek-v4-flash",
|
|
2933
|
-
name: "DeepSeek-V4-Flash",
|
|
2934
|
-
contextWindow: 2e5,
|
|
2935
|
-
maxOutputTokens: 128e3,
|
|
2936
|
-
supportsImages: true
|
|
3063
|
+
maxOutputTokens: 64e3,
|
|
3064
|
+
supportsImages: true,
|
|
3065
|
+
multiplier: .71
|
|
2937
3066
|
},
|
|
2938
3067
|
{
|
|
2939
|
-
id: "kimi-k3",
|
|
3068
|
+
id: "kimi-k3-1",
|
|
2940
3069
|
name: "Kimi-K3",
|
|
2941
|
-
contextWindow:
|
|
2942
|
-
maxOutputTokens:
|
|
3070
|
+
contextWindow: 1e6,
|
|
3071
|
+
maxOutputTokens: 32e3,
|
|
2943
3072
|
supportsImages: true,
|
|
2944
3073
|
multiplier: 1.62
|
|
2945
3074
|
},
|
|
2946
3075
|
{
|
|
2947
|
-
id: "
|
|
2948
|
-
name: "
|
|
2949
|
-
contextWindow:
|
|
2950
|
-
maxOutputTokens:
|
|
2951
|
-
supportsImages: true
|
|
3076
|
+
id: "kimi-k2.8-preview",
|
|
3077
|
+
name: "Kimi-K2.8-Preview",
|
|
3078
|
+
contextWindow: 1e6,
|
|
3079
|
+
maxOutputTokens: 64e3,
|
|
3080
|
+
supportsImages: true,
|
|
3081
|
+
multiplier: .77
|
|
2952
3082
|
},
|
|
2953
3083
|
{
|
|
2954
|
-
id: "
|
|
2955
|
-
name: "
|
|
2956
|
-
contextWindow:
|
|
2957
|
-
maxOutputTokens:
|
|
3084
|
+
id: "kimi-k2.7",
|
|
3085
|
+
name: "Kimi-K2.7-Code",
|
|
3086
|
+
contextWindow: 256e3,
|
|
3087
|
+
maxOutputTokens: 32e3,
|
|
2958
3088
|
supportsImages: true,
|
|
2959
|
-
multiplier:
|
|
3089
|
+
multiplier: .57
|
|
2960
3090
|
},
|
|
2961
3091
|
{
|
|
2962
|
-
id: "
|
|
2963
|
-
name: "
|
|
2964
|
-
contextWindow:
|
|
2965
|
-
maxOutputTokens:
|
|
3092
|
+
id: "kimi-k2.6",
|
|
3093
|
+
name: "Kimi-K2.6",
|
|
3094
|
+
contextWindow: 256e3,
|
|
3095
|
+
maxOutputTokens: 32e3,
|
|
2966
3096
|
supportsImages: true,
|
|
2967
|
-
multiplier: .
|
|
3097
|
+
multiplier: .52
|
|
3098
|
+
},
|
|
3099
|
+
{
|
|
3100
|
+
id: "minimax-m3",
|
|
3101
|
+
name: "MiniMax-M3",
|
|
3102
|
+
contextWindow: 512e3,
|
|
3103
|
+
maxOutputTokens: 64e3,
|
|
3104
|
+
supportsImages: true,
|
|
3105
|
+
multiplier: .25
|
|
2968
3106
|
}
|
|
2969
3107
|
];
|
|
2970
3108
|
/** Live catalog with a static fallback behind it. */
|
|
@@ -2973,6 +3111,19 @@ var WorkBuddyCatalog = class {
|
|
|
2973
3111
|
listeners = /* @__PURE__ */ new Set();
|
|
2974
3112
|
/** User's model selection. Empty object = follow the catalog unfiltered. */
|
|
2975
3113
|
selection = {};
|
|
3114
|
+
/**
|
|
3115
|
+
* Whether `models` came from the gateway or from the static table.
|
|
3116
|
+
*
|
|
3117
|
+
* Tracked so the card can SAY which it is showing. A failed fetch used to be
|
|
3118
|
+
* invisible on screen: the picker simply held fewer models than before, which
|
|
3119
|
+
* the user reasonably read as "the plugin deleted my models" rather than "the
|
|
3120
|
+
* network hiccuped at startup".
|
|
3121
|
+
*/
|
|
3122
|
+
source = "fallback";
|
|
3123
|
+
/** When the live list last landed. */
|
|
3124
|
+
lastUpdatedAt;
|
|
3125
|
+
/** Why the last fetch failed, for display. */
|
|
3126
|
+
lastError;
|
|
2976
3127
|
current() {
|
|
2977
3128
|
return this.models;
|
|
2978
3129
|
}
|
|
@@ -3007,6 +3158,7 @@ var WorkBuddyCatalog = class {
|
|
|
3007
3158
|
/** Restore the static fallback, e.g. when the upstream stops answering. */
|
|
3008
3159
|
reset() {
|
|
3009
3160
|
this.models = FALLBACK_WORKBUDDY_MODELS;
|
|
3161
|
+
this.source = "fallback";
|
|
3010
3162
|
this.notify();
|
|
3011
3163
|
}
|
|
3012
3164
|
/** Replace the user's selection; the adapter rebuilds from `visible()`. */
|
|
@@ -3018,6 +3170,31 @@ var WorkBuddyCatalog = class {
|
|
|
3018
3170
|
currentSelection() {
|
|
3019
3171
|
return this.selection;
|
|
3020
3172
|
}
|
|
3173
|
+
/**
|
|
3174
|
+
* Whether this catalog is serving live data or the built-in table.
|
|
3175
|
+
*
|
|
3176
|
+
* `fallback` is not an error state, but it IS a degraded one: the user is
|
|
3177
|
+
* looking at a shorter roster than the gateway offers, so the card says so
|
|
3178
|
+
* and offers a retry instead of letting them wonder where the models went.
|
|
3179
|
+
*/
|
|
3180
|
+
currentSource() {
|
|
3181
|
+
return this.source;
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* When the live list last landed, if it ever did.
|
|
3185
|
+
*
|
|
3186
|
+
* Named `catalogUpdatedAt()` rather than `updatedAt()` because the class
|
|
3187
|
+
* already had an `updatedAt` member; two members of the same name is a
|
|
3188
|
+
* compile error, and the awkwardness is a useful signal that the concept is
|
|
3189
|
+
* "when THIS catalog was refreshed", not a generic timestamp.
|
|
3190
|
+
*/
|
|
3191
|
+
catalogUpdatedAt() {
|
|
3192
|
+
return this.lastUpdatedAt;
|
|
3193
|
+
}
|
|
3194
|
+
/** Why the last fetch failed, if it did. */
|
|
3195
|
+
lastFetchError() {
|
|
3196
|
+
return this.lastError;
|
|
3197
|
+
}
|
|
3021
3198
|
onChange(listener) {
|
|
3022
3199
|
this.listeners.add(listener);
|
|
3023
3200
|
return () => this.listeners.delete(listener);
|
|
@@ -3028,6 +3205,20 @@ var WorkBuddyCatalog = class {
|
|
|
3028
3205
|
/** Replace the catalog from the live upstream list; keeps the fallback if empty. */
|
|
3029
3206
|
updateFromUpstream(models) {
|
|
3030
3207
|
this.update(catalogFromUpstream(models));
|
|
3208
|
+
this.source = "live";
|
|
3209
|
+
this.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3210
|
+
this.lastError = void 0;
|
|
3211
|
+
}
|
|
3212
|
+
/**
|
|
3213
|
+
* Record that a fetch attempt failed, leaving the current list in place.
|
|
3214
|
+
*
|
|
3215
|
+
* The list is deliberately NOT reset here: a refresh that fails should keep
|
|
3216
|
+
* whatever working catalog is already loaded, rather than demoting a healthy
|
|
3217
|
+
* session to the static table because one retry ran out.
|
|
3218
|
+
*/
|
|
3219
|
+
noteFetchFailure(message) {
|
|
3220
|
+
this.lastError = message;
|
|
3221
|
+
this.notify();
|
|
3031
3222
|
}
|
|
3032
3223
|
notify() {
|
|
3033
3224
|
for (const listener of this.listeners) listener();
|
|
@@ -3798,7 +3989,7 @@ var WorkBuddyScheduler = class {
|
|
|
3798
3989
|
async runNow(kind, force = false) {
|
|
3799
3990
|
const today = dayKey(this.now());
|
|
3800
3991
|
const state = this.states[kind];
|
|
3801
|
-
if (!force && state.
|
|
3992
|
+
if (!force && state.lastFiredHour === slotKey(this.now())) return { ...state };
|
|
3802
3993
|
this.busy = true;
|
|
3803
3994
|
try {
|
|
3804
3995
|
await this.runJob(kind, today);
|
|
@@ -3896,13 +4087,46 @@ var WorkBuddyScheduler = class {
|
|
|
3896
4087
|
* two hours still runs twice a day — but a job whose hour passed while DSH was
|
|
3897
4088
|
* closed runs immediately on the next tick instead of waiting for tomorrow.
|
|
3898
4089
|
*/
|
|
3899
|
-
|
|
4090
|
+
/**
|
|
4091
|
+
* The configured slot `kind` should consume at `now`, or undefined when none.
|
|
4092
|
+
*
|
|
4093
|
+
* Returns the SLOT STRING (not a boolean) because the caller must record the
|
|
4094
|
+
* same value it acted on. Returning a boolean was the original bug's enabler:
|
|
4095
|
+
* the tick asked "is it due", then `runJob` independently wrote "what time is
|
|
4096
|
+
* it now" — and those two answers were almost never equal.
|
|
4097
|
+
*
|
|
4098
|
+
* CATCH-UP semantics are deliberate: a candidate fires once its hour has
|
|
4099
|
+
* PASSED and its slot is still unconsumed, so a laptop that slept through
|
|
4100
|
+
* 10:00 still runs the job when it wakes, on the same day.
|
|
4101
|
+
*
|
|
4102
|
+
* The EARLIEST unconsumed candidate wins, which is what keeps a two-hour job
|
|
4103
|
+
* (`travelHours: [9, 21]`) whole: consuming the earliest due slot leaves the
|
|
4104
|
+
* later one for its own hour.
|
|
4105
|
+
*/
|
|
4106
|
+
dueSlot(kind, now) {
|
|
3900
4107
|
const hours = this.hoursOf(kind);
|
|
3901
|
-
if (hours.length === 0) return
|
|
3902
|
-
const
|
|
3903
|
-
const current = Number(hour);
|
|
4108
|
+
if (hours.length === 0) return void 0;
|
|
4109
|
+
const current = Number(zonedParts(now).hour);
|
|
3904
4110
|
const today = dayKey(now);
|
|
3905
|
-
|
|
4111
|
+
const fired = this.firedSlotsOf(kind);
|
|
4112
|
+
for (const candidate of [...hours].sort((a, b) => a - b)) {
|
|
4113
|
+
if (candidate > current) break;
|
|
4114
|
+
const slot = `${today}T${String(candidate).padStart(2, "0")}`;
|
|
4115
|
+
if (!fired.has(slot)) return slot;
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
/** Today's consumed slots for one job, as a set. */
|
|
4119
|
+
firedSlotsOf(kind) {
|
|
4120
|
+
const stored = this.states[kind].firedSlots;
|
|
4121
|
+
if (stored === void 0) return /* @__PURE__ */ new Set();
|
|
4122
|
+
const today = dayKey(this.now());
|
|
4123
|
+
return new Set(stored.filter((slot) => slot.startsWith(`${today}T`)));
|
|
4124
|
+
}
|
|
4125
|
+
/** Record one consumed slot on a job's state. */
|
|
4126
|
+
consumeSlot(kind, slot) {
|
|
4127
|
+
const state = this.states[kind];
|
|
4128
|
+
const today = dayKey(this.now());
|
|
4129
|
+
state.firedSlots = [.../* @__PURE__ */ new Set([...this.firedSlotsOf(kind), slot])].filter((entry) => entry.startsWith(`${today}T`)).sort();
|
|
3906
4130
|
}
|
|
3907
4131
|
async tick() {
|
|
3908
4132
|
if (!this.enabled || this.stopped || this.busy) return;
|
|
@@ -3913,9 +4137,10 @@ var WorkBuddyScheduler = class {
|
|
|
3913
4137
|
const today = dayKey(now);
|
|
3914
4138
|
for (const kind of JOB_KINDS) {
|
|
3915
4139
|
if (this.stopped) return;
|
|
3916
|
-
if (this.states[kind].
|
|
3917
|
-
|
|
3918
|
-
|
|
4140
|
+
if (this.states[kind].lastFiredHour === slot) continue;
|
|
4141
|
+
const consumed = this.dueSlot(kind, now);
|
|
4142
|
+
if (consumed === void 0) continue;
|
|
4143
|
+
await this.runJob(kind, today, consumed);
|
|
3919
4144
|
}
|
|
3920
4145
|
} catch (error) {
|
|
3921
4146
|
this.logger.warn?.("dsh-workbuddy-xdpool automation tick failed:", error);
|
|
@@ -4001,6 +4226,12 @@ var WorkBuddyScheduler = class {
|
|
|
4001
4226
|
/**
|
|
4002
4227
|
* Run one job against every eligible account and record the outcome.
|
|
4003
4228
|
*
|
|
4229
|
+
* `consumedSlot` is the configured slot this run spends (`dueSlot`'s answer).
|
|
4230
|
+
* It is passed IN rather than recomputed here so the value recorded is exactly
|
|
4231
|
+
* the value the schedule decided on — the defect this replaces wrote
|
|
4232
|
+
* `slotKey(now)` instead, i.e. the clock time the run finished at, which
|
|
4233
|
+
* almost never equals the configured candidate the gate had compared against.
|
|
4234
|
+
*
|
|
4004
4235
|
* The task job runs in TWO passes. The first sends the event chains that light
|
|
4005
4236
|
* up client-scored tasks; the second collects rewards. They are separate
|
|
4006
4237
|
* because scoring lands asynchronously — a chain sent and claimed within the
|
|
@@ -4008,7 +4239,7 @@ var WorkBuddyScheduler = class {
|
|
|
4008
4239
|
* while claiming wants the whole pool to have been lit up first. Splitting
|
|
4009
4240
|
* them costs one shared wait instead of one wait per account.
|
|
4010
4241
|
*/
|
|
4011
|
-
async runJob(kind, today) {
|
|
4242
|
+
async runJob(kind, today, consumedSlot) {
|
|
4012
4243
|
const accountWord = kind === "report" ? "report" : kind;
|
|
4013
4244
|
let ok = 0;
|
|
4014
4245
|
let failed = 0;
|
|
@@ -4073,7 +4304,8 @@ var WorkBuddyScheduler = class {
|
|
|
4073
4304
|
if (index < accounts.length - 1 && this.delayMs > 0 && !this.stopped) await sleep(this.delayMs);
|
|
4074
4305
|
}
|
|
4075
4306
|
const state = this.states[kind];
|
|
4076
|
-
|
|
4307
|
+
if (consumedSlot !== void 0) this.consumeSlot(kind, consumedSlot);
|
|
4308
|
+
state.lastFiredHour = slotKey(this.now());
|
|
4077
4309
|
state.lastRunDate = today;
|
|
4078
4310
|
state.lastRunAtMs = this.now().getTime();
|
|
4079
4311
|
state.ok = ok;
|
package/lib/client.js
CHANGED
|
@@ -23,6 +23,16 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
const POOL_STATUS_PATH = "/plugins/dsh-workbuddy-xdpool/status";
|
|
24
24
|
/** Plugin-owned local account rescan endpoint (re-read desktop snapshots). */
|
|
25
25
|
const POOL_RESCAN_PATH = "/plugins/dsh-workbuddy-xdpool/accounts/rescan";
|
|
26
|
+
/**
|
|
27
|
+
* Re-fetch the upstream model catalog for both regions.
|
|
28
|
+
*
|
|
29
|
+
* Separate from {@link POOL_RESCAN_PATH} because they answer different
|
|
30
|
+
* questions, and conflating them misled users: "detect accounts again" only
|
|
31
|
+
* re-read the desktop snapshots, so it could NOT recover a model list that had
|
|
32
|
+
* fallen back to the static table after a failed startup fetch. The only way
|
|
33
|
+
* out was restarting DSH.
|
|
34
|
+
*/
|
|
35
|
+
const POOL_CATALOG_REFRESH_PATH = "/plugins/dsh-workbuddy-xdpool/models/refresh";
|
|
26
36
|
/** Plugin-owned cooldown reset endpoint (clear all 429 cooldowns). */
|
|
27
37
|
const POOL_RESET_COOLDOWN_PATH = "/plugins/dsh-workbuddy-xdpool/cooldowns/reset";
|
|
28
38
|
/** Plugin-owned daily check-in action endpoint (claim today's reward). */
|
|
@@ -276,6 +286,9 @@ window.__ModuleLoader__.load({
|
|
|
276
286
|
.dsm-workbuddy-xdpool-model-budget input{margin:0;accent-color:var(--dsw-alias-brand-primary,#5686fe)}
|
|
277
287
|
.dsm-workbuddy-xdpool-models-heading{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
278
288
|
.dsm-workbuddy-xdpool-models-actions{display:flex;align-items:center;gap:8px;flex:none}
|
|
289
|
+
/* "Built-in list (offline)" chip: a warning tint, since the user is looking at
|
|
290
|
+
a SHORTER roster than the gateway offers and may wonder where models went. */
|
|
291
|
+
.dsm-workbuddy-xdpool-catalog-offline{flex:none;padding:2px 9px;border-radius:999px;font-size:11px;line-height:17px;font-weight:600;background:var(--dsw-alias-state-warning-subtle,rgba(217,119,6,.15));color:var(--dsw-alias-state-warning-primary,#d97706);white-space:nowrap}
|
|
279
292
|
|
|
280
293
|
|
|
281
294
|
/* Automation: run-now button and the per-job progress line. */
|
|
@@ -585,6 +598,8 @@ window.__ModuleLoader__.load({
|
|
|
585
598
|
const [error, setError] = (0, react.useState)(void 0);
|
|
586
599
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
587
600
|
const [cooldownBusy, setCooldownBusy] = (0, react.useState)(false);
|
|
601
|
+
/** Model-catalog refresh in flight (separate from the account rescan). */
|
|
602
|
+
const [catalogBusy, setCatalogBusy] = (0, react.useState)(false);
|
|
588
603
|
const [automationBusy, setAutomationBusy] = (0, react.useState)(false);
|
|
589
604
|
/** The automation job currently running from the card, if any. */
|
|
590
605
|
const [automationRun, setAutomationRun] = (0, react.useState)(void 0);
|
|
@@ -679,6 +694,35 @@ window.__ModuleLoader__.load({
|
|
|
679
694
|
if (mounted.current) setBusy(false);
|
|
680
695
|
}
|
|
681
696
|
};
|
|
697
|
+
/**
|
|
698
|
+
* Re-fetch the upstream model catalog for both regions.
|
|
699
|
+
*
|
|
700
|
+
* Its own action, and its own BUTTON, because the account rescan beside it
|
|
701
|
+
* could not do this job: when the startup fetch failed, the picker held the
|
|
702
|
+
* shorter built-in list and "detect accounts again" left it untouched, so the
|
|
703
|
+
* only recovery was restarting DSH. Users reasonably assumed the button
|
|
704
|
+
* covered both.
|
|
705
|
+
*/
|
|
706
|
+
const refreshCatalog = async () => {
|
|
707
|
+
setCatalogBusy(true);
|
|
708
|
+
setFlash(void 0);
|
|
709
|
+
try {
|
|
710
|
+
const response = await fetch(POOL_CATALOG_REFRESH_PATH, {
|
|
711
|
+
method: "POST",
|
|
712
|
+
headers: { accept: "application/json" },
|
|
713
|
+
credentials: "same-origin"
|
|
714
|
+
});
|
|
715
|
+
const body = await response.json();
|
|
716
|
+
if (!response.ok) throw new Error(body.error ?? `HTTP ${response.status}`);
|
|
717
|
+
await refresh(activeRegion);
|
|
718
|
+
const here = body.regions?.[activeRegion];
|
|
719
|
+
if (mounted.current) setFlash(here?.source === "live" ? t?.("row.catalogRefreshed", { count: here.models ?? 0 }) ?? "" : t?.("row.catalogRefreshOffline") ?? "");
|
|
720
|
+
} catch (cause) {
|
|
721
|
+
if (mounted.current) setError(cause instanceof Error ? cause.message : String(cause));
|
|
722
|
+
} finally {
|
|
723
|
+
if (mounted.current) setCatalogBusy(false);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
682
726
|
const resetCooldowns = async () => {
|
|
683
727
|
setCooldownBusy(true);
|
|
684
728
|
setFlash(void 0);
|
|
@@ -1270,7 +1314,7 @@ window.__ModuleLoader__.load({
|
|
|
1270
1314
|
}),
|
|
1271
1315
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1272
1316
|
className: "dsm-workbuddy-xdpool-auto-job-last",
|
|
1273
|
-
children: job?.
|
|
1317
|
+
children: job?.lastRunAtMs === void 0 ? t?.("row.autoNever") ?? "not run yet" : `${formatTime(job.lastRunAtMs)} · ${job.ok}${job.failed > 0 ? `/${job.failed}` : ""}`
|
|
1274
1318
|
}),
|
|
1275
1319
|
job?.progress === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1276
1320
|
className: "dsm-workbuddy-xdpool-auto-job-note",
|
|
@@ -1430,21 +1474,39 @@ window.__ModuleLoader__.load({
|
|
|
1430
1474
|
})]
|
|
1431
1475
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1432
1476
|
className: "dsm-workbuddy-xdpool-models-actions",
|
|
1433
|
-
children: [
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1477
|
+
children: [
|
|
1478
|
+
status?.catalogSource !== "fallback" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1479
|
+
className: "dsm-workbuddy-xdpool-catalog-offline",
|
|
1480
|
+
title: status.catalogError ?? void 0,
|
|
1481
|
+
children: t?.("row.catalogOffline") ?? "built-in list (offline)"
|
|
1482
|
+
}),
|
|
1483
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1484
|
+
type: "button",
|
|
1485
|
+
className: "dsm-btn dsm-btn-outline",
|
|
1486
|
+
disabled: catalogBusy,
|
|
1487
|
+
onClick: () => {
|
|
1488
|
+
refreshCatalog();
|
|
1489
|
+
},
|
|
1490
|
+
title: t?.("row.catalogRefreshHint") ?? "Fetch the model list again from WorkBuddy",
|
|
1491
|
+
children: catalogBusy ? t?.("row.catalogRefreshing") ?? "Fetching…" : t?.("row.catalogRefresh") ?? "Refresh models"
|
|
1492
|
+
}),
|
|
1493
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1494
|
+
type: "button",
|
|
1495
|
+
className: "dsm-btn dsm-btn-outline",
|
|
1496
|
+
disabled: !modelsDirty || savingModels,
|
|
1497
|
+
onClick: discardModels,
|
|
1498
|
+
children: t?.("row.modelsDiscard") ?? "Discard"
|
|
1499
|
+
}),
|
|
1500
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1501
|
+
type: "button",
|
|
1502
|
+
className: "dsm-btn dsm-btn-primary",
|
|
1503
|
+
disabled: !modelsDirty || savingModels || enabledCount === 0,
|
|
1504
|
+
onClick: () => {
|
|
1505
|
+
saveModels();
|
|
1506
|
+
},
|
|
1507
|
+
children: savingModels ? t?.("row.modelsSaving") ?? "Saving…" : t?.("row.modelsSave") ?? "Save"
|
|
1508
|
+
})
|
|
1509
|
+
]
|
|
1448
1510
|
})]
|
|
1449
1511
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1450
1512
|
className: "dsm-workbuddy-xdpool-model-list",
|
|
@@ -2023,6 +2085,12 @@ window.__ModuleLoader__.load({
|
|
|
2023
2085
|
"row.modelsSaved": "Model selection saved",
|
|
2024
2086
|
"row.modelsSaveError": "Could not save: {message}",
|
|
2025
2087
|
"row.modelsEmpty": "No model enabled — enable at least one before saving.",
|
|
2088
|
+
"row.catalogOffline": "built-in list (offline)",
|
|
2089
|
+
"row.catalogRefresh": "Refresh models",
|
|
2090
|
+
"row.catalogRefreshing": "Fetching…",
|
|
2091
|
+
"row.catalogRefreshHint": "Fetch the model list again from WorkBuddy",
|
|
2092
|
+
"row.catalogRefreshed": "Model list updated ({count} models)",
|
|
2093
|
+
"row.catalogRefreshOffline": "Still offline — kept the built-in list",
|
|
2026
2094
|
"row.free": "free",
|
|
2027
2095
|
"row.limitedFree": "limited free",
|
|
2028
2096
|
"row.nightDiscount": "night",
|
|
@@ -2162,6 +2230,12 @@ window.__ModuleLoader__.load({
|
|
|
2162
2230
|
"row.modelsSaved": "模型选择已保存",
|
|
2163
2231
|
"row.modelsSaveError": "保存失败:{message}",
|
|
2164
2232
|
"row.modelsEmpty": "至少要启用一个模型才能保存。",
|
|
2233
|
+
"row.catalogOffline": "内置列表(离线)",
|
|
2234
|
+
"row.catalogRefresh": "重新获取模型",
|
|
2235
|
+
"row.catalogRefreshing": "获取中…",
|
|
2236
|
+
"row.catalogRefreshHint": "重新从 WorkBuddy 拉取模型列表",
|
|
2237
|
+
"row.catalogRefreshed": "模型列表已更新({count} 个模型)",
|
|
2238
|
+
"row.catalogRefreshOffline": "仍然离线,继续使用内置列表",
|
|
2165
2239
|
"row.free": "免费",
|
|
2166
2240
|
"row.limitedFree": "限量免费",
|
|
2167
2241
|
"row.nightDiscount": "夜间",
|