dsh-connect-workbuddy 4.13.1 → 4.14.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 +84 -0
- package/README.en.md +2 -1
- package/README.md +2 -1
- package/lib/bin.js +1 -1
- package/lib/client.js +336 -16
- package/lib/{host-heartbeat-CwdR6teZ.js → host-heartbeat-B1I95fKS.js} +413 -53
- package/lib/index.d.ts +258 -26
- package/lib/index.js +33 -20
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -574,6 +574,179 @@ declare class WorkBuddyCredentialStore {
|
|
|
574
574
|
}>;
|
|
575
575
|
}
|
|
576
576
|
//#endregion
|
|
577
|
+
//#region src/promo.d.ts
|
|
578
|
+
/**
|
|
579
|
+
* WorkBuddy promotion & tier badges — the upstream's own "this model is on
|
|
580
|
+
* offer" annotation, parsed and resolved against the wall clock.
|
|
581
|
+
*
|
|
582
|
+
* WHY A SEPARATE MODULE, AND WHY IT COSTS NO REQUEST. The upstream ships the
|
|
583
|
+
* badges inside the SAME `/v3/config` document that already carries the model
|
|
584
|
+
* roster: `data.modelPromotions` and `data.modelTiers` sit beside
|
|
585
|
+
* `data.models` (observed on the global feed, 2026-10-21). The plugin already
|
|
586
|
+
* reads that document for the roster, so drawing a promotion badge adds ZERO
|
|
587
|
+
* upstream traffic — it only adds a parser, because the document is not a
|
|
588
|
+
* clean list of promotions: the same `modelPromotions` array also carries
|
|
589
|
+
* NON-promotion entries (`hy-image-3.5`, `kling-video-v3` — entries with
|
|
590
|
+
* `tags` / `videoGeneration` and no `modelIds` and no `badge`). Those must be
|
|
591
|
+
* dropped, not rendered.
|
|
592
|
+
*
|
|
593
|
+
* WHY THE RESOLUTION LIVES HERE AND NOT IN THE FETCHER. A badge is a function
|
|
594
|
+
* of the clock, not of the fetch: the upstream's windows are wall-clock
|
|
595
|
+
* windows in a named zone (`schedule.daily`, e.g. `23:00`–`7:50` in
|
|
596
|
+
* `Asia/Shanghai`) and absolute windows (`schedule.validFrom` /
|
|
597
|
+
* `schedule.validUntil`). Picking the badge once, at parse time, FREEZES it —
|
|
598
|
+
* a badge read at 22:59 would still claim "night discount" at 08:00 the next
|
|
599
|
+
* morning. So the parser keeps EVERY candidate and the RENDERER asks for the
|
|
600
|
+
* one that holds at the moment it draws. The card re-renders at least once a
|
|
601
|
+
* minute while it is open (it polls the usage route), so a window edge is
|
|
602
|
+
* picked up within a minute without a second timer.
|
|
603
|
+
*
|
|
604
|
+
* THE SHAPES THIS PARSER ACCEPTS (all observed on the global feed's
|
|
605
|
+
* `/v3/config`, 2026-10-21 — the CN feed publishes the same two arrays, with
|
|
606
|
+
* Chinese labels and the extra fields listed at the end of this header):
|
|
607
|
+
*
|
|
608
|
+
* {
|
|
609
|
+
* id: 'hy3-free-trial-202608',
|
|
610
|
+
* kind: 'discount',
|
|
611
|
+
* enabled: true,
|
|
612
|
+
* priority: 200,
|
|
613
|
+
* modelIds: ['hy3'],
|
|
614
|
+
* badge: { label: 'Free now', color: '#FF0000', display: 'activeOnly' },
|
|
615
|
+
* discount: { discountedCredits: '0x', displayMode: 'replace', factor: 0 },
|
|
616
|
+
* hover: { textZh: 'Aug 6 – Oct 31: Free daily use with unlimited credits.' },
|
|
617
|
+
* schedule: {
|
|
618
|
+
* timezone: 'Asia/Shanghai',
|
|
619
|
+
* validFrom: '2026-07-06T00:00:00+08:00',
|
|
620
|
+
* validUntil: '2026-11-01T00:00:00+08:00',
|
|
621
|
+
* },
|
|
622
|
+
* }
|
|
623
|
+
*
|
|
624
|
+
* and the tier shape (`data.modelTiers`), which has the same `badge` /
|
|
625
|
+
* `modelIds` / `priority` core, no `schedule`, and a `requiredUserType`:
|
|
626
|
+
*
|
|
627
|
+
* {
|
|
628
|
+
* id: 'gpt-pro-only',
|
|
629
|
+
* badge: { label: 'Pro Only', labelEn: 'Pro Only', color: '#009273' },
|
|
630
|
+
* hover: { textEn: 'Exclusive to Pro subscribers.', textZh: '模型仅面向 Pro 用户开放。' },
|
|
631
|
+
* modelIds: ['gpt-6-astra', …],
|
|
632
|
+
* priority: 200,
|
|
633
|
+
* requiredUserType: 'standard',
|
|
634
|
+
* tier: 'standard',
|
|
635
|
+
* }
|
|
636
|
+
*
|
|
637
|
+
* THE CN FEED IS THE SAME FEED, READ AS THE APP READS IT. The CN gateway
|
|
638
|
+
* publishes `modelPromotions` (8 entries, measured 2026-10-21) and
|
|
639
|
+
* `modelTiers` (1 entry) in the SAME `/v3/config` document — with Chinese
|
|
640
|
+
* labels (`夜间折扣` / `限时免费` / `夜间免费` / `独家限免` / `限时折扣` /
|
|
641
|
+
* `订阅优先`), a `schedule.daily` night window, a base64 `badge.icon` and an
|
|
642
|
+
* optional `badge.display: 'activeOnly'`. Those richer shapes are exactly what
|
|
643
|
+
* this parser accepts below.
|
|
644
|
+
*
|
|
645
|
+
* They are visible only to the APP's product identity: the CLI channel's
|
|
646
|
+
* answer to the same URL publishes neither array, because it is a strict
|
|
647
|
+
* subset of the app channel's document. The plugin therefore reads the
|
|
648
|
+
* product configuration as the app does (`PRODUCT_UA` in `src/upstream.ts`) —
|
|
649
|
+
* which is why a CN account sees the same badges here as in the WorkBuddy
|
|
650
|
+
* app, without a second upstream request.
|
|
651
|
+
*
|
|
652
|
+
* @module dsh-connect-workbuddy/promo
|
|
653
|
+
*/
|
|
654
|
+
/** How the upstream wants the badge to appear. */
|
|
655
|
+
interface WorkBuddyPromoBadge {
|
|
656
|
+
/**
|
|
657
|
+
* The badge's own wording, region-localized ("限时免费" on the CN feed,
|
|
658
|
+
* "Free now" on the global one).
|
|
659
|
+
*/
|
|
660
|
+
label: string;
|
|
661
|
+
/** The English wording, when the upstream publishes one. */
|
|
662
|
+
labelEn?: string;
|
|
663
|
+
/** A compact wording, when the upstream publishes one. */
|
|
664
|
+
shortLabel?: string;
|
|
665
|
+
/** The English compact wording, when the upstream publishes one. */
|
|
666
|
+
shortLabelEn?: string;
|
|
667
|
+
/**
|
|
668
|
+
* The badge's colour, as a hex string ("#009273").
|
|
669
|
+
*
|
|
670
|
+
* The upstream publishes it for most promotions and omits it for others
|
|
671
|
+
* (the CN tier entry has none), so the renderer treats it as optional.
|
|
672
|
+
*/
|
|
673
|
+
color?: string;
|
|
674
|
+
/**
|
|
675
|
+
* `'activeOnly'` — the badge belongs to the promotion's RUNNING period and
|
|
676
|
+
* must disappear when the promotion is over.
|
|
677
|
+
*
|
|
678
|
+
* Absent — the badge is STANDING: it describes the model for as long as the
|
|
679
|
+
* promotion itself lasts, while the promotion's daily window governs only
|
|
680
|
+
* the discount. The upstream uses this for its night-discount pairs, where
|
|
681
|
+
* one entry carries the badge plus the night hours and a second, separate
|
|
682
|
+
* entry carries the same badge plus the daytime hours.
|
|
683
|
+
*/
|
|
684
|
+
display?: 'activeOnly';
|
|
685
|
+
}
|
|
686
|
+
/** One daily window, as wall-clock times in `schedule.timezone`. */
|
|
687
|
+
interface WorkBuddyPromoWindow {
|
|
688
|
+
/** `'HH:mm'`, inclusive. */
|
|
689
|
+
start: string;
|
|
690
|
+
/**
|
|
691
|
+
* `'HH:mm'`, exclusive.
|
|
692
|
+
*
|
|
693
|
+
* `end` at or before `start` means the window CROSSES MIDNIGHT: `23:00` to
|
|
694
|
+
* `7:50` is the observed CN night window.
|
|
695
|
+
*/
|
|
696
|
+
end: string;
|
|
697
|
+
}
|
|
698
|
+
/** When a promotion runs. */
|
|
699
|
+
interface WorkBuddyPromoSchedule {
|
|
700
|
+
/** The IANA zone the daily windows are written in ("Asia/Shanghai"). */
|
|
701
|
+
timezone?: string;
|
|
702
|
+
/** Epoch milliseconds; the promotion has not started before it. */
|
|
703
|
+
validFrom?: number;
|
|
704
|
+
/** Epoch milliseconds; the promotion is over AT it. */
|
|
705
|
+
validUntil?: number;
|
|
706
|
+
/** The daily windows, when the upstream publishes them. */
|
|
707
|
+
daily?: readonly WorkBuddyPromoWindow[];
|
|
708
|
+
/**
|
|
709
|
+
* The upstream PUBLISHED `daily`, but not one entry of it could be evaluated.
|
|
710
|
+
*
|
|
711
|
+
* This is deliberately NOT the same as "no `daily`": an entry with no
|
|
712
|
+
* `daily` is a 24-hour campaign (the discount runs whenever the promotion
|
|
713
|
+
* does), while an entry whose every window failed to parse says nothing at
|
|
714
|
+
* all about when it runs. Reading the second as "always" would turn a live
|
|
715
|
+
* night-only campaign into a badge shown around the clock — the one
|
|
716
|
+
* direction that overstates what the upstream said. A promotion carrying
|
|
717
|
+
* this flag therefore never counts as running; the parse failure hides the
|
|
718
|
+
* badge instead of doubling it.
|
|
719
|
+
*/
|
|
720
|
+
dailyUnusable?: true;
|
|
721
|
+
}
|
|
722
|
+
/** The upstream's explanatory copy for the badge. */
|
|
723
|
+
interface WorkBuddyPromoHover {
|
|
724
|
+
/** The Chinese wording. */
|
|
725
|
+
textZh?: string;
|
|
726
|
+
/** The English wording. */
|
|
727
|
+
textEn?: string;
|
|
728
|
+
}
|
|
729
|
+
/** One upstream promotion or tier, resolved into the shape this plugin uses. */
|
|
730
|
+
interface WorkBuddyPromo {
|
|
731
|
+
/** The upstream's id ("hy3-free-trial-202608"). */
|
|
732
|
+
id: string;
|
|
733
|
+
/** Which upstream list the entry came from. */
|
|
734
|
+
source: 'promotion' | 'tier';
|
|
735
|
+
/** The models the badge applies to. */
|
|
736
|
+
modelIds: readonly string[];
|
|
737
|
+
/** What the badge says and how it looks. */
|
|
738
|
+
badge: WorkBuddyPromoBadge;
|
|
739
|
+
/** The upstream's explanatory copy, when it publishes one. */
|
|
740
|
+
hover?: WorkBuddyPromoHover;
|
|
741
|
+
/** When the promotion runs, when it publishes that. */
|
|
742
|
+
schedule?: WorkBuddyPromoSchedule;
|
|
743
|
+
/**
|
|
744
|
+
* The upstream's ordering weight: the higher the number, the more the
|
|
745
|
+
* badge matters. Used to pick ONE badge per model when several apply.
|
|
746
|
+
*/
|
|
747
|
+
priority: number;
|
|
748
|
+
}
|
|
749
|
+
//#endregion
|
|
577
750
|
//#region src/upstream.d.ts
|
|
578
751
|
/** WorkBuddy region selected by the credential's login domain. */
|
|
579
752
|
type WorkBuddyRegion = 'cn' | 'global';
|
|
@@ -609,7 +782,27 @@ type UpstreamErrorKind = 'hard_credit' |
|
|
|
609
782
|
* retrying, or switch to a less busy model." Both halves are rendered to the
|
|
610
783
|
* user; the second one is the actual remedy.
|
|
611
784
|
*/
|
|
612
|
-
'model_busy' | 'session_dead' | 'policy_reject' |
|
|
785
|
+
'model_busy' | 'session_dead' | 'policy_reject' |
|
|
786
|
+
/**
|
|
787
|
+
* The gateway's channel gate refused the request itself (business code
|
|
788
|
+
* 11128, "Illegal API invocation from an unapproved channel").
|
|
789
|
+
*
|
|
790
|
+
* Deliberately its own kind, because its `displayTips` — "this call failed
|
|
791
|
+
* channel verification, possibly because it was not sent from an official
|
|
792
|
+
* client" — names the account or the client version, and NEITHER is the
|
|
793
|
+
* cause. The gate matches the OPENING of a `system` message against a list of
|
|
794
|
+
* known third-party-agent prompts, so the same credential answers 200 to a
|
|
795
|
+
* probe and 400 to a real chat request one second later (issues #87 / #88 /
|
|
796
|
+
* #89 / #90). Filing it as `client` was technically true and practically
|
|
797
|
+
* useless: the generic "refused on its parameters" sentence left the reader
|
|
798
|
+
* to try another account, which cannot change a byte of the prompt.
|
|
799
|
+
*
|
|
800
|
+
* NOT failover-worthy for the same reason: every account sends the same
|
|
801
|
+
* system prompt, so a second account answers the same 11128 one round trip
|
|
802
|
+
* later and each attempt teaches the pool a false fact about a credential
|
|
803
|
+
* that was never the problem.
|
|
804
|
+
*/
|
|
805
|
+
'channel_reject' | 'not_found' | 'server' | 'client';
|
|
613
806
|
/** Reasoning capability as the upstream catalog declares it. */
|
|
614
807
|
interface WorkBuddyReasoning {
|
|
615
808
|
supportedEfforts?: readonly string[];
|
|
@@ -657,6 +850,20 @@ interface WorkBuddyUpstreamModel {
|
|
|
657
850
|
descriptionZh?: string;
|
|
658
851
|
descriptionEn?: string;
|
|
659
852
|
supportsToolCall?: boolean;
|
|
853
|
+
/**
|
|
854
|
+
* The upstream's own promotion / tier badges that name THIS model.
|
|
855
|
+
*
|
|
856
|
+
* Not a field of the model entry: the upstream publishes the badges in
|
|
857
|
+
* sibling arrays keyed by `modelIds` (`data.modelPromotions` /
|
|
858
|
+
* `data.modelTiers`), inside the SAME `/v3/config` document that carries
|
|
859
|
+
* the roster — so reading them costs no extra request.
|
|
860
|
+
*
|
|
861
|
+
* They ride along UNRESOLVED, and that is deliberate: a badge is a function
|
|
862
|
+
* of the clock (the upstream's windows are wall-clock hours in a named
|
|
863
|
+
* zone), so the entry that holds NOW is decided where the badge is drawn,
|
|
864
|
+
* not where the document was read. See `src/promo.ts`.
|
|
865
|
+
*/
|
|
866
|
+
promos?: readonly WorkBuddyPromo[];
|
|
660
867
|
}
|
|
661
868
|
/** One billing package as the upstream returns it, dates already parsed. */
|
|
662
869
|
interface WorkBuddyCreditPackage {
|
|
@@ -998,13 +1205,14 @@ declare class WorkBuddyUpstreamClient {
|
|
|
998
1205
|
/**
|
|
999
1206
|
* Read the model directory for the credential's region.
|
|
1000
1207
|
*
|
|
1001
|
-
* Both regions
|
|
1002
|
-
* consumes,
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1208
|
+
* Both regions read `/v3/config`, the document the WorkBuddy app itself
|
|
1209
|
+
* consumes, and both read it as the APP's product identity
|
|
1210
|
+
* ({@link PRODUCT_UA}) — the header the app sends, not the CLI channel's.
|
|
1211
|
+
* That is load-bearing, not cosmetic: the CLI channel's answer is a strict
|
|
1212
|
+
* subset of the app channel's and publishes neither `modelPromotions` nor
|
|
1213
|
+
* `modelTiers`, so reading `/v3/config` as the CLI channel is what left the
|
|
1214
|
+
* CN card without a single promotion badge while the app showed one on the
|
|
1215
|
+
* same account (see {@link PRODUCT_UA}).
|
|
1008
1216
|
*
|
|
1009
1217
|
* CN falls back to the legacy {@link MODELS_CATALOG_PATH} when `/v3` fails,
|
|
1010
1218
|
* returns a non-zero envelope, or resolves to no usable model, so a gateway
|
|
@@ -1015,8 +1223,14 @@ declare class WorkBuddyUpstreamClient {
|
|
|
1015
1223
|
*
|
|
1016
1224
|
* No user-side toggle is involved: the region comes from the credential's
|
|
1017
1225
|
* `domain`.
|
|
1226
|
+
*
|
|
1227
|
+
* `keepModelIds` is the user's own selection, passed through to
|
|
1228
|
+
* {@link selectCliModels}: a model the upstream roster dropped but the SAME
|
|
1229
|
+
* document still publishes stays in the roster, so reading the directory
|
|
1230
|
+
* never withdraws a model the user is running (issue #32's failure mode,
|
|
1231
|
+
* one step upstream of it).
|
|
1018
1232
|
*/
|
|
1019
|
-
fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
|
|
1233
|
+
fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal, keepModelIds?: readonly string[]): Promise<readonly WorkBuddyUpstreamModel[]>;
|
|
1020
1234
|
/** Query today's check-in status without changing account state. */
|
|
1021
1235
|
fetchCheckinStatus(credential: WorkBuddyCredential): Promise<WorkBuddyCheckinStatus>;
|
|
1022
1236
|
/**
|
|
@@ -2034,8 +2248,16 @@ declare function probeRequestBody(modelId: string, inputTokens?: number): string
|
|
|
2034
2248
|
* loaded, everywhere, for everyone; a 6004 says THIS ACCOUNT exceeded a volume
|
|
2035
2249
|
* limit. Reporting both as `rate-limited` made the pool bench every account for
|
|
2036
2250
|
* the cooldown of one, which is the outage issue #56 describes.
|
|
2251
|
+
*
|
|
2252
|
+
* `channel-rejected` is a third split, and it is the one that answers the
|
|
2253
|
+
* question the probe itself raises: the probe's own system prompt is a neutral
|
|
2254
|
+
* one-liner, so it can return `ok` on a model whose real chat requests are all
|
|
2255
|
+
* refused with `11128`. Filing that as `failed` (the generic bucket) made the
|
|
2256
|
+
* pair "test passes, chat fails, every account" look like a flaky upstream
|
|
2257
|
+
* instead of what it is — the gateway objecting to the PROMPT, not the account
|
|
2258
|
+
* (issues #87–#90).
|
|
2037
2259
|
*/
|
|
2038
|
-
type WorkBuddyProbeOutcome = 'ok' | 'rate-limited' | 'model-busy' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
2260
|
+
type WorkBuddyProbeOutcome = 'ok' | 'rate-limited' | 'model-busy' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' | 'channel-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
2039
2261
|
/**
|
|
2040
2262
|
* One probe result, as the card renders it.
|
|
2041
2263
|
*
|
|
@@ -2122,9 +2344,10 @@ declare function cooldownOf(input: {
|
|
|
2122
2344
|
* answer means. The extra splits here are `credential-rejected`, which the
|
|
2123
2345
|
* upstream signals with a non-JSON 401/403 edge page and which needs completely
|
|
2124
2346
|
* different advice (re-auth, not "wait"), `policy-rejected`, a JSON 403
|
|
2125
|
-
* content-policy refusal where re-auth advice would be actively misleading,
|
|
2347
|
+
* content-policy refusal where re-auth advice would be actively misleading,
|
|
2126
2348
|
* `model-busy`, the model-scoped 14003 throttle that must NOT be benched as an
|
|
2127
|
-
* account failure (issue #56)
|
|
2349
|
+
* account failure (issue #56), and `channel-rejected`, the gateway's 11128 gate
|
|
2350
|
+
* on the PROMPT's opening, which no account can change.
|
|
2128
2351
|
*/
|
|
2129
2352
|
declare function outcomeOfFailure(status: number, body: string): WorkBuddyProbeOutcome;
|
|
2130
2353
|
/**
|
|
@@ -2364,19 +2587,6 @@ interface WorkBuddyPoolTestRow {
|
|
|
2364
2587
|
}
|
|
2365
2588
|
//#endregion
|
|
2366
2589
|
//#region src/status-paths.d.ts
|
|
2367
|
-
/**
|
|
2368
|
-
* Node-free constants and types shared by the Host and browser halves.
|
|
2369
|
-
*
|
|
2370
|
-
* 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
|
|
2371
|
-
* — 「3 条同源只读路由(usage / models:refresh / accounts:refresh)+ 一份
|
|
2372
|
-
* 与浏览器共享的 node-free 类型定义」的 host↔client 桥梁形态来自该项目
|
|
2373
|
-
* (其 `status-paths.ts` 亦如此,并注明沿用
|
|
2374
|
-
* corrinehu/dsh-workbuddy-connect 的 status-route 模式)。
|
|
2375
|
-
* 改动:路由路径改用本插件 id;类型字段按 WorkBuddy 上游实际给出的能力
|
|
2376
|
-
* (积分倍率、多模态、推理档位)调整,不保留 trae 的 1M 变体字段。
|
|
2377
|
-
*
|
|
2378
|
-
* @module dsh-connect-workbuddy/status-paths
|
|
2379
|
-
*/
|
|
2380
2590
|
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
2381
2591
|
declare const WORKBUDDY_USAGE_PATH = "/plugins/dsh-connect-workbuddy/usage";
|
|
2382
2592
|
/** Plugin-owned live model refresh endpoint. */
|
|
@@ -2612,6 +2822,18 @@ interface WorkBuddyWebModel {
|
|
|
2612
2822
|
refusesOff?: boolean;
|
|
2613
2823
|
};
|
|
2614
2824
|
description?: string;
|
|
2825
|
+
/**
|
|
2826
|
+
* The upstream's promotion / tier badges that name this model, UNRESOLVED.
|
|
2827
|
+
*
|
|
2828
|
+
* Every candidate that names the model rides along, in priority order, so
|
|
2829
|
+
* the card can pick the one that holds at the moment it draws — a badge is a
|
|
2830
|
+
* function of the clock, and a badge frozen at fetch time would keep
|
|
2831
|
+
* claiming a night discount after the night is over. See `src/promo.ts`.
|
|
2832
|
+
*
|
|
2833
|
+
* Persisted inside `lastCatalog` (it rides along in the saved entries) and
|
|
2834
|
+
* read back from there, which is why it must stay JSON-shaped.
|
|
2835
|
+
*/
|
|
2836
|
+
promos?: readonly WorkBuddyPromo[];
|
|
2615
2837
|
}
|
|
2616
2838
|
/**
|
|
2617
2839
|
* Project one card row into its persisted `lastCatalog` shape: the native
|
|
@@ -2822,7 +3044,17 @@ type WorkBuddyWebProbeOutcome = 'ok' | 'rate-limited' |
|
|
|
2822
3044
|
* to learn what the first one already said. The remedy is the upstream's own:
|
|
2823
3045
|
* wait about a minute, or switch to a less busy model.
|
|
2824
3046
|
*/
|
|
2825
|
-
'model-busy' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' |
|
|
3047
|
+
'model-busy' | 'out-of-credit' | 'credential-rejected' | 'policy-rejected' |
|
|
3048
|
+
/**
|
|
3049
|
+
* The gateway's channel gate refused the request itself (upstream code 11128).
|
|
3050
|
+
*
|
|
3051
|
+
* Kept apart from `failed` because its whole point is the question it answers:
|
|
3052
|
+
* the probe passes and the chat fails on the SAME model and the SAME account,
|
|
3053
|
+
* because the gate matches the OPENING of a `system` message and the probe's
|
|
3054
|
+
* is a neutral one-liner. A generic failure would leave the user swapping
|
|
3055
|
+
* accounts that cannot change a byte of their prompt (issues #87–#90).
|
|
3056
|
+
*/
|
|
3057
|
+
'channel-rejected' | 'unavailable' | 'not-found' | 'failed';
|
|
2826
3058
|
/** One model's probe result, as the card renders it. */
|
|
2827
3059
|
interface WorkBuddyWebProbeResult {
|
|
2828
3060
|
modelId: string;
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as withEffectiveOff, A as workbuddyAccountId, B as modelIdOfBody, C as defaultDesktopAuthDirs, D as isVaultAccountId, E as isEncryptedCredentialError, F as WorkBuddyUpstreamClient, G as regionOf, H as parseReasoning, I as classifyUpstreamError, J as defaultOffModelIds, K as systemOpeningsOfBody, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, Q as recordRefusal, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as offDefaultFor, Y as lastRefusalFor, Z as offRefused, _ as WORKBUDDY_AUTH_FILENAME, _t as writeFileAtomic, a as readHostHeartbeat, at as findWorkbuddyAppExecutable, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as isWorkbuddyBundle, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as openEncryptedField, et as WORKBUDDY_APP_EXECUTABLE_ENV, f as WorkBuddyCatalog, ft as readAtRestKey, g as ENCRYPTED_CREDENTIAL_CODE, gt as withFileLock, h as fallbackModelsFor, ht as resolveDshHome, i as processStartTimeMs, it as fetchAtRestKeyPayload, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as macosBundleExecutable, m as deriveCatalog, n as clearHostHeartbeat, nt as deriveAtRestKey, o as workbuddyHostHeartbeatPath, p as applyContextBudgets, pt as workbuddyAppExecutableCandidates, q as REFUSES_OFF_MODEL_IDS, r as isHeartbeatProcessAlive, rt as deriveAtRestKeyId, s as writeHostHeartbeat, st as isEncryptedFieldWrapper, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as clearAtRestKeyCache, u as FALLBACK_WORKBUDDY_MODELS, ut as macosNestedAppBundles, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-B1I95fKS.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -2276,6 +2276,7 @@ const KIND_STATUS = {
|
|
|
2276
2276
|
model_busy: 429,
|
|
2277
2277
|
session_dead: 401,
|
|
2278
2278
|
policy_reject: 403,
|
|
2279
|
+
channel_reject: 400,
|
|
2279
2280
|
not_found: 502,
|
|
2280
2281
|
server: 502,
|
|
2281
2282
|
client: 400
|
|
@@ -2344,6 +2345,13 @@ const KIND_STATUS = {
|
|
|
2344
2345
|
* reset is hours away, so the retry budget cannot produce an answer and must
|
|
2345
2346
|
* not be spent. One word, two opposite temporal claims — so the spelling
|
|
2346
2347
|
* follows the clock, not the status code both share.
|
|
2348
|
+
*
|
|
2349
|
+
* `channel_reject` is not listed, and the reason is the same rule applied to a
|
|
2350
|
+
* third clock: the gate's verdict is DETERMINISTIC for a given prompt, so an
|
|
2351
|
+
* in-place retry would spend the budget re-sending bytes it already refused, and
|
|
2352
|
+
* cross-provider failover cannot help either (every account sends that same
|
|
2353
|
+
* prompt). Its 400 already places it outside the host's retry set, so `type`
|
|
2354
|
+
* alone, untranslated, is the honest spelling.
|
|
2347
2355
|
*/
|
|
2348
2356
|
const KIND_HOST_CODE = { soft_rate: "quota_exceeded" };
|
|
2349
2357
|
/**
|
|
@@ -2916,6 +2924,7 @@ function failoverReasonText(failure) {
|
|
|
2916
2924
|
case "model_busy": return "该模型当前繁忙(不是账号问题)";
|
|
2917
2925
|
case "session_dead": return "登录已失效";
|
|
2918
2926
|
case "policy_reject": return "被服务端策略拒绝";
|
|
2927
|
+
case "channel_reject": return "被网关的渠道校验拦截(不是账号、额度或网络问题)";
|
|
2919
2928
|
case "not_found": return "模型不可用";
|
|
2920
2929
|
case "server": return "上游异常";
|
|
2921
2930
|
case "client": return "请求被上游拒绝";
|
|
@@ -3247,7 +3256,7 @@ function createWorkBuddyShim(options) {
|
|
|
3247
3256
|
* which another account may well survive.
|
|
3248
3257
|
*/
|
|
3249
3258
|
function isFailoverWorthy(kind) {
|
|
3250
|
-
return kind !== "client" && kind !== "model_busy";
|
|
3259
|
+
return kind !== "client" && kind !== "model_busy" && kind !== "channel_reject";
|
|
3251
3260
|
}
|
|
3252
3261
|
async function chatCompletions(req, res) {
|
|
3253
3262
|
if (!isJsonContentType(req)) {
|
|
@@ -3375,6 +3384,18 @@ function createWorkBuddyShim(options) {
|
|
|
3375
3384
|
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, message, hostErrorCode(result.kind));
|
|
3376
3385
|
return;
|
|
3377
3386
|
}
|
|
3387
|
+
if (result.kind === "channel_reject") {
|
|
3388
|
+
const detail = result.detail;
|
|
3389
|
+
const meta = [];
|
|
3390
|
+
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
3391
|
+
if (detail?.requestId !== void 0) meta.push(`requestId ${detail.requestId}`);
|
|
3392
|
+
const stamp = meta.length === 0 ? "" : `(${meta.join(",")})`;
|
|
3393
|
+
const openings = systemOpeningsOfBody(prepared);
|
|
3394
|
+
const evidence = openings.length === 0 ? "" : `命中的 system 提示开头:${openings.map((opening) => `「${opening}」`).join("、")}。`;
|
|
3395
|
+
const message = `workbuddy 上游失败:${failoverReasonText(result)}(http ${result.status})${note}${stamp}。${evidence}该请求被 WorkBuddy 网关的渠道校验拦截,上游把它描述成「不是官方客户端发出的」,但那是误导性文案:网关匹配的是 system 提示的开头,与账号、积分、额度、网络都无关,换账号或换区域也不会改变结果。若这段开头来自宿主或其他插件,请修改该提示;插件已对已知的第三方 agent 指纹做归一化,新的指纹可反馈到 issue 里`;
|
|
3396
|
+
writeOpenAIError(res, KIND_STATUS[result.kind], result.kind, message, hostErrorCode(result.kind));
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3378
3399
|
const detail = result.detail;
|
|
3379
3400
|
const meta = [];
|
|
3380
3401
|
if (detail?.upstreamCode !== void 0) meta.push(`code ${detail.upstreamCode}`);
|
|
@@ -3734,9 +3755,10 @@ function cooldownOf(input) {
|
|
|
3734
3755
|
* answer means. The extra splits here are `credential-rejected`, which the
|
|
3735
3756
|
* upstream signals with a non-JSON 401/403 edge page and which needs completely
|
|
3736
3757
|
* different advice (re-auth, not "wait"), `policy-rejected`, a JSON 403
|
|
3737
|
-
* content-policy refusal where re-auth advice would be actively misleading,
|
|
3758
|
+
* content-policy refusal where re-auth advice would be actively misleading,
|
|
3738
3759
|
* `model-busy`, the model-scoped 14003 throttle that must NOT be benched as an
|
|
3739
|
-
* account failure (issue #56)
|
|
3760
|
+
* account failure (issue #56), and `channel-rejected`, the gateway's 11128 gate
|
|
3761
|
+
* on the PROMPT's opening, which no account can change.
|
|
3740
3762
|
*/
|
|
3741
3763
|
function outcomeOfFailure(status, body) {
|
|
3742
3764
|
if (status === 401 || status === 403) {
|
|
@@ -3748,6 +3770,7 @@ function outcomeOfFailure(status, body) {
|
|
|
3748
3770
|
case "model_busy": return "model-busy";
|
|
3749
3771
|
case "hard_credit": return "out-of-credit";
|
|
3750
3772
|
case "session_dead": return "credential-rejected";
|
|
3773
|
+
case "channel_reject": return "channel-rejected";
|
|
3751
3774
|
case "not_found": return "not-found";
|
|
3752
3775
|
default: return status === 0 || status >= 500 ? "unavailable" : "failed";
|
|
3753
3776
|
}
|
|
@@ -4096,6 +4119,7 @@ function exclusionOf(probe, nowMs) {
|
|
|
4096
4119
|
case "model-busy": return retryDue(probe, nowMs, 12e4) ? void 0 : "model-busy";
|
|
4097
4120
|
case "out-of-credit": return retryDue(probe, nowMs) ? void 0 : "out-of-credit";
|
|
4098
4121
|
case "policy-rejected": return;
|
|
4122
|
+
case "channel-rejected": return;
|
|
4099
4123
|
default: return "unusable";
|
|
4100
4124
|
}
|
|
4101
4125
|
}
|
|
@@ -4976,19 +5000,6 @@ async function writePoolProbes(region, updates) {
|
|
|
4976
5000
|
}
|
|
4977
5001
|
//#endregion
|
|
4978
5002
|
//#region src/status-paths.ts
|
|
4979
|
-
/**
|
|
4980
|
-
* Node-free constants and types shared by the Host and browser halves.
|
|
4981
|
-
*
|
|
4982
|
-
* 参考:dingminhua/dsh-connect-trae(MIT,Copyright (c) 2026 LaoDing)
|
|
4983
|
-
* — 「3 条同源只读路由(usage / models:refresh / accounts:refresh)+ 一份
|
|
4984
|
-
* 与浏览器共享的 node-free 类型定义」的 host↔client 桥梁形态来自该项目
|
|
4985
|
-
* (其 `status-paths.ts` 亦如此,并注明沿用
|
|
4986
|
-
* corrinehu/dsh-workbuddy-connect 的 status-route 模式)。
|
|
4987
|
-
* 改动:路由路径改用本插件 id;类型字段按 WorkBuddy 上游实际给出的能力
|
|
4988
|
-
* (积分倍率、多模态、推理档位)调整,不保留 trae 的 1M 变体字段。
|
|
4989
|
-
*
|
|
4990
|
-
* @module dsh-connect-workbuddy/status-paths
|
|
4991
|
-
*/
|
|
4992
5003
|
/** Plugin-owned usage endpoint consumed by its browser half. */
|
|
4993
5004
|
const WORKBUDDY_USAGE_PATH = "/plugins/dsh-connect-workbuddy/usage";
|
|
4994
5005
|
/**
|
|
@@ -5379,7 +5390,8 @@ function toWebModel(model, budgets) {
|
|
|
5379
5390
|
...model.reasoning.canDisableThinking === void 0 ? {} : { canDisableThinking: model.reasoning.canDisableThinking },
|
|
5380
5391
|
...offRefused(model.id) ? { offRefused: true } : {},
|
|
5381
5392
|
...model.reasoning.refusesOff === true ? { refusesOff: true } : {}
|
|
5382
|
-
} }
|
|
5393
|
+
} },
|
|
5394
|
+
...model.promos === void 0 ? {} : { promos: model.promos }
|
|
5383
5395
|
};
|
|
5384
5396
|
}
|
|
5385
5397
|
/**
|
|
@@ -6609,7 +6621,8 @@ function apply(ctx, config) {
|
|
|
6609
6621
|
const effectiveAccountFor = (region, value) => selectAccountFor(region, value, legacyAccountRegion);
|
|
6610
6622
|
const discoverModels = async (region, signal) => {
|
|
6611
6623
|
const credential = await stacks[region].store.resolve();
|
|
6612
|
-
|
|
6624
|
+
const keep = regionStateOf(current(), region).enabledModelIds ?? [];
|
|
6625
|
+
return client.fetchModels(credential, signal, keep);
|
|
6613
6626
|
};
|
|
6614
6627
|
/** Push the current config into every region's store selection and catalog. */
|
|
6615
6628
|
const applySelection = (value) => {
|
|
@@ -7611,7 +7624,7 @@ function apply(ctx, config) {
|
|
|
7611
7624
|
try {
|
|
7612
7625
|
const credential = await stacks[region].store.resolve();
|
|
7613
7626
|
if (stopped) return;
|
|
7614
|
-
const models = await client.fetchModels(credential);
|
|
7627
|
+
const models = await client.fetchModels(credential, void 0, regionStateOf(current(), region).enabledModelIds ?? []);
|
|
7615
7628
|
if (stopped) return;
|
|
7616
7629
|
const state = regionStateOf(current(), region);
|
|
7617
7630
|
stacks[region].catalog.set(withEffectiveOff(withImageSelection(deriveCatalog(models, new Set(state.enabledModelIds ?? []), state.contextBudgets ?? {}), new Set(state.imageModelIds ?? [])), new Set(savedOffModelIds(state, models))));
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-connect-workbuddy",
|
|
3
3
|
"displayName": "DSH Connect WorkBuddy",
|
|
4
4
|
"description": "把本机登录的 WorkBuddy 模型(国内版与国际版)接入 DeepSeek Harness:每次刷新按上游目录自动读取每个模型的积分倍率,支持逐模型图片输入开关、多账号切换与账号池自动换号,并提供每日签到、卡片与输入框里的胶囊积分读数等只读积分概览。",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.14.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|