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/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' | 'not_found' | 'server' | 'client';
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 prefer `/v3/config`, the document the WorkBuddy app itself
1002
- * consumes, so the plugin's roster and the app's agree. They differ in the
1003
- * user agent that requests it, and that difference is load-bearing: `/v3`
1004
- * serves a different roster per client channel. Global asks as the desktop
1005
- * channel (see {@link DESKTOP_UA}); CN asks as the CLI channel
1006
- * ({@link CLIENT_UA}), which is the only CN channel whose `/v3` answer
1007
- * contains the free `hy4-preview-f` (see {@link GLOBAL_CONFIG_PATH}).
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, and
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' | 'unavailable' | 'not-found' | 'failed';
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 WORKBUDDY_APP_EXECUTABLE_ENV, 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 lastRefusalFor, K as REFUSES_OFF_MODEL_IDS, L as declaredTools, M as CREDENTIAL_REJECTED_CODE, N as WORKBUDDY_FALLBACK_SYSTEM_PROMPT, O as legacyWorkbuddyOwnAuthPath, P as WorkBuddyCredentialRejectedError, Q as withEffectiveOff, R as isCredentialRejectedError, S as defaultDesktopAuthCandidates, T as hasEncryptedCredentialFields, U as parseUpstreamModel, V as parseCreditMultiplier, W as prepareChatBody, X as offRefused, Y as offDefaultFor, Z as recordRefusal, _ as WORKBUDDY_AUTH_FILENAME, a as readHostHeartbeat, b as WorkBuddyEncryptedCredentialError, c as WORKBUDDY_CONNECT_VERSION, ct as macosBundleExecutable, d as FALLBACK_WORKBUDDY_MODELS_GLOBAL, dt as readAtRestKey, et as clearAtRestKeyCache, f as WorkBuddyCatalog, ft as workbuddyAppExecutableCandidates, g as ENCRYPTED_CREDENTIAL_CODE, gt as writeFileAtomic, h as fallbackModelsFor, ht as withFileLock, i as processStartTimeMs, it as findWorkbuddyAppExecutable, j as workbuddyOwnAuthPath, k as parseWorkBuddyAuth, l as piAiRuntimeInfo, lt as macosNestedAppBundles, m as deriveCatalog, mt as resolveDshHome, n as clearHostHeartbeat, nt as deriveAtRestKeyId, o as workbuddyHostHeartbeatPath, ot as isEncryptedFieldWrapper, p as applyContextBudgets, q as defaultOffModelIds, r as isHeartbeatProcessAlive, rt as fetchAtRestKeyPayload, s as writeHostHeartbeat, st as isWorkbuddyBundle, t as WORKBUDDY_HOST_HEARTBEAT_FILENAME, tt as deriveAtRestKey, u as FALLBACK_WORKBUDDY_MODELS, ut as openEncryptedField, v as WORKBUDDY_AUTH_FILE_ENV, w as defaultDesktopAuthPath, x as authFileName, y as WorkBuddyCredentialStore, z as knownRegionOf } from "./host-heartbeat-CwdR6teZ.js";
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, and
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
- return client.fetchModels(credential, signal);
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.13.1",
5
+ "version": "4.14.0",
6
6
  "author": "dingminhua",
7
7
  "copyright": "Copyright (c) 2026 LaoDing",
8
8
  "license": "MIT",