dsh-workbuddy-xdpool 1.7.2 → 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 +37 -0
- package/lib/bin.js +234 -43
- package/lib/client.js +89 -15
- package/lib/index.d.ts +1033 -926
- package/lib/index.js +332 -58
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@
|
|
|
4
4
|
|
|
5
5
|
版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
6
6
|
|
|
7
|
+
## 1.7.3 — 模型不会因为一次网络抖动就消失
|
|
8
|
+
|
|
9
|
+
> 开机那一秒网络还没起来,插件就把模型列表「降级」了,然后一整天都不再试第二次。
|
|
10
|
+
|
|
11
|
+
**你会看到**:某次重启后,国内版的模型从 16 个变成 10 个,`deepseek-v4.1-flash` 之类凭空消失(国际版没事)。更麻烦的是你之前勾选过那个模型,于是**依赖这个 provider 的东西开始报错**(`has no configured model "deepseek-v4.1-flash"`),会话被悄悄切到国际版的同名模型上,而你根本不知道为什么。
|
|
12
|
+
|
|
13
|
+
界面**没有任何提示** —— 只有日志里一条不起眼的 `[W]`。点卡片上的「重新检测账号」?**没用**。唯一的恢复方式是重启 DSH。
|
|
14
|
+
|
|
15
|
+
**为什么**:启动时去拉模型列表,那一次**失败了**(日志显示同一秒里有好几个独立组件都报 `fetch failed`,而 0.7 秒后国际版就成功了 —— 典型的开机网络还没就绪)。失败之后:
|
|
16
|
+
|
|
17
|
+
1. **不重试**,一次就放弃;
|
|
18
|
+
2. **静默回退**到插件内置的那张静态表 —— 那张表里没有 `deepseek-v4.1-flash`,于是「拉取失败」在你眼里就是「模型被删了」;
|
|
19
|
+
3. 「重新检测账号」这个按钮**只重新扫描账号**,压根不碰模型列表。名字起得太容易让人误会,你会以为它能修好,实际什么都不做。
|
|
20
|
+
|
|
21
|
+
**怎么修的**:
|
|
22
|
+
|
|
23
|
+
- **拉取失败自动重试**(1 秒 / 3 秒 / 8 秒,最多 3 次)。开机抖动基本一次就能救回来。重试会写日志并标明第几次,所以「列表为什么慢」是可查的。**一个 region 的重试不影响另一个** —— 两个网关是独立主机,没理由同归于尽;
|
|
24
|
+
- **新增「重新获取模型」按钮**,并把它和「重新检测账号」分清楚:前者只管模型,后者只管账号,而且**后者现在也会顺带刷新模型**(因为大家都会去点它);
|
|
25
|
+
- **卡片会明说**:当前列表是「内置列表(离线)」时,模型区标题旁挂一个橙色标记,告诉你在看的是兜底表而不是完整清单,旁边就是重试按钮;
|
|
26
|
+
- **兜底表按真实上游数据重建**。这是顺手挖出来的一个更严重的问题:旧表**几乎全部过时** —— `hy3` 的窗口写着 32K(实际 192K)、`kimi-k3` 这个 id 上游早就不用了(实际是 `kimi-k3-1`),而 `deepseek-v4.1-flash`、`kimi-k2.6/2.7/2.8`、`glm-5v-turbo`、`hy3-x` 全都不在表里。我是**直接拉了国内版的真实接口**逐条核对后重写的,包含窗口大小和倍率。过时的兜底表比短表更糟 —— 它看起来权威,实际是错的。
|
|
27
|
+
|
|
28
|
+
### 一个差点修反的地方(顺手修了,免得白干)
|
|
29
|
+
|
|
30
|
+
改完重试我差点以为完事,结果发现:「刷新失败」会把一个**本来正常的**列表也打回兜底状态。也就是说,如果当前用的是实时列表、只是这次刷新没成功,卡片反而会显示「离线」。这是**修反了** —— 所以现在**刷新失败保留现有列表**,只有真的没有实时数据时才标记为兜底。
|
|
31
|
+
|
|
32
|
+
### 明确没做的
|
|
33
|
+
|
|
34
|
+
- **不做定时自动重拉**。开机失败已有重试兜底,而正常运行时定期轮询模型接口只会增加无谓的上游请求;真要更新,点按钮即可。
|
|
35
|
+
- **不做「缓存上次成功的列表到磁盘」**。这能彻底消灭回退问题,但会把模型清单持久化,涉及与 settings 文档的版本兼容;本轮先修「抖动即降级」这个主因,不夹带更大的改动。
|
|
36
|
+
|
|
37
|
+
### 验证
|
|
38
|
+
|
|
39
|
+
- 类型检查全绿,测试 **274 项全绿**(原 262 + 新增 13,另删掉 1 项一次性探针)。
|
|
40
|
+
- 新增 `tests/catalog-resilience.test.ts` 覆盖:首次失败第二次成功的恢复、用尽重试后抛错、**被调用方主动取消的请求不重试**(否则会在被要求停止后继续干活)、重试写日志、兜底表含关键模型且无重复 id、窗口值均有效。
|
|
41
|
+
- **反向验证**:把重试去掉 → 3 项变红;把「刷新失败降级」的错误改法放回去 → 1 项变红。确认测试真的能抓住这两个缺陷。
|
|
42
|
+
- 拉取国内版**真实接口**核对兜底表:共 16 个模型,已全部按实测数据录入。
|
|
43
|
+
|
|
7
44
|
## 1.7.2 — 自动化不再「一跑跑一天」
|
|
8
45
|
|
|
9
46
|
> 定好几点跑,就几点跑。之前是定好几点跑,然后从那一刻起每个整点都跑,跑到你睡觉。
|
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
|
|
2923
|
-
|
|
2924
|
-
{
|
|
2925
|
-
id: "deepseek-v4-pro",
|
|
2926
|
-
name: "DeepSeek-V4-Pro",
|
|
2927
|
-
contextWindow: 2e5,
|
|
2928
|
-
maxOutputTokens: 128e3,
|
|
2929
|
-
supportsImages: true
|
|
3055
|
+
maxOutputTokens: 48e3,
|
|
3056
|
+
supportsImages: false,
|
|
3057
|
+
multiplier: .79
|
|
2930
3058
|
},
|
|
2931
3059
|
{
|
|
2932
|
-
id: "
|
|
2933
|
-
name: "
|
|
3060
|
+
id: "glm-5v-turbo",
|
|
3061
|
+
name: "GLM-5v-Turbo",
|
|
2934
3062
|
contextWindow: 2e5,
|
|
2935
|
-
maxOutputTokens:
|
|
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();
|
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);
|
|
@@ -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": "夜间",
|