dsh-workbuddy-connect 0.6.2 → 0.6.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/README.en.md +1 -0
- package/README.md +1 -0
- package/lib/bin.js +4 -4
- package/lib/index.d.ts +158 -142
- package/lib/index.js +2 -2
- package/lib/{variants-B3Pa4EBr.js → variants-D_oxWP_K.js} +254 -26
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -145,6 +145,7 @@ dsh plugin --profile web exec dsh-workbuddy-connect doctor --provider workbuddy-
|
|
|
145
145
|
## Known limitations
|
|
146
146
|
|
|
147
147
|
- Verified on macOS with the DSH Web / Desktop / TUI profiles (as of 0.3.2 this requires `0.1.5-rc.1`+ and Node 22+; TUI requires the terminal UI package `0.10.0-beta.5` or newer — see the Install section). Windows probes Local and Roaming AppData in order; WSL first reads credentials from the mounted Windows user profile. If the Windows and Linux user names differ and Windows environment variables are not forwarded into WSL, point `WORKBUDDY_AUTH_FILE` (or `WORKBUDDY_AI_AUTH_FILE` for the international version) at the actual file.
|
|
148
|
+
- **Encrypted desktop credential helper discovery**: the CN version uses the verified app discovery for macOS; on Windows it first checks `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe`, then the WorkBuddy uninstall registry records. If automatic discovery cannot resolve it, set `WORKBUDDY_ELECTRON_BIN` to the actual `WorkBuddy.exe`, then fully quit and restart DSH (the plugin reads this variable when constructed). Linux has no built-in auto-discovery; the card offers Agent Assist when needed, while an explicit environment path remains supported. Windows auto-discovery is not enabled for WorkBuddy AI.
|
|
148
149
|
- **The international version's model catalog comes from the app's own interface**: the service splits it by User-Agent, which is a private implementation detail that a server-side change can break. When that happens the plugin degrades to this account's last successful catalog and then to its built-in roster, showing the source (live / saved / built-in), the fetch time, and the failure reason on the card — but long-term compatibility is not guaranteed. The CN version's catalog uses the same interface as the official CLI and is unaffected.
|
|
149
150
|
- **International-version environments not yet covered**: on Windows / WSL / Linux no reliable source for the international app's version has been located yet, so the saved value or the built-in default is used. On macOS, real-shim checks covered complete GPT-family replies, tool calls, and continued turns.
|
|
150
151
|
- **Behaviour change with no credentials**: a version whose app was never signed in — and that left no plugin-owned copy — no longer shows a model group. The CN version used to display a built-in fallback list, but every model on it failed when selected.
|
package/README.md
CHANGED
|
@@ -154,6 +154,7 @@ dsh plugin --profile web exec dsh-workbuddy-connect doctor --provider workbuddy-
|
|
|
154
154
|
## 已知限制
|
|
155
155
|
|
|
156
156
|
- 在 macOS 的 DSH Web / Desktop / TUI 下验证通过(0.3.2 起要求 `0.1.5-rc.1`+、Node 22+;TUI 需终端界面插件 `0.10.0-beta.5` 及以上,见安装章节说明)。Windows 会依次探测 Local 与 Roaming AppData;WSL 会优先从挂载的 Windows 用户目录读取登录凭据。若 Windows 与 Linux 用户名不同且 Windows 环境变量未传入 WSL,请通过 `WORKBUDDY_AUTH_FILE`(国际版为 `WORKBUDDY_AI_AUTH_FILE`)指定实际位置。
|
|
157
|
+
- **加密桌面凭据的解密程序定位**:国内版 macOS 使用已验证的 App 发现;Windows 先检查 `%LOCALAPPDATA%\Programs\WorkBuddy\WorkBuddy.exe`,再检查 WorkBuddy 卸载注册表记录。若自动定位仍不可用,可设置 `WORKBUDDY_ELECTRON_BIN` 指向实际的 `WorkBuddy.exe`,然后完全退出并重启 DSH(插件在构造时读取该变量)。Linux 没有内置自动定位;失败时卡片提供 Agent Assist,显式环境变量仍可用。国际版不启用 Windows 自动定位。
|
|
157
158
|
- **国际版的模型目录来自 App 界面接口**:服务端按 User-Agent 分流下发,属私有实现,上游改动可能使其失效。届时插件按「本账号上次成功目录 → 内置目录」降级,并在卡片上标明来源(实时 / 已保存 / 内置)、更新时间与失败原因,但不能保证长期兼容。国内版目录走官方 CLI 同款接口,不受此影响。
|
|
158
159
|
- **国际版仍未覆盖的环境**:Windows / WSL / Linux 下国际版 App 的版本读取尚未找到可靠来源,会退回最近保存的版本或内置值。macOS 上已通过真实 shim 验证 GPT 系完整回复、工具调用与续轮。
|
|
159
160
|
- **无凭据时的行为变化**:某版 App 从未登录、也没留下插件自留副本时,该版模型分组不再显示。此前国内版会显示一份内置兜底列表,但那些模型选了必然报错。
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { D as
|
|
2
|
+
import { D as electronDiscoveryFor, E as WorkBuddyAtRestKeyProvider, O as WorkBuddyUpstreamClient, X as resolveAppVersion, b as WorkBuddyCredentialStore, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, l as readHostHeartbeat, n as CN_VARIANT, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, u as workbuddyHostHeartbeatPath } from "./variants-D_oxWP_K.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
|
@@ -30,8 +30,8 @@ function printJson(value) {
|
|
|
30
30
|
* One variant's store plus the client that performs its refreshes.
|
|
31
31
|
*
|
|
32
32
|
* The key provider is injected explicitly, and per variant, for the same
|
|
33
|
-
* reason the plugin host does it: discovery is CN/
|
|
34
|
-
* must not be left to its no-arg default — that default is deliberately
|
|
33
|
+
* reason the plugin host does it: discovery is CN/platform-specific, so the
|
|
34
|
+
* provider must not be left to its no-arg default — that default is deliberately
|
|
35
35
|
* `discovery: 'none'`, and relying on it here would quietly strip the CN CLI
|
|
36
36
|
* of the decryption it has always had.
|
|
37
37
|
*/
|
|
@@ -40,7 +40,7 @@ function makeStore(variant) {
|
|
|
40
40
|
return new WorkBuddyCredentialStore({
|
|
41
41
|
variant,
|
|
42
42
|
refresh: (credential) => client.refreshToken(credential),
|
|
43
|
-
keyProvider: new WorkBuddyAtRestKeyProvider({ discovery: variant
|
|
43
|
+
keyProvider: new WorkBuddyAtRestKeyProvider({ discovery: electronDiscoveryFor(variant) })
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
/** The plugin-owned credential copy for one variant, for display. */
|
package/lib/index.d.ts
CHANGED
|
@@ -20,9 +20,9 @@ type WorkBuddySignedOutReasonCode =
|
|
|
20
20
|
'credential-region-mismatch' |
|
|
21
21
|
/** An encrypted credential exists but could not be opened (wrong key, GCM failure, helper crash). */
|
|
22
22
|
'encrypted-credential-unreadable' |
|
|
23
|
-
/** CN
|
|
23
|
+
/** CN on a supported platform: discovery ran to completion and found no usable candidate. */
|
|
24
24
|
'electron-binary-not-found' |
|
|
25
|
-
/** CN
|
|
25
|
+
/** CN on a supported platform: discovery found more than one distinct usable app. */
|
|
26
26
|
'electron-binary-ambiguous' |
|
|
27
27
|
/** No auto-discovery for this product/platform and no explicit path configured. */
|
|
28
28
|
'electron-binary-unavailable' |
|
|
@@ -31,145 +31,6 @@ type WorkBuddySignedOutReasonCode =
|
|
|
31
31
|
/** Discovery could not finish: tool missing, timeout, output overflow, unreadable plist. */
|
|
32
32
|
'electron-discovery-incomplete';
|
|
33
33
|
//#endregion
|
|
34
|
-
//#region src/desktop-credential-protection.d.ts
|
|
35
|
-
/** The four states a desktop auth document can be read as. */
|
|
36
|
-
type DesktopAuthFormat = 'absent' | 'plaintext' | 'encrypted' | 'unrecognized';
|
|
37
|
-
/** The spawned helper. Separated from the provider so tests can stand it in. */
|
|
38
|
-
type WorkBuddyKeyPayloadSource = () => Promise<string>;
|
|
39
|
-
/**
|
|
40
|
-
* Which automatic discovery, if any, this provider may run when no explicit
|
|
41
|
-
* binary is configured.
|
|
42
|
-
*
|
|
43
|
-
* `none` is the safe default: a provider that has not been told which product
|
|
44
|
-
* it serves must not reach for another product's app. `macos-workbuddy` is the
|
|
45
|
-
* CN line — the only one whose at-rest credentials and app layout have been
|
|
46
|
-
* verified live — and resolves the platform default and then Spotlight.
|
|
47
|
-
*/
|
|
48
|
-
type WorkBuddyElectronDiscovery = 'none' | 'macos-workbuddy';
|
|
49
|
-
/** Seams the discovery flow runs through, so tests never spawn a process. */
|
|
50
|
-
interface WorkBuddyDiscoveryTools {
|
|
51
|
-
/** Candidate `.app` bundles for the CN bundle id, or a throw for an unusable tool. */
|
|
52
|
-
findApps: (signal: AbortSignal) => Promise<readonly string[]>;
|
|
53
|
-
/**
|
|
54
|
-
* `CFBundleIdentifier` of a bundle, or `undefined` when the tool could not
|
|
55
|
-
* read it — which is "we could not check this candidate", never "it does not
|
|
56
|
-
* match". A successful read of a *different* id returns that id, and the
|
|
57
|
-
* caller excludes the candidate.
|
|
58
|
-
*/
|
|
59
|
-
bundleIdentifier: (bundlePath: string, signal: AbortSignal) => Promise<string | undefined>;
|
|
60
|
-
/** Display version, best effort; `undefined` when unavailable. */
|
|
61
|
-
bundleVersion: (bundlePath: string, signal: AbortSignal) => Promise<string | undefined>;
|
|
62
|
-
}
|
|
63
|
-
/** Provider options. */
|
|
64
|
-
interface WorkBuddyAtRestKeyProviderOptions {
|
|
65
|
-
/** Explicit Electron binary; overrides the platform default and env. */
|
|
66
|
-
electronPath?: string;
|
|
67
|
-
/** Helper timeout in milliseconds; default 10s. */
|
|
68
|
-
timeoutMs?: number;
|
|
69
|
-
/**
|
|
70
|
-
* Where the payload comes from. Defaults to spawning WorkBuddy's own
|
|
71
|
-
* Electron with `ELECTRON_RUN_AS_NODE=1`; tests supply a stand-in so no
|
|
72
|
-
* test ever touches the real binary or a real key. Supplying this replaces
|
|
73
|
-
* path *resolution* too, so tests about resolution use
|
|
74
|
-
* {@link spawnHelper} instead.
|
|
75
|
-
*/
|
|
76
|
-
source?: WorkBuddyKeyPayloadSource;
|
|
77
|
-
/**
|
|
78
|
-
* Runs the helper at the resolved path. Distinct from {@link source}, which
|
|
79
|
-
* replaces the whole payload path: this seam keeps resolution — explicit
|
|
80
|
-
* config, platform default, discovery — real, so tests can exercise it
|
|
81
|
-
* without spawning anything.
|
|
82
|
-
*/
|
|
83
|
-
spawnHelper?: (electronPath: string) => Promise<string>;
|
|
84
|
-
/**
|
|
85
|
-
* Automatic discovery budget; defaults to `'none'` (see
|
|
86
|
-
* {@link WorkBuddyElectronDiscovery}). Passed explicitly per variant at the
|
|
87
|
-
* composition root, never inferred from the environment.
|
|
88
|
-
*/
|
|
89
|
-
discovery?: WorkBuddyElectronDiscovery;
|
|
90
|
-
/**
|
|
91
|
-
* Platform default binary, consulted only when `discovery` is enabled and no
|
|
92
|
-
* explicit path is configured. Injectable so tests can force the fallback
|
|
93
|
-
* branch without moving the real app; `null` means "no default here".
|
|
94
|
-
*/
|
|
95
|
-
defaultElectronPath?: string | undefined;
|
|
96
|
-
/** Discovery subprocesses; injectable so tests never spawn. */
|
|
97
|
-
tools?: WorkBuddyDiscoveryTools;
|
|
98
|
-
/**
|
|
99
|
-
* Total budget for one discovery run, covering the search and every
|
|
100
|
-
* candidate check. Injectable so tests can exercise exhaustion without
|
|
101
|
-
* waiting out the production 10s.
|
|
102
|
-
*/
|
|
103
|
-
discoveryBudgetMs?: number;
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* In-memory protector-key resolver: one spawn per key id, single-flight, never
|
|
107
|
-
* persisted. The cache is keyed by the id envelopes ask for, so an envelope
|
|
108
|
-
* sealed under a rotated key triggers exactly one fresh resolution.
|
|
109
|
-
*/ declare class WorkBuddyAtRestKeyProvider {
|
|
110
|
-
/**
|
|
111
|
-
* The explicit binary, when one was configured. `undefined` here means "the
|
|
112
|
-
* caller did not name one", which is what lets discovery run — an explicit
|
|
113
|
-
* path that turns out to be unusable is an error, never a reason to look for
|
|
114
|
-
* a different app.
|
|
115
|
-
*/
|
|
116
|
-
private readonly explicitPath;
|
|
117
|
-
private readonly defaultPath;
|
|
118
|
-
private readonly discovery;
|
|
119
|
-
private readonly tools;
|
|
120
|
-
private readonly discoveryBudgetMs;
|
|
121
|
-
private readonly timeoutMs;
|
|
122
|
-
private readonly source;
|
|
123
|
-
private readonly spawnHelper;
|
|
124
|
-
/**
|
|
125
|
-
* The path discovery settled on, cached only on success. A failure leaves
|
|
126
|
-
* this unset so the next attempt tries again — the user may install or move
|
|
127
|
-
* the app without restarting DSH.
|
|
128
|
-
*/
|
|
129
|
-
private discoveredPath;
|
|
130
|
-
private cache;
|
|
131
|
-
private inflight;
|
|
132
|
-
constructor(options?: WorkBuddyAtRestKeyProviderOptions);
|
|
133
|
-
/**
|
|
134
|
-
* The binary the default helper would use, for diagnostics.
|
|
135
|
-
*
|
|
136
|
-
* Reports a *discovery result* once one exists, so diagnostics describe what
|
|
137
|
-
* would actually run rather than the default that was bypassed. Discovery
|
|
138
|
-
* itself stays in {@link resolveElectronPath}: this accessor never triggers a
|
|
139
|
-
* search (the constructor must remain I/O-free, and callers may ask before
|
|
140
|
-
* any resolution has happened).
|
|
141
|
-
*/
|
|
142
|
-
helperPath(): string | undefined;
|
|
143
|
-
/**
|
|
144
|
-
* A protector key matching one of the requested envelope key ids. The first
|
|
145
|
-
* id the cache answers wins; otherwise one spawn resolves the current key,
|
|
146
|
-
* which must match a request — a mismatch means the envelopes were sealed by
|
|
147
|
-
* a different install than the one this machine now runs, and no key we can
|
|
148
|
-
* reach will open them.
|
|
149
|
-
*/
|
|
150
|
-
protectorKeyFor(requested: readonly string[]): Promise<Buffer>;
|
|
151
|
-
private ingest;
|
|
152
|
-
/**
|
|
153
|
-
* The binary to spawn, or a diagnosable error saying why there is none.
|
|
154
|
-
*
|
|
155
|
-
* Order is the contract: an explicit path is used as-is and never falls back;
|
|
156
|
-
* discovery runs only for a provider that was configured for it, and only
|
|
157
|
-
* after the platform default has been tried and found unusable.
|
|
158
|
-
*/
|
|
159
|
-
private resolveElectronPath;
|
|
160
|
-
/**
|
|
161
|
-
* Resolve the CN app through Spotlight, then prove each candidate's identity
|
|
162
|
-
* before it can be executed.
|
|
163
|
-
*
|
|
164
|
-
* The whole flow shares one budget: a hang in one candidate must not extend
|
|
165
|
-
* the wait for the others, and running out of budget is reported as an
|
|
166
|
-
* unfinished check rather than an absent app.
|
|
167
|
-
*/
|
|
168
|
-
private discoverMacosApp;
|
|
169
|
-
private spawnPayload;
|
|
170
|
-
private spawnAt;
|
|
171
|
-
}
|
|
172
|
-
//#endregion
|
|
173
34
|
//#region src/app-version.d.ts
|
|
174
35
|
/** Basename of the saved version under `$DSH_HOME`. */
|
|
175
36
|
declare const WORKBUDDY_APP_VERSION_FILENAME = ".workbuddy-ai-version.json";
|
|
@@ -260,7 +121,7 @@ declare const FALLBACK_CN_APP_VERSION = "5.5.6";
|
|
|
260
121
|
declare const CN_APP_VERSION_FILENAME = ".workbuddy-app-version.json";
|
|
261
122
|
/** The resolved identity a chat request presents as. */
|
|
262
123
|
interface ChatIdentity {
|
|
263
|
-
/** Desktop App version; drives
|
|
124
|
+
/** Desktop App version; drives the desktop UA and `X-IDE-Version`. */
|
|
264
125
|
clientVersion: string;
|
|
265
126
|
/** Bundled agent-CLI version; absent drops the `CLI/…` UA token. */
|
|
266
127
|
cliVersion?: string;
|
|
@@ -795,6 +656,161 @@ declare const AI_VARIANT: WorkBuddyVariant;
|
|
|
795
656
|
/** Look up a variant by provider id. */
|
|
796
657
|
declare function variantFor(id: string): WorkBuddyVariant | undefined;
|
|
797
658
|
//#endregion
|
|
659
|
+
//#region src/desktop-credential-protection.d.ts
|
|
660
|
+
/** The four states a desktop auth document can be read as. */
|
|
661
|
+
type DesktopAuthFormat = 'absent' | 'plaintext' | 'encrypted' | 'unrecognized';
|
|
662
|
+
/** The spawned helper. Separated from the provider so tests can stand it in. */
|
|
663
|
+
type WorkBuddyKeyPayloadSource = () => Promise<string>;
|
|
664
|
+
/**
|
|
665
|
+
* Which automatic discovery, if any, this provider may run when no explicit
|
|
666
|
+
* binary is configured.
|
|
667
|
+
*
|
|
668
|
+
* `none` is the safe default: a provider that has not been told which product
|
|
669
|
+
* it serves must not reach for another product's app. The CN line selects a
|
|
670
|
+
* platform-specific discovery only where that platform's app layout has been
|
|
671
|
+
* verified; Global remains `none`.
|
|
672
|
+
*/
|
|
673
|
+
type WorkBuddyElectronDiscovery = 'none' | 'macos-workbuddy' | 'windows-workbuddy';
|
|
674
|
+
/** Seams the discovery flow runs through, so tests never spawn a process. */
|
|
675
|
+
interface WorkBuddyDiscoveryTools {
|
|
676
|
+
/** Candidate `.app` bundles for the CN bundle id, or a throw for an unusable tool. */
|
|
677
|
+
findApps: (signal: AbortSignal) => Promise<readonly string[]>;
|
|
678
|
+
/**
|
|
679
|
+
* `CFBundleIdentifier` of a bundle, or `undefined` when the tool could not
|
|
680
|
+
* read it — which is "we could not check this candidate", never "it does not
|
|
681
|
+
* match". A successful read of a *different* id returns that id, and the
|
|
682
|
+
* caller excludes the candidate.
|
|
683
|
+
*/
|
|
684
|
+
bundleIdentifier: (bundlePath: string, signal: AbortSignal) => Promise<string | undefined>;
|
|
685
|
+
/** Display version, best effort; `undefined` when unavailable. */
|
|
686
|
+
bundleVersion: (bundlePath: string, signal: AbortSignal) => Promise<string | undefined>;
|
|
687
|
+
}
|
|
688
|
+
/** Windows-only seam for querying one uninstall registry root. */
|
|
689
|
+
interface WorkBuddyWindowsDiscoveryTools {
|
|
690
|
+
queryUninstallRoot: (root: string, signal: AbortSignal) => Promise<string>;
|
|
691
|
+
}
|
|
692
|
+
/** Provider options. */
|
|
693
|
+
interface WorkBuddyAtRestKeyProviderOptions {
|
|
694
|
+
/** Explicit Electron binary; overrides the platform default and env. */
|
|
695
|
+
electronPath?: string;
|
|
696
|
+
/** Helper timeout in milliseconds; default 10s. */
|
|
697
|
+
timeoutMs?: number;
|
|
698
|
+
/**
|
|
699
|
+
* Where the payload comes from. Defaults to spawning WorkBuddy's own
|
|
700
|
+
* Electron with `ELECTRON_RUN_AS_NODE=1`; tests supply a stand-in so no
|
|
701
|
+
* test ever touches the real binary or a real key. Supplying this replaces
|
|
702
|
+
* path *resolution* too, so tests about resolution use
|
|
703
|
+
* {@link spawnHelper} instead.
|
|
704
|
+
*/
|
|
705
|
+
source?: WorkBuddyKeyPayloadSource;
|
|
706
|
+
/**
|
|
707
|
+
* Runs the helper at the resolved path. Distinct from {@link source}, which
|
|
708
|
+
* replaces the whole payload path: this seam keeps resolution — explicit
|
|
709
|
+
* config, platform default, discovery — real, so tests can exercise it
|
|
710
|
+
* without spawning anything.
|
|
711
|
+
*/
|
|
712
|
+
spawnHelper?: (electronPath: string) => Promise<string>;
|
|
713
|
+
/**
|
|
714
|
+
* Automatic discovery budget; defaults to `'none'` (see
|
|
715
|
+
* {@link WorkBuddyElectronDiscovery}). Passed explicitly per variant at the
|
|
716
|
+
* composition root, never inferred from the environment.
|
|
717
|
+
*/
|
|
718
|
+
discovery?: WorkBuddyElectronDiscovery;
|
|
719
|
+
/**
|
|
720
|
+
* Platform default binary, consulted only when `discovery` is enabled and no
|
|
721
|
+
* explicit path is configured. Injectable so tests can force the fallback
|
|
722
|
+
* branch without moving the real app; `null` means "no default here".
|
|
723
|
+
*/
|
|
724
|
+
defaultElectronPath?: string | undefined;
|
|
725
|
+
/** Discovery subprocesses; injectable so tests never spawn. */
|
|
726
|
+
tools?: WorkBuddyDiscoveryTools;
|
|
727
|
+
/** Windows registry discovery subprocess; injectable so tests never spawn. */
|
|
728
|
+
windowsTools?: WorkBuddyWindowsDiscoveryTools;
|
|
729
|
+
/** Platform override for deterministic discovery tests. */
|
|
730
|
+
platform?: NodeJS.Platform;
|
|
731
|
+
/**
|
|
732
|
+
* Total budget for one discovery run, covering the search and every
|
|
733
|
+
* candidate check. Injectable so tests can exercise exhaustion without
|
|
734
|
+
* waiting out the production 10s.
|
|
735
|
+
*/
|
|
736
|
+
discoveryBudgetMs?: number;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* In-memory protector-key resolver: one spawn per key id, single-flight, never
|
|
740
|
+
* persisted. The cache is keyed by the id envelopes ask for, so an envelope
|
|
741
|
+
* sealed under a rotated key triggers exactly one fresh resolution.
|
|
742
|
+
*/ declare class WorkBuddyAtRestKeyProvider {
|
|
743
|
+
/**
|
|
744
|
+
* The explicit binary, when one was configured. `undefined` here means "the
|
|
745
|
+
* caller did not name one", which is what lets discovery run — an explicit
|
|
746
|
+
* path that turns out to be unusable is an error, never a reason to look for
|
|
747
|
+
* a different app.
|
|
748
|
+
*/
|
|
749
|
+
private readonly explicitPath;
|
|
750
|
+
private readonly defaultPath;
|
|
751
|
+
private readonly discovery;
|
|
752
|
+
private readonly tools;
|
|
753
|
+
private readonly windowsTools;
|
|
754
|
+
private readonly platform;
|
|
755
|
+
private readonly discoveryBudgetMs;
|
|
756
|
+
private readonly timeoutMs;
|
|
757
|
+
private readonly source;
|
|
758
|
+
private readonly spawnHelper;
|
|
759
|
+
/**
|
|
760
|
+
* The path discovery settled on, cached only on success. A failure leaves
|
|
761
|
+
* this unset so the next attempt tries again — the user may install or move
|
|
762
|
+
* the app without restarting DSH.
|
|
763
|
+
*/
|
|
764
|
+
private discoveredPath;
|
|
765
|
+
private cache;
|
|
766
|
+
private inflight;
|
|
767
|
+
constructor(options?: WorkBuddyAtRestKeyProviderOptions);
|
|
768
|
+
/**
|
|
769
|
+
* The binary the default helper would use, for diagnostics.
|
|
770
|
+
*
|
|
771
|
+
* Reports a *discovery result* once one exists, so diagnostics describe what
|
|
772
|
+
* would actually run rather than the default that was bypassed. Discovery
|
|
773
|
+
* itself stays in {@link resolveElectronPath}: this accessor never triggers a
|
|
774
|
+
* search (the constructor must remain I/O-free, and callers may ask before
|
|
775
|
+
* any resolution has happened).
|
|
776
|
+
*/
|
|
777
|
+
helperPath(): string | undefined;
|
|
778
|
+
/**
|
|
779
|
+
* A protector key matching one of the requested envelope key ids. The first
|
|
780
|
+
* id the cache answers wins; otherwise one spawn resolves the current key,
|
|
781
|
+
* which must match a request — a mismatch means the envelopes were sealed by
|
|
782
|
+
* a different install than the one this machine now runs, and no key we can
|
|
783
|
+
* reach will open them.
|
|
784
|
+
*/
|
|
785
|
+
protectorKeyFor(requested: readonly string[]): Promise<Buffer>;
|
|
786
|
+
private ingest;
|
|
787
|
+
/**
|
|
788
|
+
* The binary to spawn, or a diagnosable error saying why there is none.
|
|
789
|
+
*
|
|
790
|
+
* Order is the contract: an explicit path is used as-is and never falls back;
|
|
791
|
+
* discovery runs only for a provider that was configured for it, and only
|
|
792
|
+
* after the platform default has been tried and found unusable.
|
|
793
|
+
*/
|
|
794
|
+
private resolveElectronPath;
|
|
795
|
+
/**
|
|
796
|
+
* Resolve the CN app through Spotlight, then prove each candidate's identity
|
|
797
|
+
* before it can be executed.
|
|
798
|
+
*
|
|
799
|
+
* The whole flow shares one budget: a hang in one candidate must not extend
|
|
800
|
+
* the wait for the others, and running out of budget is reported as an
|
|
801
|
+
* unfinished check rather than an absent app.
|
|
802
|
+
*/
|
|
803
|
+
private discoverMacosApp;
|
|
804
|
+
/**
|
|
805
|
+
* Resolve the CN app through Windows uninstall records. Registry entries
|
|
806
|
+
* provide hints, not trust: every DisplayIcon candidate must still be a
|
|
807
|
+
* WorkBuddy Electron binary with the known Electron layout before execution.
|
|
808
|
+
*/
|
|
809
|
+
private discoverWindowsApp;
|
|
810
|
+
private spawnPayload;
|
|
811
|
+
private spawnAt;
|
|
812
|
+
}
|
|
813
|
+
//#endregion
|
|
798
814
|
//#region src/auth.d.ts
|
|
799
815
|
/** Normalized WorkBuddy credential, timestamps in epoch milliseconds. */
|
|
800
816
|
interface WorkBuddyCredential {
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as modelWithCurrentPromotion, B as FALLBACK_CN_APP_VERSION, C as desktopAuthCandidatesFor, D as electronDiscoveryFor, E as WorkBuddyAtRestKeyProvider, F as regionOf, G as validCliVersion, H as fallbackChatIdentity, I as PROBE_EFFORT_CANDIDATES, J as installedAppVersion, K as WORKBUDDY_APP_VERSION_FILENAME, L as probeModel, M as parseModelCatalog, N as prepareChatBody, O as WorkBuddyUpstreamClient, P as prepareInternationalChatBody, R as randomSentinel, S as defaultDesktopAuthPath, T as workbuddyOwnAuthPath, U as readCliVersion, V as chatUserAgent, W as resolveChatIdentity, X as resolveAppVersion, Y as readBundleVersion, Z as validAppVersion, _ as WorkBuddyCatalog, a as WORKBUDDY_HOST_HEARTBEAT_FILENAME, b as WorkBuddyCredentialStore, c as processStartTimeMs, d as writeHostHeartbeat, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, j as normalizeCredits, k as classifyUpstreamError, l as readHostHeartbeat, n as CN_VARIANT, o as clearHostHeartbeat, q as appUserAgent, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, t as AI_VARIANT, u as workbuddyHostHeartbeatPath, v as WORKBUDDY_AUTH_FILENAME, w as parseWorkBuddyAuth, x as defaultDesktopAuthCandidates, y as WORKBUDDY_AUTH_FILE_ENV, z as CN_APP_VERSION_FILENAME } from "./variants-D_oxWP_K.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { resolveImageAttachmentAccess, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -1766,7 +1766,7 @@ function apply(ctx, config) {
|
|
|
1766
1766
|
* case that must not fall back to a shared preference bucket.
|
|
1767
1767
|
*/
|
|
1768
1768
|
const lastAccounts = /* @__PURE__ */ new Map();
|
|
1769
|
-
const atRestKeysFor = (variant) => new WorkBuddyAtRestKeyProvider({ discovery: variant
|
|
1769
|
+
const atRestKeysFor = (variant) => new WorkBuddyAtRestKeyProvider({ discovery: electronDiscoveryFor(variant) });
|
|
1770
1770
|
const runtimes = WORKBUDDY_VARIANTS.map((variant) => createVariantRuntime(config, variant, () => current(), (id) => lastIdentities.get(id), (id) => lastAccounts.get(id), atRestKeysFor(variant)));
|
|
1771
1771
|
const probeKey = createProbeKey();
|
|
1772
1772
|
let setMaximumContextWindow;
|
|
@@ -5,7 +5,7 @@ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
|
5
5
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
6
6
|
import { createDecipheriv, createHash, randomBytes } from "node:crypto";
|
|
7
7
|
import { execFile, execFileSync } from "node:child_process";
|
|
8
|
-
import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
8
|
+
import { accessSync, constants, readFileSync, realpathSync, statSync } from "node:fs";
|
|
9
9
|
//#region src/app-version.ts
|
|
10
10
|
/**
|
|
11
11
|
* The international desktop app's version, used as the `/v3/config` UA.
|
|
@@ -18,9 +18,10 @@ import { accessSync, constants, realpathSync, statSync } from "node:fs";
|
|
|
18
18
|
* removed — returns the 21-model App document. The UA is therefore built from
|
|
19
19
|
* the form verified in code, not from the earlier prose.
|
|
20
20
|
*
|
|
21
|
-
* The version
|
|
22
|
-
* plist, or a bad cached value
|
|
23
|
-
* a compiled-in constant, and
|
|
21
|
+
* The version feeds the international catalog UA and the chat identity's
|
|
22
|
+
* `X-IDE-Version`: a missing App, an unreadable plist, or a bad cached value
|
|
23
|
+
* degrades to the last saved value and finally to a compiled-in constant, and
|
|
24
|
+
* never blocks credential use or the provider.
|
|
24
25
|
*
|
|
25
26
|
* @module dsh-workbuddy-connect/app-version
|
|
26
27
|
*/
|
|
@@ -665,18 +666,21 @@ function commonHeaders(credential) {
|
|
|
665
666
|
/**
|
|
666
667
|
* Chat request headers, including the X-No-* conventions the official CLI uses.
|
|
667
668
|
*
|
|
668
|
-
* `userAgent`
|
|
669
|
-
*
|
|
670
|
-
*
|
|
669
|
+
* `userAgent` and `clientVersion` carry the same resolved desktop identity for
|
|
670
|
+
* chat and probe requests. Refresh shares `commonHeaders` but never this
|
|
671
|
+
* override, so the two paths cannot drift into each other.
|
|
671
672
|
*/
|
|
672
|
-
function chatHeaders(credential, userAgent) {
|
|
673
|
+
function chatHeaders(credential, userAgent, clientVersion) {
|
|
673
674
|
return {
|
|
674
675
|
...commonHeaders(credential),
|
|
675
|
-
|
|
676
|
+
"User-Agent": userAgent,
|
|
676
677
|
"Content-Type": "application/json",
|
|
677
678
|
...credential.uid === "" ? { "X-No-User-Id": "1" } : { "X-User-Id": credential.uid },
|
|
678
679
|
...credential.enterpriseId === void 0 || credential.enterpriseId === "" ? { "X-No-Enterprise-Id": "1" } : { "X-Enterprise-Id": credential.enterpriseId },
|
|
679
680
|
...credential.domain === "" ? { "X-No-Department-Info": "1" } : { "X-Domain": credential.domain },
|
|
681
|
+
"X-IDE-Type": "WorkBuddy",
|
|
682
|
+
"X-IDE-Name": "WorkBuddy",
|
|
683
|
+
"X-IDE-Version": clientVersion,
|
|
680
684
|
"X-Product": "SaaS"
|
|
681
685
|
};
|
|
682
686
|
}
|
|
@@ -821,18 +825,21 @@ var WorkBuddyUpstreamClient = class {
|
|
|
821
825
|
/** POST the chat endpoint; a successful answer is the raw SSE response. */
|
|
822
826
|
async chatStream(credential, bodyJson, signal) {
|
|
823
827
|
const region = regionOf(credential.domain);
|
|
828
|
+
let identity;
|
|
824
829
|
let userAgent;
|
|
825
830
|
try {
|
|
826
|
-
|
|
831
|
+
identity = await this.resolveChatIdentity(region);
|
|
832
|
+
userAgent = chatUserAgent(identity, region);
|
|
827
833
|
} catch {
|
|
828
|
-
|
|
834
|
+
identity = fallbackChatIdentity(region);
|
|
835
|
+
userAgent = chatUserAgent(identity, region);
|
|
829
836
|
}
|
|
830
837
|
let response;
|
|
831
838
|
try {
|
|
832
839
|
response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {
|
|
833
840
|
method: "POST",
|
|
834
841
|
headers: {
|
|
835
|
-
...chatHeaders(credential, userAgent),
|
|
842
|
+
...chatHeaders(credential, userAgent, identity.clientVersion),
|
|
836
843
|
"Authorization": `Bearer ${credential.accessToken}`
|
|
837
844
|
},
|
|
838
845
|
body: region === "global" ? prepareInternationalChatBody(bodyJson) : bodyJson,
|
|
@@ -1144,11 +1151,14 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1144
1151
|
*/
|
|
1145
1152
|
async probeEffort(credential, model, effort, signal) {
|
|
1146
1153
|
const international = regionOf(credential.domain) === "global";
|
|
1154
|
+
let identity;
|
|
1147
1155
|
let userAgent;
|
|
1148
1156
|
try {
|
|
1149
|
-
|
|
1157
|
+
identity = await this.resolveChatIdentity(international ? "global" : "cn");
|
|
1158
|
+
userAgent = chatUserAgent(identity, international ? "global" : "cn");
|
|
1150
1159
|
} catch {
|
|
1151
|
-
|
|
1160
|
+
identity = fallbackChatIdentity(international ? "global" : "cn");
|
|
1161
|
+
userAgent = chatUserAgent(identity, international ? "global" : "cn");
|
|
1152
1162
|
}
|
|
1153
1163
|
const payload = {
|
|
1154
1164
|
model,
|
|
@@ -1168,7 +1178,7 @@ var WorkBuddyUpstreamClient = class {
|
|
|
1168
1178
|
response = await fetch(`${chatBase(credential)}/v2/chat/completions`, {
|
|
1169
1179
|
method: "POST",
|
|
1170
1180
|
headers: {
|
|
1171
|
-
...chatHeaders(credential, userAgent),
|
|
1181
|
+
...chatHeaders(credential, userAgent, identity.clientVersion),
|
|
1172
1182
|
"Authorization": `Bearer ${credential.accessToken}`
|
|
1173
1183
|
},
|
|
1174
1184
|
body: JSON.stringify(payload),
|
|
@@ -1471,16 +1481,19 @@ const INTERNATIONAL_PROBE_MAX_TOKENS = 16;
|
|
|
1471
1481
|
*/
|
|
1472
1482
|
/** Env variable that overrides the WorkBuddy Electron binary used as the key helper. */
|
|
1473
1483
|
const WORKBUDDY_ELECTRON_BIN_ENV = "WORKBUDDY_ELECTRON_BIN";
|
|
1474
|
-
/** Platform-default Electron
|
|
1484
|
+
/** Platform-default Electron binaries confirmed for the CN app (5.6.2). */
|
|
1475
1485
|
const MACOS_ELECTRON_PATH = "/Applications/WorkBuddy.app/Contents/MacOS/Electron";
|
|
1476
1486
|
/**
|
|
1477
1487
|
* The Electron binary the helper would spawn on this platform, or `undefined`
|
|
1478
|
-
* where no default has been verified.
|
|
1479
|
-
*
|
|
1480
|
-
*
|
|
1488
|
+
* where no default has been verified. Windows requires `LOCALAPPDATA` to be
|
|
1489
|
+
* present; platforms without a verified layout must set
|
|
1490
|
+
* {@link WORKBUDDY_ELECTRON_BIN_ENV} explicitly.
|
|
1481
1491
|
*/
|
|
1482
|
-
function defaultWorkBuddyElectronPath() {
|
|
1483
|
-
|
|
1492
|
+
function defaultWorkBuddyElectronPath(platform = process.platform) {
|
|
1493
|
+
if (platform === "darwin") return MACOS_ELECTRON_PATH;
|
|
1494
|
+
if (platform !== "win32") return void 0;
|
|
1495
|
+
const localAppData = process.env.LOCALAPPDATA?.trim();
|
|
1496
|
+
return localAppData === void 0 || localAppData === "" ? void 0 : join(localAppData, "Programs", "WorkBuddy", "WorkBuddy.exe");
|
|
1484
1497
|
}
|
|
1485
1498
|
/** Distinct key ids across the wrapped fields, in field order. */
|
|
1486
1499
|
function keyIdsOf(fields) {
|
|
@@ -1661,11 +1674,26 @@ function parseAtRestPayload(text) {
|
|
|
1661
1674
|
function deriveProtectorKey(secret) {
|
|
1662
1675
|
return createHash("sha256").update(secret, "utf8").digest();
|
|
1663
1676
|
}
|
|
1677
|
+
/** Select discovery from product and platform at the composition root. */
|
|
1678
|
+
function electronDiscoveryFor(variant, platform = process.platform) {
|
|
1679
|
+
if (variant.region !== "cn") return "none";
|
|
1680
|
+
if (platform === "darwin") return "macos-workbuddy";
|
|
1681
|
+
if (platform === "win32") return "windows-workbuddy";
|
|
1682
|
+
return "none";
|
|
1683
|
+
}
|
|
1664
1684
|
/** The CN app's bundle id; the only one this round resolves by discovery. */
|
|
1665
1685
|
const WORKBUDDY_CN_BUNDLE_ID = "com.tencent.workbuddy.mac";
|
|
1666
1686
|
/** Absolute tool paths: never resolved through PATH, which a user can change. */
|
|
1667
1687
|
const MDFIND_BIN = "/usr/bin/mdfind";
|
|
1668
1688
|
const PLUTIL_BIN = "/usr/bin/plutil";
|
|
1689
|
+
const WINDOWS_REGISTRY_ROOTS = [
|
|
1690
|
+
"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
1691
|
+
"HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
|
1692
|
+
"HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
|
|
1693
|
+
];
|
|
1694
|
+
const WINDOWS_REGISTRY_OUTPUT_MAX_BYTES = 1048576;
|
|
1695
|
+
const WINDOWS_ELECTRON_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/u;
|
|
1696
|
+
const WINDOWS_WORKBUDDY_DISPLAY_NAME_PATTERN = /^WorkBuddy(?:\s+\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.-]+)?)?$/u;
|
|
1669
1697
|
/** One discovery subprocess's own limits; see {@link WorkBuddyAtRestKeyProviderOptions}. */
|
|
1670
1698
|
const WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS = 3e3;
|
|
1671
1699
|
const MDFIND_MAX_OUTPUT_BYTES = 1048576;
|
|
@@ -1749,6 +1777,58 @@ function workBuddyDiscoveryTools() {
|
|
|
1749
1777
|
};
|
|
1750
1778
|
}
|
|
1751
1779
|
/**
|
|
1780
|
+
* The Windows registry discovery tool. It is deliberately separate from the
|
|
1781
|
+
* macOS Spotlight/plutil seam: the two platforms have different identity and
|
|
1782
|
+
* candidate rules, and neither tool should accidentally become cross-platform.
|
|
1783
|
+
*/
|
|
1784
|
+
function workBuddyWindowsDiscoveryTools() {
|
|
1785
|
+
const systemRoot = process.env.SystemRoot?.trim();
|
|
1786
|
+
const regPath = systemRoot === void 0 || systemRoot === "" ? void 0 : join(systemRoot, "System32", "reg.exe");
|
|
1787
|
+
const runTool = (root, signal) => new Promise((resolve, reject) => {
|
|
1788
|
+
if (regPath === void 0) {
|
|
1789
|
+
reject(new DiscoveryIncompleteError("SystemRoot is not configured"));
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
if (signal.aborted) {
|
|
1793
|
+
reject(new DiscoveryIncompleteError("reg.exe was not started: the discovery budget was already spent"));
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
let settled = false;
|
|
1797
|
+
const child = execFile(regPath, [
|
|
1798
|
+
"query",
|
|
1799
|
+
root,
|
|
1800
|
+
"/s"
|
|
1801
|
+
], {
|
|
1802
|
+
maxBuffer: WINDOWS_REGISTRY_OUTPUT_MAX_BYTES,
|
|
1803
|
+
timeout: WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS,
|
|
1804
|
+
windowsHide: true
|
|
1805
|
+
}, (error, stdout, stderr) => {
|
|
1806
|
+
if (settled) return;
|
|
1807
|
+
settled = true;
|
|
1808
|
+
if (error !== null && error !== void 0) {
|
|
1809
|
+
if (error.killed !== true && (error.code === 1 || error.code === "1") && windowsRegistryKeyMissing(stderr)) {
|
|
1810
|
+
resolve("");
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
reject(new DiscoveryIncompleteError(`reg.exe could not complete (${error.killed === true ? "timed out" : String(error.code ?? "unavailable")})`));
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
resolve(stdout);
|
|
1817
|
+
});
|
|
1818
|
+
const abort = () => {
|
|
1819
|
+
if (settled) return;
|
|
1820
|
+
settled = true;
|
|
1821
|
+
child.kill();
|
|
1822
|
+
reject(new DiscoveryIncompleteError("reg.exe was abandoned: the discovery budget was spent"));
|
|
1823
|
+
};
|
|
1824
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1825
|
+
child.on("close", () => {
|
|
1826
|
+
signal.removeEventListener("abort", abort);
|
|
1827
|
+
});
|
|
1828
|
+
});
|
|
1829
|
+
return { queryUninstallRoot: runTool };
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1752
1832
|
* In-memory protector-key resolver: one spawn per key id, single-flight, never
|
|
1753
1833
|
* persisted. The cache is keyed by the id envelopes ask for, so an envelope
|
|
1754
1834
|
* sealed under a rotated key triggers exactly one fresh resolution.
|
|
@@ -1763,6 +1843,8 @@ function workBuddyDiscoveryTools() {
|
|
|
1763
1843
|
defaultPath;
|
|
1764
1844
|
discovery;
|
|
1765
1845
|
tools;
|
|
1846
|
+
windowsTools;
|
|
1847
|
+
platform;
|
|
1766
1848
|
discoveryBudgetMs;
|
|
1767
1849
|
timeoutMs;
|
|
1768
1850
|
source;
|
|
@@ -1780,8 +1862,10 @@ function workBuddyDiscoveryTools() {
|
|
|
1780
1862
|
const envPath = fromEnv === void 0 || fromEnv === "" ? void 0 : fromEnv;
|
|
1781
1863
|
this.explicitPath = options.electronPath ?? envPath;
|
|
1782
1864
|
this.discovery = options.discovery ?? "none";
|
|
1783
|
-
this.
|
|
1865
|
+
this.platform = options.platform ?? process.platform;
|
|
1866
|
+
this.defaultPath = this.discovery === "none" ? void 0 : options.defaultElectronPath === void 0 ? defaultWorkBuddyElectronPath(this.platform) : options.defaultElectronPath ?? void 0;
|
|
1784
1867
|
this.tools = options.tools ?? workBuddyDiscoveryTools();
|
|
1868
|
+
this.windowsTools = options.windowsTools ?? workBuddyWindowsDiscoveryTools();
|
|
1785
1869
|
this.discoveryBudgetMs = options.discoveryBudgetMs ?? 1e4;
|
|
1786
1870
|
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
1787
1871
|
this.spawnHelper = options.spawnHelper ?? ((path) => this.spawnAt(path));
|
|
@@ -1848,7 +1932,7 @@ function workBuddyDiscoveryTools() {
|
|
|
1848
1932
|
if (isExecutable(this.discoveredPath)) return this.discoveredPath;
|
|
1849
1933
|
this.discoveredPath = void 0;
|
|
1850
1934
|
}
|
|
1851
|
-
const found = await this.discoverMacosApp();
|
|
1935
|
+
const found = this.discovery === "macos-workbuddy" ? await this.discoverMacosApp() : await this.discoverWindowsApp();
|
|
1852
1936
|
this.discoveredPath = found;
|
|
1853
1937
|
return found;
|
|
1854
1938
|
}
|
|
@@ -1861,7 +1945,7 @@ function workBuddyDiscoveryTools() {
|
|
|
1861
1945
|
* unfinished check rather than an absent app.
|
|
1862
1946
|
*/
|
|
1863
1947
|
async discoverMacosApp() {
|
|
1864
|
-
if (
|
|
1948
|
+
if (this.platform !== "darwin") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no WorkBuddy Electron binary is configured for this platform; set ${WORKBUDDY_ELECTRON_BIN_ENV} to the app's Electron binary`);
|
|
1865
1949
|
const controller = new AbortController();
|
|
1866
1950
|
const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
|
|
1867
1951
|
try {
|
|
@@ -1923,6 +2007,49 @@ function workBuddyDiscoveryTools() {
|
|
|
1923
2007
|
controller.abort();
|
|
1924
2008
|
}
|
|
1925
2009
|
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Resolve the CN app through Windows uninstall records. Registry entries
|
|
2012
|
+
* provide hints, not trust: every DisplayIcon candidate must still be a
|
|
2013
|
+
* WorkBuddy Electron binary with the known Electron layout before execution.
|
|
2014
|
+
*/
|
|
2015
|
+
async discoverWindowsApp() {
|
|
2016
|
+
if (this.platform !== "win32") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no WorkBuddy Electron binary is configured for this platform; set ${WORKBUDDY_ELECTRON_BIN_ENV} to the app's Electron binary`);
|
|
2017
|
+
const controller = new AbortController();
|
|
2018
|
+
const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
|
|
2019
|
+
try {
|
|
2020
|
+
const candidates = [];
|
|
2021
|
+
let unresolved = false;
|
|
2022
|
+
for (const root of WINDOWS_REGISTRY_ROOTS) {
|
|
2023
|
+
let output;
|
|
2024
|
+
try {
|
|
2025
|
+
output = await this.windowsTools.queryUninstallRoot(root, controller.signal);
|
|
2026
|
+
} catch {
|
|
2027
|
+
unresolved = true;
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
const parsed = parseWindowsRegistryOutput(output);
|
|
2031
|
+
candidates.push(...parsed.candidates);
|
|
2032
|
+
unresolved ||= parsed.incomplete;
|
|
2033
|
+
}
|
|
2034
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2035
|
+
for (const candidate of new Set(candidates)) {
|
|
2036
|
+
const inspection = inspectWindowsElectronCandidate(candidate, this.platform);
|
|
2037
|
+
if (inspection === "unresolved") {
|
|
2038
|
+
unresolved = true;
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
2041
|
+
if (inspection === void 0) continue;
|
|
2042
|
+
seen.set(inspection.identity, inspection.electronPath);
|
|
2043
|
+
}
|
|
2044
|
+
if (unresolved) throw discoveryIncomplete("some registry entries or candidates could not be checked");
|
|
2045
|
+
if (seen.size > 1) throw new WorkBuddyElectronPathError("electron-binary-ambiguous", `more than one WorkBuddy application was found, so none was chosen:\n${[...seen.values()].map((path) => ` - ${path}`).join("\n")}\n set ${WORKBUDDY_ELECTRON_BIN_ENV} to the one to use`);
|
|
2046
|
+
if (seen.size === 0) throw new WorkBuddyElectronPathError("electron-binary-not-found", "no usable WorkBuddy Electron binary was found in the default location or Windows uninstall records");
|
|
2047
|
+
return [...seen.values()][0];
|
|
2048
|
+
} finally {
|
|
2049
|
+
clearTimeout(timer);
|
|
2050
|
+
controller.abort();
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
1926
2053
|
async spawnPayload() {
|
|
1927
2054
|
return await this.spawnHelper(await this.resolveElectronPath());
|
|
1928
2055
|
}
|
|
@@ -1951,6 +2078,107 @@ function workBuddyDiscoveryTools() {
|
|
|
1951
2078
|
});
|
|
1952
2079
|
}
|
|
1953
2080
|
};
|
|
2081
|
+
/** Only known missing-key diagnostics can safely make a failed query empty. */
|
|
2082
|
+
function windowsRegistryKeyMissing(stderr) {
|
|
2083
|
+
const detail = stderr.trim();
|
|
2084
|
+
return /^ERROR:\s*The system was unable to find the specified registry key or value\.?$/iu.test(detail) || /^错误[::]\s*系统找不到指定的注册表项或值[。.]?$/u.test(detail);
|
|
2085
|
+
}
|
|
2086
|
+
/** Parse the value columns emitted by `reg query ... /s`. */
|
|
2087
|
+
function parseWindowsRegistryOutput(output) {
|
|
2088
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2089
|
+
let currentKey;
|
|
2090
|
+
for (const line of output.split(/\r?\n/u)) {
|
|
2091
|
+
const keyMatch = /^\s*(HKEY_[^\r\n]+?)\s*$/iu.exec(line);
|
|
2092
|
+
if (keyMatch !== null) {
|
|
2093
|
+
currentKey = keyMatch[1];
|
|
2094
|
+
entries.set(currentKey, {});
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2097
|
+
if (currentKey === void 0) continue;
|
|
2098
|
+
const valueMatch = /^\s+(DisplayName|DisplayIcon)\s+REG_[A-Z0-9_]+\s*(.*?)\s*$/iu.exec(line);
|
|
2099
|
+
if (valueMatch === null) continue;
|
|
2100
|
+
const entry = entries.get(currentKey);
|
|
2101
|
+
if (entry === void 0) continue;
|
|
2102
|
+
const value = valueMatch[2] ?? "";
|
|
2103
|
+
if (valueMatch[1].toLowerCase() === "displayname") entry.displayName = value;
|
|
2104
|
+
else entry.displayIcon = value;
|
|
2105
|
+
}
|
|
2106
|
+
const candidates = [];
|
|
2107
|
+
let incomplete = false;
|
|
2108
|
+
for (const entry of entries.values()) {
|
|
2109
|
+
if (entry.displayName === void 0) continue;
|
|
2110
|
+
if (!WINDOWS_WORKBUDDY_DISPLAY_NAME_PATTERN.test(entry.displayName.trim())) continue;
|
|
2111
|
+
const displayIcon = entry.displayIcon === void 0 ? void 0 : parseWindowsDisplayIcon(entry.displayIcon);
|
|
2112
|
+
if (displayIcon === void 0) incomplete = true;
|
|
2113
|
+
else candidates.push(displayIcon);
|
|
2114
|
+
}
|
|
2115
|
+
return {
|
|
2116
|
+
candidates,
|
|
2117
|
+
incomplete
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
/** Read a quoted DisplayIcon path and remove the Windows icon-index suffix. */
|
|
2121
|
+
function parseWindowsDisplayIcon(value) {
|
|
2122
|
+
const raw = value.trim();
|
|
2123
|
+
let path;
|
|
2124
|
+
if (raw.startsWith("\"")) {
|
|
2125
|
+
const closingQuote = raw.indexOf("\"", 1);
|
|
2126
|
+
if (closingQuote < 0) return void 0;
|
|
2127
|
+
const suffix = raw.slice(closingQuote + 1).trim();
|
|
2128
|
+
if (suffix !== "" && !/^,\d+$/u.test(suffix)) return void 0;
|
|
2129
|
+
path = raw.slice(1, closingQuote).replace(/,\d+$/u, "");
|
|
2130
|
+
} else {
|
|
2131
|
+
const match = /^(.+?\.exe)(?:,\d+)?$/iu.exec(raw);
|
|
2132
|
+
if (match === null) return void 0;
|
|
2133
|
+
path = match[1];
|
|
2134
|
+
}
|
|
2135
|
+
path = path.trim();
|
|
2136
|
+
return /\.exe$/iu.test(path) ? path : void 0;
|
|
2137
|
+
}
|
|
2138
|
+
/**
|
|
2139
|
+
* Validate the known WorkBuddy Windows layout. `undefined` is a decidable
|
|
2140
|
+
* exclusion; `unresolved` is reserved for errors that prevent checking.
|
|
2141
|
+
*/
|
|
2142
|
+
function inspectWindowsElectronCandidate(electronPath, platform) {
|
|
2143
|
+
if (platform !== "win32" || basename(electronPath).toLowerCase() !== "workbuddy.exe") return void 0;
|
|
2144
|
+
let binaryStat;
|
|
2145
|
+
try {
|
|
2146
|
+
binaryStat = statSync(electronPath);
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
return isENOENT$1(error) ? void 0 : "unresolved";
|
|
2149
|
+
}
|
|
2150
|
+
if (!binaryStat.isFile()) return void 0;
|
|
2151
|
+
try {
|
|
2152
|
+
accessSync(electronPath, constants.X_OK);
|
|
2153
|
+
} catch (error) {
|
|
2154
|
+
return isENOENT$1(error) ? void 0 : "unresolved";
|
|
2155
|
+
}
|
|
2156
|
+
const installRoot = dirname(electronPath);
|
|
2157
|
+
let version;
|
|
2158
|
+
try {
|
|
2159
|
+
version = readFileSync(join(installRoot, "version"), "utf8").trim();
|
|
2160
|
+
} catch (error) {
|
|
2161
|
+
return isENOENT$1(error) ? void 0 : "unresolved";
|
|
2162
|
+
}
|
|
2163
|
+
if (!WINDOWS_ELECTRON_VERSION_PATTERN.test(version)) return void 0;
|
|
2164
|
+
let resourcesStat;
|
|
2165
|
+
try {
|
|
2166
|
+
resourcesStat = statSync(join(installRoot, "resources", "app.asar"));
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
return isENOENT$1(error) ? void 0 : "unresolved";
|
|
2169
|
+
}
|
|
2170
|
+
if (!resourcesStat.isFile()) return void 0;
|
|
2171
|
+
let identity;
|
|
2172
|
+
try {
|
|
2173
|
+
identity = realpathSync(electronPath);
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
return isENOENT$1(error) ? void 0 : "unresolved";
|
|
2176
|
+
}
|
|
2177
|
+
return {
|
|
2178
|
+
electronPath,
|
|
2179
|
+
identity: platform === "win32" ? identity.toLowerCase() : identity
|
|
2180
|
+
};
|
|
2181
|
+
}
|
|
1954
2182
|
/** Whether a path exists and is executable; never throws. */
|
|
1955
2183
|
function isExecutable(path) {
|
|
1956
2184
|
try {
|
|
@@ -3229,7 +3457,7 @@ const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
|
|
|
3229
3457
|
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
|
|
3230
3458
|
//#endregion
|
|
3231
3459
|
//#region src/version.ts
|
|
3232
|
-
const WORKBUDDY_CONNECT_VERSION = "0.6.
|
|
3460
|
+
const WORKBUDDY_CONNECT_VERSION = "0.6.3";
|
|
3233
3461
|
//#endregion
|
|
3234
3462
|
//#region src/host-heartbeat.ts
|
|
3235
3463
|
/**
|
|
@@ -3454,4 +3682,4 @@ function variantFor(id) {
|
|
|
3454
3682
|
return WORKBUDDY_VARIANTS.find((variant) => variant.id === id);
|
|
3455
3683
|
}
|
|
3456
3684
|
//#endregion
|
|
3457
|
-
export {
|
|
3685
|
+
export { modelWithCurrentPromotion as A, FALLBACK_CN_APP_VERSION as B, desktopAuthCandidatesFor as C, electronDiscoveryFor as D, WorkBuddyAtRestKeyProvider as E, regionOf as F, validCliVersion as G, fallbackChatIdentity as H, PROBE_EFFORT_CANDIDATES as I, installedAppVersion as J, WORKBUDDY_APP_VERSION_FILENAME as K, probeModel as L, parseModelCatalog as M, prepareChatBody as N, WorkBuddyUpstreamClient as O, prepareInternationalChatBody as P, randomSentinel as R, defaultDesktopAuthPath as S, workbuddyOwnAuthPath as T, readCliVersion as U, chatUserAgent as V, resolveChatIdentity as W, resolveAppVersion as X, readBundleVersion as Y, validAppVersion as Z, WorkBuddyCatalog as _, WORKBUDDY_HOST_HEARTBEAT_FILENAME as a, WorkBuddyCredentialStore as b, processStartTimeMs as c, writeHostHeartbeat as d, WORKBUDDY_CONNECT_VERSION as f, FALLBACK_WORKBUDDY_MODELS as g, FALLBACK_WORKBUDDY_AI_MODELS as h, variantFor as i, normalizeCredits as j, classifyUpstreamError as k, readHostHeartbeat as l, WORKBUDDY_STATUS_PATH as m, CN_VARIANT as n, clearHostHeartbeat as o, WORKBUDDY_PROBE_PATH as p, appUserAgent as q, WORKBUDDY_VARIANTS as r, isHeartbeatProcessAlive as s, AI_VARIANT as t, workbuddyHostHeartbeatPath as u, WORKBUDDY_AUTH_FILENAME as v, parseWorkBuddyAuth as w, defaultDesktopAuthCandidates as x, WORKBUDDY_AUTH_FILE_ENV as y, CN_APP_VERSION_FILENAME as z };
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dsh-workbuddy-connect",
|
|
3
3
|
"displayName": "DSH WorkBuddy Connect",
|
|
4
4
|
"description": "将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness — bring WorkBuddy desktop app models into DeepSeek Harness with zero configuration.",
|
|
5
|
-
"version": "0.6.
|
|
5
|
+
"version": "0.6.3",
|
|
6
6
|
"author": "corrinehu",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh-plugin",
|