dsh-workbuddy-connect 0.6.0 → 0.6.1
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/bin.js +12 -3
- package/lib/client.js +230 -8
- package/lib/index.d.ts +133 -30
- package/lib/index.js +5 -4
- package/lib/{variants-CnrmSn0Q.js → variants-Cdp_ii6z.js} +331 -34
- package/package.json +1 -1
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { D as WorkBuddyUpstreamClient, Y 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-
|
|
2
|
+
import { D as WorkBuddyUpstreamClient, E as WorkBuddyAtRestKeyProvider, Y 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-Cdp_ii6z.js";
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
//#region src/bin.ts
|
|
@@ -26,12 +26,21 @@ function printHelp() {
|
|
|
26
26
|
function printJson(value) {
|
|
27
27
|
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
28
28
|
}
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* One variant's store plus the client that performs its refreshes.
|
|
31
|
+
*
|
|
32
|
+
* The key provider is injected explicitly, and per variant, for the same
|
|
33
|
+
* reason the plugin host does it: discovery is CN/macOS-only, so the provider
|
|
34
|
+
* must not be left to its no-arg default — that default is deliberately
|
|
35
|
+
* `discovery: 'none'`, and relying on it here would quietly strip the CN CLI
|
|
36
|
+
* of the decryption it has always had.
|
|
37
|
+
*/
|
|
30
38
|
function makeStore(variant) {
|
|
31
39
|
const client = new WorkBuddyUpstreamClient();
|
|
32
40
|
return new WorkBuddyCredentialStore({
|
|
33
41
|
variant,
|
|
34
|
-
refresh: (credential) => client.refreshToken(credential)
|
|
42
|
+
refresh: (credential) => client.refreshToken(credential),
|
|
43
|
+
keyProvider: new WorkBuddyAtRestKeyProvider({ discovery: variant.id === CN_VARIANT.id ? "macos-workbuddy" : "none" })
|
|
35
44
|
});
|
|
36
45
|
}
|
|
37
46
|
/** The plugin-owned credential copy for one variant, for display. */
|
package/lib/client.js
CHANGED
|
@@ -30,6 +30,21 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
*/
|
|
31
31
|
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
|
|
32
32
|
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
|
|
33
|
+
/** Every reason code, for validation without trusting a wire value. */
|
|
34
|
+
const SIGNED_OUT_REASON_CODES = [
|
|
35
|
+
"no-credential",
|
|
36
|
+
"credential-region-mismatch",
|
|
37
|
+
"encrypted-credential-unreadable",
|
|
38
|
+
"electron-binary-not-found",
|
|
39
|
+
"electron-binary-ambiguous",
|
|
40
|
+
"electron-binary-unavailable",
|
|
41
|
+
"electron-path-invalid",
|
|
42
|
+
"electron-discovery-incomplete"
|
|
43
|
+
];
|
|
44
|
+
/** Whether a value is one of the closed set of signed-out reason codes. */
|
|
45
|
+
function isWorkBuddySignedOutReasonCode(value) {
|
|
46
|
+
return typeof value === "string" && SIGNED_OUT_REASON_CODES.includes(value);
|
|
47
|
+
}
|
|
33
48
|
//#endregion
|
|
34
49
|
//#region src/client/status-document.ts
|
|
35
50
|
/**
|
|
@@ -43,6 +58,11 @@ window.__ModuleLoader__.load({
|
|
|
43
58
|
* The check is deliberately limited to the discriminator (plus `error`'s
|
|
44
59
|
* `message`, which the error paragraph renders): validating optional fields
|
|
45
60
|
* here would reject documents the host legitimately omits fields from.
|
|
61
|
+
*
|
|
62
|
+
* `reasonCode` is therefore *not* rejected here — a card renders `reason`
|
|
63
|
+
* either way — but every reader must narrow it with
|
|
64
|
+
* `isWorkBuddySignedOutReasonCode` before branching on it, since the wire
|
|
65
|
+
* value is not guaranteed to be inside the enum.
|
|
46
66
|
*/
|
|
47
67
|
function isWorkBuddyWebStatus(value) {
|
|
48
68
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
@@ -67,7 +87,9 @@ window.__ModuleLoader__.load({
|
|
|
67
87
|
introKey: "intro",
|
|
68
88
|
signedOutKey: "signedOutHint",
|
|
69
89
|
statusPath: WORKBUDDY_STATUS_PATH,
|
|
70
|
-
probePath: WORKBUDDY_PROBE_PATH
|
|
90
|
+
probePath: WORKBUDDY_PROBE_PATH,
|
|
91
|
+
appName: "WorkBuddy",
|
|
92
|
+
unavailableKey: "assistUnavailableCN"
|
|
71
93
|
};
|
|
72
94
|
/** International WorkBuddy AI. */
|
|
73
95
|
const AI_CARD_VARIANT = {
|
|
@@ -76,11 +98,119 @@ window.__ModuleLoader__.load({
|
|
|
76
98
|
introKey: "introAI",
|
|
77
99
|
signedOutKey: "signedOutHintAI",
|
|
78
100
|
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
79
|
-
probePath: WORKBUDDY_AI_PROBE_PATH
|
|
101
|
+
probePath: WORKBUDDY_AI_PROBE_PATH,
|
|
102
|
+
appName: "WorkBuddy AI",
|
|
103
|
+
unavailableKey: "assistUnavailableAI"
|
|
80
104
|
};
|
|
81
105
|
/** Both cards, in display order. */
|
|
82
106
|
const CARD_VARIANTS = [CN_CARD_VARIANT, AI_CARD_VARIANT];
|
|
83
107
|
const POLL_INTERVAL_MS = 6e4;
|
|
108
|
+
/**
|
|
109
|
+
* The reason codes whose failures the Agent assist block covers: the plugin
|
|
110
|
+
* cannot reach a decryption program, for any of the five reasons in §5.5.
|
|
111
|
+
*
|
|
112
|
+
* This is the *only* place the card decides whether the block applies. It
|
|
113
|
+
* branches on the code, never on `reason` text: the prose is written for a
|
|
114
|
+
* human and is expected to change, so matching it would silently stop
|
|
115
|
+
* matching after any wording edit.
|
|
116
|
+
*
|
|
117
|
+
* `encrypted-credential-unreadable` is deliberately absent — the app was found
|
|
118
|
+
* and ran, so "look for the app" is not the fix for it.
|
|
119
|
+
*/
|
|
120
|
+
const ASSIST_REASON_CODES = [
|
|
121
|
+
"electron-binary-not-found",
|
|
122
|
+
"electron-binary-ambiguous",
|
|
123
|
+
"electron-binary-unavailable",
|
|
124
|
+
"electron-path-invalid",
|
|
125
|
+
"electron-discovery-incomplete"
|
|
126
|
+
];
|
|
127
|
+
/** Locale key for one failure's summary inside the Agent prompt. */
|
|
128
|
+
function assistSummaryKey(code, variant) {
|
|
129
|
+
switch (code) {
|
|
130
|
+
case "electron-binary-not-found": return "assistNotFound";
|
|
131
|
+
case "electron-binary-ambiguous": return "assistAmbiguous";
|
|
132
|
+
case "electron-discovery-incomplete": return "assistIncomplete";
|
|
133
|
+
case "electron-path-invalid": return "assistPathInvalid";
|
|
134
|
+
default: return variant.unavailableKey;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Copy text to the clipboard, reporting whether it worked. */
|
|
138
|
+
async function copyPrompt(text) {
|
|
139
|
+
try {
|
|
140
|
+
if (navigator.clipboard?.writeText === void 0) return false;
|
|
141
|
+
await navigator.clipboard.writeText(text);
|
|
142
|
+
return true;
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The Agent assist block for a path failure: what is wrong, one copyable
|
|
149
|
+
* request, and a re-check. Rendered only for the codes above.
|
|
150
|
+
*/
|
|
151
|
+
function AssistBlock({ t, variant, code, busy, onRecheck }) {
|
|
152
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
153
|
+
const [copyFailed, setCopyFailed] = (0, react.useState)(false);
|
|
154
|
+
const prompt = t("assistantPrompt", {
|
|
155
|
+
appName: variant.appName,
|
|
156
|
+
failureSummary: t(assistSummaryKey(code, variant))
|
|
157
|
+
});
|
|
158
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
159
|
+
style: assistStyle,
|
|
160
|
+
children: [
|
|
161
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
|
|
162
|
+
style: assistHeadingStyle,
|
|
163
|
+
children: t("assistantHeading")
|
|
164
|
+
}),
|
|
165
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
166
|
+
style: bodyStyle,
|
|
167
|
+
children: t("assistantIntro")
|
|
168
|
+
}),
|
|
169
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
170
|
+
style: assistPromptRowStyle,
|
|
171
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
172
|
+
style: assistPromptStyle,
|
|
173
|
+
children: prompt
|
|
174
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
175
|
+
type: "button",
|
|
176
|
+
style: buttonStyle$1,
|
|
177
|
+
onClick: () => {
|
|
178
|
+
(async () => {
|
|
179
|
+
const ok = await copyPrompt(prompt);
|
|
180
|
+
setCopied(ok);
|
|
181
|
+
setCopyFailed(!ok);
|
|
182
|
+
})();
|
|
183
|
+
},
|
|
184
|
+
children: t("assistantCopy")
|
|
185
|
+
})]
|
|
186
|
+
}),
|
|
187
|
+
copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
188
|
+
style: assistFeedbackStyle,
|
|
189
|
+
role: "status",
|
|
190
|
+
children: t("assistantCopied")
|
|
191
|
+
}) : null,
|
|
192
|
+
copyFailed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
193
|
+
style: assistFeedbackStyle,
|
|
194
|
+
role: "status",
|
|
195
|
+
children: t("assistantCopyFailed")
|
|
196
|
+
}) : null,
|
|
197
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
198
|
+
style: bodyStyle,
|
|
199
|
+
children: t("assistantAfter")
|
|
200
|
+
}),
|
|
201
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
202
|
+
style: rowStyle,
|
|
203
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
204
|
+
type: "button",
|
|
205
|
+
style: buttonStyle$1,
|
|
206
|
+
disabled: busy,
|
|
207
|
+
onClick: onRecheck,
|
|
208
|
+
children: busy ? t("assistantRechecking") : t("assistantRecheck")
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
]
|
|
212
|
+
});
|
|
213
|
+
}
|
|
84
214
|
const cardStyle = {
|
|
85
215
|
overflow: "hidden",
|
|
86
216
|
border: "1px solid var(--dsw-alias-border-l2)",
|
|
@@ -225,6 +355,53 @@ window.__ModuleLoader__.load({
|
|
|
225
355
|
gap: 2
|
|
226
356
|
};
|
|
227
357
|
/**
|
|
358
|
+
* The Agent assist block. Sits beside the existing error line rather than
|
|
359
|
+
* replacing it: the short diagnosis stays the headline, this adds the way out.
|
|
360
|
+
*/
|
|
361
|
+
const assistStyle = {
|
|
362
|
+
display: "flex",
|
|
363
|
+
flexDirection: "column",
|
|
364
|
+
gap: 10,
|
|
365
|
+
padding: "12px 14px",
|
|
366
|
+
marginTop: 12,
|
|
367
|
+
border: "1px solid var(--dsw-alias-border-l2)",
|
|
368
|
+
borderRadius: 10,
|
|
369
|
+
background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.04))"
|
|
370
|
+
};
|
|
371
|
+
const assistHeadingStyle = {
|
|
372
|
+
margin: 0,
|
|
373
|
+
fontSize: 14,
|
|
374
|
+
lineHeight: "20px",
|
|
375
|
+
fontWeight: 600,
|
|
376
|
+
color: "var(--dsw-alias-label-primary)"
|
|
377
|
+
};
|
|
378
|
+
const assistPromptRowStyle = {
|
|
379
|
+
display: "flex",
|
|
380
|
+
flexWrap: "wrap",
|
|
381
|
+
alignItems: "flex-start",
|
|
382
|
+
gap: 8,
|
|
383
|
+
padding: "8px 9px 8px 11px",
|
|
384
|
+
borderRadius: 7,
|
|
385
|
+
background: "var(--dsw-alias-bg-layer-1)"
|
|
386
|
+
};
|
|
387
|
+
const assistPromptStyle = {
|
|
388
|
+
flex: "1 1 220px",
|
|
389
|
+
minWidth: 0,
|
|
390
|
+
margin: 0,
|
|
391
|
+
color: "var(--dsw-alias-label-primary)",
|
|
392
|
+
fontSize: 12,
|
|
393
|
+
lineHeight: "19px",
|
|
394
|
+
whiteSpace: "pre-wrap",
|
|
395
|
+
overflowWrap: "anywhere",
|
|
396
|
+
userSelect: "text"
|
|
397
|
+
};
|
|
398
|
+
const assistFeedbackStyle = {
|
|
399
|
+
margin: 0,
|
|
400
|
+
fontSize: 12,
|
|
401
|
+
lineHeight: "19px",
|
|
402
|
+
color: "var(--dsw-alias-label-secondary)"
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
228
405
|
* Left half of one merged context/visibility row: the visibility checkbox (when
|
|
229
406
|
* the account has one) beside the model's name and rate. Kept as a flex span so
|
|
230
407
|
* the capacity column stays flush right no matter how long the name runs.
|
|
@@ -987,6 +1164,13 @@ window.__ModuleLoader__.load({
|
|
|
987
1164
|
});
|
|
988
1165
|
}, [control]);
|
|
989
1166
|
const title = t(variant.titleKey);
|
|
1167
|
+
/**
|
|
1168
|
+
* The failure the assist block covers, when this document has one. Computed
|
|
1169
|
+
* once so the block and the header's refresh button agree on whether the
|
|
1170
|
+
* block owns the re-check action — showing both would put two buttons with
|
|
1171
|
+
* the same effect side by side.
|
|
1172
|
+
*/
|
|
1173
|
+
const assistCode = status?.status === "signed-out" && isWorkBuddySignedOutReasonCode(status.reasonCode) && ASSIST_REASON_CODES.includes(status.reasonCode) ? status.reasonCode : void 0;
|
|
990
1174
|
const label = status === void 0 ? t("loading") : status.status === "signed-in" ? status.nickname === void 0 ? t("signedInAs", { nickname: "" }).trimEnd().replace(/[::]$/, "") : t("signedInAs", { nickname: status.nickname }) : status.status === "error" ? t("requestFailed") : t("signedOut");
|
|
991
1175
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
992
1176
|
style: cardStyle,
|
|
@@ -1032,7 +1216,7 @@ window.__ModuleLoader__.load({
|
|
|
1032
1216
|
"aria-hidden": "true",
|
|
1033
1217
|
style: dotStyle(status === void 0 ? "loading" : status.status)
|
|
1034
1218
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
|
|
1035
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1219
|
+
}), assistCode === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1036
1220
|
type: "button",
|
|
1037
1221
|
style: buttonStyle$1,
|
|
1038
1222
|
disabled: busy,
|
|
@@ -1040,7 +1224,7 @@ window.__ModuleLoader__.load({
|
|
|
1040
1224
|
manualRefresh();
|
|
1041
1225
|
},
|
|
1042
1226
|
children: busy ? t("refreshing") : t("refresh")
|
|
1043
|
-
})]
|
|
1227
|
+
}) : null]
|
|
1044
1228
|
}),
|
|
1045
1229
|
readFailure === void 0 || signedIn === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1046
1230
|
style: errorStyle,
|
|
@@ -1178,10 +1362,18 @@ window.__ModuleLoader__.load({
|
|
|
1178
1362
|
})]
|
|
1179
1363
|
})
|
|
1180
1364
|
] }) : null,
|
|
1181
|
-
status?.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1365
|
+
status?.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1182
1366
|
style: status.reason === void 0 ? bodyStyle : errorStyle,
|
|
1183
1367
|
children: status.reason ?? t(variant.signedOutKey)
|
|
1184
|
-
}) :
|
|
1368
|
+
}), assistCode === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AssistBlock, {
|
|
1369
|
+
t,
|
|
1370
|
+
variant,
|
|
1371
|
+
code: assistCode,
|
|
1372
|
+
busy,
|
|
1373
|
+
onRecheck: () => {
|
|
1374
|
+
manualRefresh();
|
|
1375
|
+
}
|
|
1376
|
+
})] }) : null,
|
|
1185
1377
|
status?.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1186
1378
|
style: errorStyle,
|
|
1187
1379
|
children: status.message
|
|
@@ -1745,7 +1937,22 @@ window.__ModuleLoader__.load({
|
|
|
1745
1937
|
probeResultUnknown: "Detection did not complete",
|
|
1746
1938
|
probeResultAt: "Detected {time}",
|
|
1747
1939
|
probeResultEmpty: "No detectable models right now.",
|
|
1748
|
-
probeFailed: "Detection failed: {message}"
|
|
1940
|
+
probeFailed: "Detection failed: {message}",
|
|
1941
|
+
assistantHeading: "Let an Agent sort this out",
|
|
1942
|
+
assistantIntro: "Send the request below to your Agent; it will check the app location and the launch configuration for you.",
|
|
1943
|
+
assistantCopy: "Copy for Agent",
|
|
1944
|
+
assistantCopied: "Copied",
|
|
1945
|
+
assistantCopyFailed: "Copy failed — select the text above and copy it manually",
|
|
1946
|
+
assistantAfter: "When your Agent is done, come back and check again. If the DSH launch environment was changed, restart DSH first as instructed.",
|
|
1947
|
+
assistantRecheck: "Done — check again",
|
|
1948
|
+
assistantRechecking: "Checking…",
|
|
1949
|
+
assistantPrompt: "DSH's dsh-workbuddy-connect cannot use my {appName}: {failureSummary}. Please check the actual installation location and any existing path configuration, help the plugin use it correctly, and verify recovery. If the DSH launch environment must be changed or DSH restarted, give me clear steps; do not only set an environment variable temporarily in the current shell.",
|
|
1950
|
+
assistNotFound: "no usable decryption program was found",
|
|
1951
|
+
assistAmbiguous: "more than one WorkBuddy copy was found and none could be chosen safely",
|
|
1952
|
+
assistIncomplete: "the automatic search could not be completed",
|
|
1953
|
+
assistPathInvalid: "the configured program path is not usable",
|
|
1954
|
+
assistUnavailableCN: "no decryption program is configured for this platform",
|
|
1955
|
+
assistUnavailableAI: "no decryption program is configured for WorkBuddy AI"
|
|
1749
1956
|
};
|
|
1750
1957
|
const zh = {
|
|
1751
1958
|
title: "DSH WorkBuddy Connect",
|
|
@@ -1831,7 +2038,22 @@ window.__ModuleLoader__.load({
|
|
|
1831
2038
|
probeResultUnknown: "检测未完成",
|
|
1832
2039
|
probeResultAt: "检测于 {time}",
|
|
1833
2040
|
probeResultEmpty: "当前没有可检测的模型。",
|
|
1834
|
-
probeFailed: "检测失败:{message}"
|
|
2041
|
+
probeFailed: "检测失败:{message}",
|
|
2042
|
+
assistantHeading: "让 Agent 帮你处理",
|
|
2043
|
+
assistantIntro: "把下面这段请求发给你的 Agent,它会协助检查应用位置和启动配置。",
|
|
2044
|
+
assistantCopy: "复制给 Agent",
|
|
2045
|
+
assistantCopied: "已复制",
|
|
2046
|
+
assistantCopyFailed: "复制失败,请手动选择上方文字复制",
|
|
2047
|
+
assistantAfter: "Agent 处理完成后,回到这里重新检查;如果修改了 DSH 的启动环境,请先按指引重启 DSH。",
|
|
2048
|
+
assistantRecheck: "已处理,重新检查",
|
|
2049
|
+
assistantRechecking: "正在检查…",
|
|
2050
|
+
assistantPrompt: "DSH 的 dsh-workbuddy-connect 无法使用我的 {appName}:{failureSummary}。请帮我检查实际安装位置和已有路径配置,让插件能正确使用它,并验证恢复结果;如果需要修改 DSH 的启动环境或重启,请给我明确的操作步骤,不要只在当前 shell 临时设置环境变量。",
|
|
2051
|
+
assistNotFound: "没有找到可用的解密程序",
|
|
2052
|
+
assistAmbiguous: "找到了多个 WorkBuddy 副本,无法安全自动选择",
|
|
2053
|
+
assistIncomplete: "自动定位未能完成",
|
|
2054
|
+
assistPathInvalid: "指定的程序路径不可用",
|
|
2055
|
+
assistUnavailableCN: "当前平台尚未配置解密程序",
|
|
2056
|
+
assistUnavailableAI: "尚未配置 WorkBuddy AI 的解密程序"
|
|
1835
2057
|
};
|
|
1836
2058
|
//#endregion
|
|
1837
2059
|
//#region src/client/index.tsx
|
package/lib/index.d.ts
CHANGED
|
@@ -4,36 +4,62 @@ import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai";
|
|
|
4
4
|
import { Context } from "@deepseek-ai/cordis";
|
|
5
5
|
import { SettingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
6
6
|
import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
|
|
7
|
-
//#region src/
|
|
7
|
+
//#region src/status-paths.d.ts
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* resolution, and field decryption for the desktop app's encrypted auth file.
|
|
11
|
-
*
|
|
12
|
-
* Since WorkBuddy 5.6 the desktop app encrypts `auth.accessToken` and
|
|
13
|
-
* `auth.refreshToken` at rest (`buildPolicy: "fields"`, on by default), so the
|
|
14
|
-
* plugin reads `{$wbEncrypted:1, envelope}` wrappers instead of token strings
|
|
15
|
-
* (issues #39/#40). Everything needed to open them lives on the same machine:
|
|
16
|
-
*
|
|
17
|
-
* - the sealed payload (`{version:1, atRestSecretKey}`) comes from the
|
|
18
|
-
* WorkBuddy-modified Electron's private `workbuddyStorage` binding, reached
|
|
19
|
-
* by running *its own* binary once with `ELECTRON_RUN_AS_NODE=1`;
|
|
20
|
-
* - `protectorKey = sha256(atRestSecretKey, utf8)` opens the envelopes with
|
|
21
|
-
* AES-256-GCM; the AAD builder below is transcribed from the app's own
|
|
22
|
-
* `buildAuthenticatedContextAad` (verified live against 5.6.2, see
|
|
23
|
-
* `docs/r3-final.js` in the working copy — not committed).
|
|
9
|
+
* Why no credential is usable, as a closed enum the browser half switches on.
|
|
24
10
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* logged; error messages carry sizes, ids, and exit codes only.
|
|
30
|
-
*
|
|
31
|
-
* @module dsh-workbuddy-connect/desktop-credential-protection
|
|
11
|
+
* Deliberately separate from `reason`: `reason` is free text meant for a human
|
|
12
|
+
* to read, so matching on it would break the moment the wording changes. This
|
|
13
|
+
* is the machine-readable half, and the card uses it — never a substring of
|
|
14
|
+
* `reason` — to decide whether the Agent assist block applies.
|
|
32
15
|
*/
|
|
16
|
+
type WorkBuddySignedOutReasonCode =
|
|
17
|
+
/** Nobody is signed in; nothing diagnosable beyond that. */
|
|
18
|
+
'no-credential' |
|
|
19
|
+
/** A credential for the *other* product was found in this variant's file. */
|
|
20
|
+
'credential-region-mismatch' |
|
|
21
|
+
/** An encrypted credential exists but could not be opened (wrong key, GCM failure, helper crash). */
|
|
22
|
+
'encrypted-credential-unreadable' |
|
|
23
|
+
/** CN/macOS: discovery ran to completion and produced no usable candidate. */
|
|
24
|
+
'electron-binary-not-found' |
|
|
25
|
+
/** CN/macOS: discovery found more than one distinct usable app. */
|
|
26
|
+
'electron-binary-ambiguous' |
|
|
27
|
+
/** No auto-discovery for this product/platform and no explicit path configured. */
|
|
28
|
+
'electron-binary-unavailable' |
|
|
29
|
+
/** An explicit path (option or env) is set but missing or not executable. */
|
|
30
|
+
'electron-path-invalid' |
|
|
31
|
+
/** Discovery could not finish: tool missing, timeout, output overflow, unreadable plist. */
|
|
32
|
+
'electron-discovery-incomplete';
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/desktop-credential-protection.d.ts
|
|
33
35
|
/** The four states a desktop auth document can be read as. */
|
|
34
36
|
type DesktopAuthFormat = 'absent' | 'plaintext' | 'encrypted' | 'unrecognized';
|
|
35
37
|
/** The spawned helper. Separated from the provider so tests can stand it in. */
|
|
36
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
|
+
}
|
|
37
63
|
/** Provider options. */
|
|
38
64
|
interface WorkBuddyAtRestKeyProviderOptions {
|
|
39
65
|
/** Explicit Electron binary; overrides the platform default and env. */
|
|
@@ -43,23 +69,76 @@ interface WorkBuddyAtRestKeyProviderOptions {
|
|
|
43
69
|
/**
|
|
44
70
|
* Where the payload comes from. Defaults to spawning WorkBuddy's own
|
|
45
71
|
* Electron with `ELECTRON_RUN_AS_NODE=1`; tests supply a stand-in so no
|
|
46
|
-
* test ever touches the real binary or a real key.
|
|
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.
|
|
47
75
|
*/
|
|
48
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;
|
|
49
104
|
}
|
|
50
105
|
/**
|
|
51
106
|
* In-memory protector-key resolver: one spawn per key id, single-flight, never
|
|
52
107
|
* persisted. The cache is keyed by the id envelopes ask for, so an envelope
|
|
53
108
|
* sealed under a rotated key triggers exactly one fresh resolution.
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
|
|
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;
|
|
57
121
|
private readonly timeoutMs;
|
|
58
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;
|
|
59
130
|
private cache;
|
|
60
131
|
private inflight;
|
|
61
132
|
constructor(options?: WorkBuddyAtRestKeyProviderOptions);
|
|
62
|
-
/**
|
|
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
|
+
*/
|
|
63
142
|
helperPath(): string | undefined;
|
|
64
143
|
/**
|
|
65
144
|
* A protector key matching one of the requested envelope key ids. The first
|
|
@@ -70,7 +149,25 @@ declare class WorkBuddyAtRestKeyProvider {
|
|
|
70
149
|
*/
|
|
71
150
|
protectorKeyFor(requested: readonly string[]): Promise<Buffer>;
|
|
72
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;
|
|
73
169
|
private spawnPayload;
|
|
170
|
+
private spawnAt;
|
|
74
171
|
}
|
|
75
172
|
//#endregion
|
|
76
173
|
//#region src/app-version.d.ts
|
|
@@ -726,6 +823,11 @@ interface WorkBuddyAuthStatus {
|
|
|
726
823
|
* Present only on `signed-out`, and never a substitute for fixing the file.
|
|
727
824
|
*/
|
|
728
825
|
reason?: string;
|
|
826
|
+
/**
|
|
827
|
+
* Machine-readable companion to {@link reason}, for callers that must branch
|
|
828
|
+
* on the cause. Never derived by matching `reason` text.
|
|
829
|
+
*/
|
|
830
|
+
reasonCode?: WorkBuddySignedOutReasonCode;
|
|
729
831
|
}
|
|
730
832
|
/** Constructor options; only {@link refresh} is required. */
|
|
731
833
|
interface WorkBuddyStoreOptions {
|
|
@@ -1362,8 +1464,9 @@ declare function readHostHeartbeat(): Promise<WorkBuddyHostHeartbeat | undefined
|
|
|
1362
1464
|
* - macOS / Linux: `ps -o lstart=` prints a local-time "EEE MMM DD HH:MM:SS YYYY";
|
|
1363
1465
|
* `Date.parse` resolves it against the local clock, which matches how
|
|
1364
1466
|
* `registeredAt` (a `Date.now()` absolute value) is expressed.
|
|
1365
|
-
* - Windows:
|
|
1366
|
-
*
|
|
1467
|
+
* - Windows: `wmic` prints a CIM_DATETIME `CreationDate` — local fields plus a
|
|
1468
|
+
* signed minute offset (see {@link parseWmiCreationDate}); the epoch it
|
|
1469
|
+
* yields is comparable to `registeredAt`.
|
|
1367
1470
|
*
|
|
1368
1471
|
* Failures return `undefined` so callers can fall back to plain PID liveness
|
|
1369
1472
|
* rather than mis-report a running host as dead.
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as normalizeCredits, B as chatUserAgent, C as desktopAuthCandidatesFor, D as WorkBuddyUpstreamClient, E as WorkBuddyAtRestKeyProvider, F as PROBE_EFFORT_CANDIDATES, G as WORKBUDDY_APP_VERSION_FILENAME, H as readCliVersion, I as probeModel, J as readBundleVersion, K as appUserAgent, L as randomSentinel, M as prepareChatBody, N as prepareInternationalChatBody, O as classifyUpstreamError, P as regionOf, R as CN_APP_VERSION_FILENAME, S as defaultDesktopAuthPath, T as workbuddyOwnAuthPath, U as resolveChatIdentity, V as fallbackChatIdentity, W as validCliVersion, X as validAppVersion, Y as resolveAppVersion, _ 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 parseModelCatalog, k as modelWithCurrentPromotion, l as readHostHeartbeat, n as CN_VARIANT, o as clearHostHeartbeat, q as installedAppVersion, 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 FALLBACK_CN_APP_VERSION } from "./variants-
|
|
1
|
+
import { A as normalizeCredits, B as chatUserAgent, C as desktopAuthCandidatesFor, D as WorkBuddyUpstreamClient, E as WorkBuddyAtRestKeyProvider, F as PROBE_EFFORT_CANDIDATES, G as WORKBUDDY_APP_VERSION_FILENAME, H as readCliVersion, I as probeModel, J as readBundleVersion, K as appUserAgent, L as randomSentinel, M as prepareChatBody, N as prepareInternationalChatBody, O as classifyUpstreamError, P as regionOf, R as CN_APP_VERSION_FILENAME, S as defaultDesktopAuthPath, T as workbuddyOwnAuthPath, U as resolveChatIdentity, V as fallbackChatIdentity, W as validCliVersion, X as validAppVersion, Y as resolveAppVersion, _ 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 parseModelCatalog, k as modelWithCurrentPromotion, l as readHostHeartbeat, n as CN_VARIANT, o as clearHostHeartbeat, q as installedAppVersion, 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 FALLBACK_CN_APP_VERSION } from "./variants-Cdp_ii6z.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
@@ -1170,7 +1170,8 @@ async function workBuddyWebStatus(deps) {
|
|
|
1170
1170
|
const authStatus = await deps.store.status();
|
|
1171
1171
|
if (authStatus.state !== "signed-in") return {
|
|
1172
1172
|
status: "signed-out",
|
|
1173
|
-
...authStatus.reason === void 0 ? {} : { reason: authStatus.reason }
|
|
1173
|
+
...authStatus.reason === void 0 ? {} : { reason: authStatus.reason },
|
|
1174
|
+
...authStatus.reasonCode === void 0 ? {} : { reasonCode: authStatus.reasonCode }
|
|
1174
1175
|
};
|
|
1175
1176
|
const status = {
|
|
1176
1177
|
status: "signed-in",
|
|
@@ -1762,8 +1763,8 @@ function apply(ctx, config) {
|
|
|
1762
1763
|
* case that must not fall back to a shared preference bucket.
|
|
1763
1764
|
*/
|
|
1764
1765
|
const lastAccounts = /* @__PURE__ */ new Map();
|
|
1765
|
-
const
|
|
1766
|
-
const runtimes = WORKBUDDY_VARIANTS.map((variant) => createVariantRuntime(config, variant, () => current(), (id) => lastIdentities.get(id), (id) => lastAccounts.get(id),
|
|
1766
|
+
const atRestKeysFor = (variant) => new WorkBuddyAtRestKeyProvider({ discovery: variant.id === CN_VARIANT.id ? "macos-workbuddy" : "none" });
|
|
1767
|
+
const runtimes = WORKBUDDY_VARIANTS.map((variant) => createVariantRuntime(config, variant, () => current(), (id) => lastIdentities.get(id), (id) => lastAccounts.get(id), atRestKeysFor(variant)));
|
|
1767
1768
|
const probeKey = createProbeKey();
|
|
1768
1769
|
let setMaximumContextWindow;
|
|
1769
1770
|
/**
|
|
@@ -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 } from "node:fs";
|
|
8
|
+
import { accessSync, constants, 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.
|
|
@@ -1389,10 +1389,11 @@ function prepareInternationalChatBody(source) {
|
|
|
1389
1389
|
return prepared;
|
|
1390
1390
|
}
|
|
1391
1391
|
if (!isObject(body)) return prepared;
|
|
1392
|
+
dropUnsupportedEffort(body);
|
|
1392
1393
|
const messages = body["messages"];
|
|
1393
|
-
if (!Array.isArray(messages)) return
|
|
1394
|
+
if (!Array.isArray(messages)) return JSON.stringify(body);
|
|
1394
1395
|
const first = messages[0];
|
|
1395
|
-
if (isObject(first) && first["role"] === "system") return
|
|
1396
|
+
if (isObject(first) && first["role"] === "system") return JSON.stringify(body);
|
|
1396
1397
|
messages.unshift({
|
|
1397
1398
|
role: "system",
|
|
1398
1399
|
content: INTERNATIONAL_SYSTEM_PROMPT
|
|
@@ -1400,6 +1401,30 @@ function prepareInternationalChatBody(source) {
|
|
|
1400
1401
|
return JSON.stringify(body);
|
|
1401
1402
|
}
|
|
1402
1403
|
/**
|
|
1404
|
+
* Remove the adapter's own `off` effort spelling from the **international** wire.
|
|
1405
|
+
*
|
|
1406
|
+
* `thinkingLevelMap.off` is pinned to the literal `'off'` for models that
|
|
1407
|
+
* declare `canDisableThinking`, so that the level stays selectable. pi-ai
|
|
1408
|
+
* sends that value for any request carrying no explicit level, and the
|
|
1409
|
+
* international endpoint rejects it on the GPT family with HTTP 400 `11133` /
|
|
1410
|
+
* `extError.param === 'reasoning.effort'` (issue #49). Omission is the only
|
|
1411
|
+
* form measured good on every such model; a literal `'none'` is *not* a safe
|
|
1412
|
+
* substitute — accepted by the GPT-5.6 family and GLM, rejected by
|
|
1413
|
+
* `gpt-6-astra`.
|
|
1414
|
+
*
|
|
1415
|
+
* Consequences, stated honestly: the international picker still offers Off,
|
|
1416
|
+
* but selecting it now means "the field is omitted" — the model's actual
|
|
1417
|
+
* behaviour is decided upstream and is *not* guaranteed to disable thinking
|
|
1418
|
+
* or to match the catalog's `defaultEffort`. Declared spellings
|
|
1419
|
+
* (`low`/`medium`/`high`/`xhigh`/`max`) and an explicit `none` pass through
|
|
1420
|
+
* untouched. The CN variant is deliberately unaffected: its endpoint has
|
|
1421
|
+
* accepted this spelling in every measurement so far, and keeping its wire
|
|
1422
|
+
* unchanged is a scope decision, not a claim about that endpoint's future.
|
|
1423
|
+
*/
|
|
1424
|
+
function dropUnsupportedEffort(obj) {
|
|
1425
|
+
if (obj["reasoning_effort"] === "off") delete obj["reasoning_effort"];
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1403
1428
|
* The system prompt injected when the international endpoint receives a body
|
|
1404
1429
|
* with none.
|
|
1405
1430
|
*
|
|
@@ -1636,26 +1661,145 @@ function parseAtRestPayload(text) {
|
|
|
1636
1661
|
function deriveProtectorKey(secret) {
|
|
1637
1662
|
return createHash("sha256").update(secret, "utf8").digest();
|
|
1638
1663
|
}
|
|
1664
|
+
/** The CN app's bundle id; the only one this round resolves by discovery. */
|
|
1665
|
+
const WORKBUDDY_CN_BUNDLE_ID = "com.tencent.workbuddy.mac";
|
|
1666
|
+
/** Absolute tool paths: never resolved through PATH, which a user can change. */
|
|
1667
|
+
const MDFIND_BIN = "/usr/bin/mdfind";
|
|
1668
|
+
const PLUTIL_BIN = "/usr/bin/plutil";
|
|
1669
|
+
/** One discovery subprocess's own limits; see {@link WorkBuddyAtRestKeyProviderOptions}. */
|
|
1670
|
+
const WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS = 3e3;
|
|
1671
|
+
const MDFIND_MAX_OUTPUT_BYTES = 1048576;
|
|
1672
|
+
const PLUTIL_MAX_OUTPUT_BYTES = 65536;
|
|
1673
|
+
/**
|
|
1674
|
+
* Why a discovery step could not produce an answer. Every one of these means
|
|
1675
|
+
* "we do not know", explicitly *not* "the candidate does not exist" — the
|
|
1676
|
+
* distinction is what keeps a half-finished check from being mistaken for a
|
|
1677
|
+
* unique candidate.
|
|
1678
|
+
*/
|
|
1679
|
+
var DiscoveryIncompleteError = class extends Error {};
|
|
1680
|
+
/**
|
|
1681
|
+
* The default discovery tools: Spotlight for the bundle, `/usr/bin/plutil` for
|
|
1682
|
+
* identity. Every failure that means "we could not tell" — a missing tool, a
|
|
1683
|
+
* timeout, an oversized answer — is raised as {@link DiscoveryIncompleteError}
|
|
1684
|
+
* so it can never be silently read as "no such app".
|
|
1685
|
+
*/
|
|
1686
|
+
function workBuddyDiscoveryTools() {
|
|
1687
|
+
const runTool = (bin, args, maxBytes, signal) => new Promise((resolve, reject) => {
|
|
1688
|
+
if (signal.aborted) {
|
|
1689
|
+
reject(new DiscoveryIncompleteError(`${bin} was not started: the discovery budget was already spent`));
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
let settled = false;
|
|
1693
|
+
const child = execFile(bin, [...args], {
|
|
1694
|
+
maxBuffer: maxBytes,
|
|
1695
|
+
timeout: WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS
|
|
1696
|
+
}, (error, stdout) => {
|
|
1697
|
+
if (settled) return;
|
|
1698
|
+
settled = true;
|
|
1699
|
+
if (error !== null && error !== void 0) {
|
|
1700
|
+
reject(new DiscoveryIncompleteError(`${bin} could not complete (${error.killed === true ? "timed out" : String(error.code ?? "unavailable")})`));
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
resolve(stdout);
|
|
1704
|
+
});
|
|
1705
|
+
const abort = () => {
|
|
1706
|
+
if (settled) return;
|
|
1707
|
+
settled = true;
|
|
1708
|
+
child.kill();
|
|
1709
|
+
reject(new DiscoveryIncompleteError(`${bin} was abandoned: the discovery budget was spent`));
|
|
1710
|
+
};
|
|
1711
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1712
|
+
child.on("close", () => {
|
|
1713
|
+
signal.removeEventListener("abort", abort);
|
|
1714
|
+
});
|
|
1715
|
+
});
|
|
1716
|
+
return {
|
|
1717
|
+
findApps: async (signal) => {
|
|
1718
|
+
return (await runTool(MDFIND_BIN, [`kMDItemCFBundleIdentifier == '${WORKBUDDY_CN_BUNDLE_ID}'`], MDFIND_MAX_OUTPUT_BYTES, signal)).split("\n").map((line) => line.trim()).filter((line) => line.endsWith(".app"));
|
|
1719
|
+
},
|
|
1720
|
+
bundleIdentifier: async (bundlePath, signal) => {
|
|
1721
|
+
try {
|
|
1722
|
+
return (await runTool(PLUTIL_BIN, [
|
|
1723
|
+
"-extract",
|
|
1724
|
+
"CFBundleIdentifier",
|
|
1725
|
+
"raw",
|
|
1726
|
+
"-o",
|
|
1727
|
+
"-",
|
|
1728
|
+
join(bundlePath, "Contents", "Info.plist")
|
|
1729
|
+
], PLUTIL_MAX_OUTPUT_BYTES, signal)).trim();
|
|
1730
|
+
} catch {
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
},
|
|
1734
|
+
bundleVersion: async (bundlePath, signal) => {
|
|
1735
|
+
try {
|
|
1736
|
+
const version = (await runTool(PLUTIL_BIN, [
|
|
1737
|
+
"-extract",
|
|
1738
|
+
"CFBundleShortVersionString",
|
|
1739
|
+
"raw",
|
|
1740
|
+
"-o",
|
|
1741
|
+
"-",
|
|
1742
|
+
join(bundlePath, "Contents", "Info.plist")
|
|
1743
|
+
], PLUTIL_MAX_OUTPUT_BYTES, signal)).trim();
|
|
1744
|
+
return version === "" ? void 0 : version;
|
|
1745
|
+
} catch {
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1639
1751
|
/**
|
|
1640
1752
|
* In-memory protector-key resolver: one spawn per key id, single-flight, never
|
|
1641
1753
|
* persisted. The cache is keyed by the id envelopes ask for, so an envelope
|
|
1642
1754
|
* sealed under a rotated key triggers exactly one fresh resolution.
|
|
1643
|
-
*/
|
|
1644
|
-
|
|
1645
|
-
|
|
1755
|
+
*/ var WorkBuddyAtRestKeyProvider = class {
|
|
1756
|
+
/**
|
|
1757
|
+
* The explicit binary, when one was configured. `undefined` here means "the
|
|
1758
|
+
* caller did not name one", which is what lets discovery run — an explicit
|
|
1759
|
+
* path that turns out to be unusable is an error, never a reason to look for
|
|
1760
|
+
* a different app.
|
|
1761
|
+
*/
|
|
1762
|
+
explicitPath;
|
|
1763
|
+
defaultPath;
|
|
1764
|
+
discovery;
|
|
1765
|
+
tools;
|
|
1766
|
+
discoveryBudgetMs;
|
|
1646
1767
|
timeoutMs;
|
|
1647
1768
|
source;
|
|
1769
|
+
spawnHelper;
|
|
1770
|
+
/**
|
|
1771
|
+
* The path discovery settled on, cached only on success. A failure leaves
|
|
1772
|
+
* this unset so the next attempt tries again — the user may install or move
|
|
1773
|
+
* the app without restarting DSH.
|
|
1774
|
+
*/
|
|
1775
|
+
discoveredPath;
|
|
1648
1776
|
cache;
|
|
1649
1777
|
inflight;
|
|
1650
1778
|
constructor(options = {}) {
|
|
1651
1779
|
const fromEnv = process.env[WORKBUDDY_ELECTRON_BIN_ENV]?.trim();
|
|
1652
|
-
|
|
1780
|
+
const envPath = fromEnv === void 0 || fromEnv === "" ? void 0 : fromEnv;
|
|
1781
|
+
this.explicitPath = options.electronPath ?? envPath;
|
|
1782
|
+
this.discovery = options.discovery ?? "none";
|
|
1783
|
+
this.defaultPath = options.defaultElectronPath === void 0 ? defaultWorkBuddyElectronPath() : options.defaultElectronPath ?? void 0;
|
|
1784
|
+
this.tools = options.tools ?? workBuddyDiscoveryTools();
|
|
1785
|
+
this.discoveryBudgetMs = options.discoveryBudgetMs ?? 1e4;
|
|
1653
1786
|
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
1787
|
+
this.spawnHelper = options.spawnHelper ?? ((path) => this.spawnAt(path));
|
|
1654
1788
|
this.source = options.source ?? (() => this.spawnPayload());
|
|
1655
1789
|
}
|
|
1656
|
-
/**
|
|
1790
|
+
/**
|
|
1791
|
+
* The binary the default helper would use, for diagnostics.
|
|
1792
|
+
*
|
|
1793
|
+
* Reports a *discovery result* once one exists, so diagnostics describe what
|
|
1794
|
+
* would actually run rather than the default that was bypassed. Discovery
|
|
1795
|
+
* itself stays in {@link resolveElectronPath}: this accessor never triggers a
|
|
1796
|
+
* search (the constructor must remain I/O-free, and callers may ask before
|
|
1797
|
+
* any resolution has happened).
|
|
1798
|
+
*/
|
|
1657
1799
|
helperPath() {
|
|
1658
|
-
return this.
|
|
1800
|
+
if (this.explicitPath !== void 0) return this.explicitPath;
|
|
1801
|
+
if (this.discovery === "none") return void 0;
|
|
1802
|
+
return this.discoveredPath ?? this.defaultPath;
|
|
1659
1803
|
}
|
|
1660
1804
|
/**
|
|
1661
1805
|
* A protector key matching one of the requested envelope key ids. The first
|
|
@@ -1665,19 +1809,19 @@ var WorkBuddyAtRestKeyProvider = class {
|
|
|
1665
1809
|
* reach will open them.
|
|
1666
1810
|
*/
|
|
1667
1811
|
async protectorKeyFor(requested) {
|
|
1668
|
-
if (requested.length === 0) throw new
|
|
1812
|
+
if (requested.length === 0) throw new WorkBuddyElectronPathError("encrypted-credential-unreadable", "encrypted desktop credential carries no key ids");
|
|
1669
1813
|
const cached = this.cache;
|
|
1670
1814
|
if (cached !== void 0 && requested.includes(cached.keyId)) return cached.key;
|
|
1671
1815
|
this.inflight ??= this.source().then((text) => this.ingest(text)).finally(() => {
|
|
1672
1816
|
this.inflight = void 0;
|
|
1673
1817
|
});
|
|
1674
1818
|
const resolved = await this.inflight;
|
|
1675
|
-
if (!requested.includes(resolved.keyId)) throw new
|
|
1819
|
+
if (!requested.includes(resolved.keyId)) throw new WorkBuddyElectronPathError("encrypted-credential-unreadable", `WorkBuddy's current at-rest key (id ${resolved.keyId}) does not match the credential's envelope (id ${requested.join(" or ")}); the desktop credential was sealed by a different WorkBuddy installation`);
|
|
1676
1820
|
return resolved.key;
|
|
1677
1821
|
}
|
|
1678
1822
|
ingest(text) {
|
|
1679
1823
|
const payload = parseAtRestPayload(text);
|
|
1680
|
-
if (payload === void 0) throw new
|
|
1824
|
+
if (payload === void 0) throw new WorkBuddyElectronPathError("encrypted-credential-unreadable", "WorkBuddy key helper returned an unusable at-rest payload (expected {version:1, atRestSecretKey})");
|
|
1681
1825
|
const key = deriveProtectorKey(payload.atRestSecretKey);
|
|
1682
1826
|
const resolved = {
|
|
1683
1827
|
key,
|
|
@@ -1686,14 +1830,103 @@ var WorkBuddyAtRestKeyProvider = class {
|
|
|
1686
1830
|
this.cache = resolved;
|
|
1687
1831
|
return resolved;
|
|
1688
1832
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1833
|
+
/**
|
|
1834
|
+
* The binary to spawn, or a diagnosable error saying why there is none.
|
|
1835
|
+
*
|
|
1836
|
+
* Order is the contract: an explicit path is used as-is and never falls back;
|
|
1837
|
+
* discovery runs only for a provider that was configured for it, and only
|
|
1838
|
+
* after the platform default has been tried and found unusable.
|
|
1839
|
+
*/
|
|
1840
|
+
async resolveElectronPath() {
|
|
1841
|
+
if (this.explicitPath !== void 0) {
|
|
1842
|
+
if (!isExecutable(this.explicitPath)) throw new WorkBuddyElectronPathError("electron-path-invalid", `the configured WorkBuddy Electron binary is not available at ${this.explicitPath}; check ${WORKBUDDY_ELECTRON_BIN_ENV} or unset it to let the plugin look for the app itself`);
|
|
1843
|
+
return this.explicitPath;
|
|
1844
|
+
}
|
|
1845
|
+
if (this.discovery === "none") 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`);
|
|
1846
|
+
if (this.defaultPath !== void 0 && isExecutable(this.defaultPath)) return this.defaultPath;
|
|
1847
|
+
if (this.discoveredPath !== void 0) {
|
|
1848
|
+
if (isExecutable(this.discoveredPath)) return this.discoveredPath;
|
|
1849
|
+
this.discoveredPath = void 0;
|
|
1850
|
+
}
|
|
1851
|
+
const found = await this.discoverMacosApp();
|
|
1852
|
+
this.discoveredPath = found;
|
|
1853
|
+
return found;
|
|
1854
|
+
}
|
|
1855
|
+
/**
|
|
1856
|
+
* Resolve the CN app through Spotlight, then prove each candidate's identity
|
|
1857
|
+
* before it can be executed.
|
|
1858
|
+
*
|
|
1859
|
+
* The whole flow shares one budget: a hang in one candidate must not extend
|
|
1860
|
+
* the wait for the others, and running out of budget is reported as an
|
|
1861
|
+
* unfinished check rather than an absent app.
|
|
1862
|
+
*/
|
|
1863
|
+
async discoverMacosApp() {
|
|
1864
|
+
if (process.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
|
+
const controller = new AbortController();
|
|
1866
|
+
const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
|
|
1692
1867
|
try {
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1868
|
+
let candidates;
|
|
1869
|
+
try {
|
|
1870
|
+
candidates = await this.tools.findApps(controller.signal);
|
|
1871
|
+
} catch {
|
|
1872
|
+
throw discoveryIncomplete("the app search did not complete");
|
|
1873
|
+
}
|
|
1874
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1875
|
+
let unresolved = false;
|
|
1876
|
+
for (const candidate of candidates) {
|
|
1877
|
+
try {
|
|
1878
|
+
statSync(candidate);
|
|
1879
|
+
} catch (error) {
|
|
1880
|
+
if (isENOENT$1(error)) continue;
|
|
1881
|
+
unresolved = true;
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
let bundleIdentifier;
|
|
1885
|
+
try {
|
|
1886
|
+
bundleIdentifier = await this.tools.bundleIdentifier(candidate, controller.signal);
|
|
1887
|
+
} catch {
|
|
1888
|
+
unresolved = true;
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
if (bundleIdentifier === void 0) {
|
|
1892
|
+
unresolved = true;
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1895
|
+
if (bundleIdentifier !== "com.tencent.workbuddy.mac") continue;
|
|
1896
|
+
const electronPath = join(candidate, "Contents", "MacOS", "Electron");
|
|
1897
|
+
if (!isExecutable(electronPath)) continue;
|
|
1898
|
+
let identity;
|
|
1899
|
+
try {
|
|
1900
|
+
identity = realpathSync(candidate);
|
|
1901
|
+
} catch {
|
|
1902
|
+
identity = candidate;
|
|
1903
|
+
}
|
|
1904
|
+
if (seen.has(identity)) continue;
|
|
1905
|
+
let version;
|
|
1906
|
+
try {
|
|
1907
|
+
version = await this.tools.bundleVersion(candidate, controller.signal);
|
|
1908
|
+
} catch {
|
|
1909
|
+
version = void 0;
|
|
1910
|
+
}
|
|
1911
|
+
seen.set(identity, {
|
|
1912
|
+
bundlePath: candidate,
|
|
1913
|
+
electronPath,
|
|
1914
|
+
...version === void 0 ? {} : { version }
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
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((app) => ` - ${app.bundlePath}${app.version === void 0 ? "" : ` (${app.version})`}`).join("\n")}\n set ${WORKBUDDY_ELECTRON_BIN_ENV} to the one to use`);
|
|
1918
|
+
if (unresolved) throw discoveryIncomplete("some candidates could not be checked");
|
|
1919
|
+
if (seen.size === 0) throw new WorkBuddyElectronPathError("electron-binary-not-found", "no WorkBuddy application was found in the default location or the system index; if WorkBuddy is installed elsewhere, it may not be indexed yet");
|
|
1920
|
+
return [...seen.values()][0].electronPath;
|
|
1921
|
+
} finally {
|
|
1922
|
+
clearTimeout(timer);
|
|
1923
|
+
controller.abort();
|
|
1696
1924
|
}
|
|
1925
|
+
}
|
|
1926
|
+
async spawnPayload() {
|
|
1927
|
+
return await this.spawnHelper(await this.resolveElectronPath());
|
|
1928
|
+
}
|
|
1929
|
+
async spawnAt(electronPath) {
|
|
1697
1930
|
return await new Promise((resolve, reject) => {
|
|
1698
1931
|
execFile(electronPath, [HELPER_SCRIPT_ARGUMENT_FLAG, HELPER_SCRIPT], {
|
|
1699
1932
|
timeout: this.timeoutMs,
|
|
@@ -1705,13 +1938,12 @@ var WorkBuddyAtRestKeyProvider = class {
|
|
|
1705
1938
|
}
|
|
1706
1939
|
}, (error, stdout) => {
|
|
1707
1940
|
if (error !== null && error !== void 0) {
|
|
1708
|
-
|
|
1709
|
-
reject(/* @__PURE__ */ new Error(`the WorkBuddy key helper (${electronPath}) ${reason}`));
|
|
1941
|
+
reject(new WorkBuddyElectronPathError("encrypted-credential-unreadable", `the WorkBuddy key helper (${electronPath}) ${error.killed === true ? `timed out or was killed after ${String(this.timeoutMs)}ms` : error.code !== void 0 ? `exited with code ${String(error.code)}` : "could not be started"}`));
|
|
1710
1942
|
return;
|
|
1711
1943
|
}
|
|
1712
1944
|
const output = stdout.trim();
|
|
1713
1945
|
if (output === "") {
|
|
1714
|
-
reject(
|
|
1946
|
+
reject(new WorkBuddyElectronPathError("encrypted-credential-unreadable", `the WorkBuddy key helper (${electronPath}) produced no payload`));
|
|
1715
1947
|
return;
|
|
1716
1948
|
}
|
|
1717
1949
|
resolve(output);
|
|
@@ -1719,6 +1951,39 @@ var WorkBuddyAtRestKeyProvider = class {
|
|
|
1719
1951
|
});
|
|
1720
1952
|
}
|
|
1721
1953
|
};
|
|
1954
|
+
/** Whether a path exists and is executable; never throws. */
|
|
1955
|
+
function isExecutable(path) {
|
|
1956
|
+
try {
|
|
1957
|
+
accessSync(path, constants.X_OK);
|
|
1958
|
+
return true;
|
|
1959
|
+
} catch {
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* A failure the card must be able to classify. The code travels with the error
|
|
1965
|
+
* so the store can promote it to `reasonCode` without re-deriving the cause
|
|
1966
|
+
* from prose.
|
|
1967
|
+
*/
|
|
1968
|
+
var WorkBuddyElectronPathError = class extends Error {
|
|
1969
|
+
reasonCode;
|
|
1970
|
+
constructor(reasonCode, message) {
|
|
1971
|
+
super(message);
|
|
1972
|
+
this.name = "WorkBuddyElectronPathError";
|
|
1973
|
+
this.reasonCode = reasonCode;
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
/** Read the reason code off an arbitrary thrown value, when it carries one. */
|
|
1977
|
+
function reasonCodeOf(error) {
|
|
1978
|
+
return error instanceof WorkBuddyElectronPathError ? error.reasonCode : void 0;
|
|
1979
|
+
}
|
|
1980
|
+
/** Whether a filesystem error reports an absent path (`existsSync` cannot tell). */
|
|
1981
|
+
function isENOENT$1(error) {
|
|
1982
|
+
return error?.code === "ENOENT";
|
|
1983
|
+
}
|
|
1984
|
+
function discoveryIncomplete(detail) {
|
|
1985
|
+
return new WorkBuddyElectronPathError("electron-discovery-incomplete", `the WorkBuddy application search did not finish (${detail}); this is not proof that the app is missing`);
|
|
1986
|
+
}
|
|
1722
1987
|
/**
|
|
1723
1988
|
* The helper: run inside WorkBuddy's Electron as plain Node, where the
|
|
1724
1989
|
* private `workbuddyStorage` binding exists, and print only the payload. It
|
|
@@ -1971,7 +2236,7 @@ var WorkBuddyCredentialStore = class {
|
|
|
1971
2236
|
if (this.variant !== void 0) for (const [label, credential] of [["desktop file", desktop], ["plugin copy", own]]) {
|
|
1972
2237
|
if (credential === void 0) continue;
|
|
1973
2238
|
const region = regionOf(credential.domain);
|
|
1974
|
-
if (region !== this.variant.region) throw new
|
|
2239
|
+
if (region !== this.variant.region) throw new WorkBuddyElectronPathError("credential-region-mismatch", `${this.variant.displayName} received a ${region === "cn" ? "WorkBuddy (CN)" : "WorkBuddy AI"} credential in its ${label} (domain ${JSON.stringify(credential.domain)}); point ${this.variant.env} at the ${this.variant.appName} sign-in, or remove the mismatched file`);
|
|
1975
2240
|
}
|
|
1976
2241
|
if (desktop === void 0) return own;
|
|
1977
2242
|
if (own === void 0) return desktop;
|
|
@@ -2000,7 +2265,10 @@ var WorkBuddyCredentialStore = class {
|
|
|
2000
2265
|
async status() {
|
|
2001
2266
|
try {
|
|
2002
2267
|
const credential = await this.current();
|
|
2003
|
-
if (credential === void 0) return {
|
|
2268
|
+
if (credential === void 0) return {
|
|
2269
|
+
state: "signed-out",
|
|
2270
|
+
reasonCode: "no-credential"
|
|
2271
|
+
};
|
|
2004
2272
|
return {
|
|
2005
2273
|
state: "signed-in",
|
|
2006
2274
|
expiresAtMs: credential.expiresAtMs,
|
|
@@ -2012,7 +2280,8 @@ var WorkBuddyCredentialStore = class {
|
|
|
2012
2280
|
} catch (error) {
|
|
2013
2281
|
return {
|
|
2014
2282
|
state: "signed-out",
|
|
2015
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
2283
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
2284
|
+
...reasonCodeOf(error) === void 0 ? {} : { reasonCode: reasonCodeOf(error) }
|
|
2016
2285
|
};
|
|
2017
2286
|
}
|
|
2018
2287
|
}
|
|
@@ -2092,7 +2361,7 @@ var WorkBuddyCredentialStore = class {
|
|
|
2092
2361
|
const key = await this.keyProvider.protectorKeyFor(keyIdsOf(wrapped.fields));
|
|
2093
2362
|
return parseWorkBuddyAuth(unwrapDesktopAuthDocument(classification, (field) => {
|
|
2094
2363
|
const plaintext = openAuthField(key, field.envelope);
|
|
2095
|
-
if (plaintext === void 0) throw new
|
|
2364
|
+
if (plaintext === void 0) throw new WorkBuddyElectronPathError("encrypted-credential-unreadable", `the encrypted desktop credential's ${field.field} could not be decrypted (envelope key id ${field.envelope.keyId}); the WorkBuddy app may hold a different at-rest key — open it once to reseal the sign-in`);
|
|
2096
2365
|
return plaintext;
|
|
2097
2366
|
}));
|
|
2098
2367
|
}
|
|
@@ -2960,7 +3229,7 @@ const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
|
|
|
2960
3229
|
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
|
|
2961
3230
|
//#endregion
|
|
2962
3231
|
//#region src/version.ts
|
|
2963
|
-
const WORKBUDDY_CONNECT_VERSION = "0.6.
|
|
3232
|
+
const WORKBUDDY_CONNECT_VERSION = "0.6.1";
|
|
2964
3233
|
//#endregion
|
|
2965
3234
|
//#region src/host-heartbeat.ts
|
|
2966
3235
|
/**
|
|
@@ -3026,14 +3295,44 @@ async function readHostHeartbeat() {
|
|
|
3026
3295
|
} catch {}
|
|
3027
3296
|
}
|
|
3028
3297
|
/**
|
|
3298
|
+
* Parse a WMI `CreationDate` (CIM_DATETIME) into epoch milliseconds; returns
|
|
3299
|
+
* `undefined` for anything that does not match the format, never throws.
|
|
3300
|
+
*
|
|
3301
|
+
* The CIM_DATETIME layout is `yyyymmddHHMMSS.mmmmmmsUUU`: local wall-clock
|
|
3302
|
+
* fields, a 6-digit microsecond fraction, and a **3-digit signed UTC offset
|
|
3303
|
+
* in minutes** (`+000`, `+480` for UTC+8, `-300` for UTC−5). The fields are
|
|
3304
|
+
* therefore *not* UTC — the offset must be subtracted to obtain the epoch:
|
|
3305
|
+
* `+480` means local time runs 480 minutes ahead of UTC, so
|
|
3306
|
+
* `20260923104314.239907+480` is `2026-09-23T02:43:14.000Z`. A 4-digit offset
|
|
3307
|
+
* is not part of the format and is rejected rather than partially matched.
|
|
3308
|
+
*/
|
|
3309
|
+
function parseWmiCreationDate(value) {
|
|
3310
|
+
const m = value.trim().match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d+)([+-]\d{3})$/);
|
|
3311
|
+
if (m === null) return void 0;
|
|
3312
|
+
const [, y, mo, d, h, mi, s, , offset] = m;
|
|
3313
|
+
const [year, month, day, hour, minute, second] = [
|
|
3314
|
+
y,
|
|
3315
|
+
mo,
|
|
3316
|
+
d,
|
|
3317
|
+
h,
|
|
3318
|
+
mi,
|
|
3319
|
+
s
|
|
3320
|
+
].map(Number);
|
|
3321
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) return void 0;
|
|
3322
|
+
if (hour > 23 || minute > 59 || second > 59) return void 0;
|
|
3323
|
+
const epoch = Date.UTC(year, month - 1, day, hour, minute, second) - Number(offset) * 6e4;
|
|
3324
|
+
return Number.isFinite(epoch) ? epoch : void 0;
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3029
3327
|
* Absolute start time (epoch ms) of the process holding `pid`, or `undefined`
|
|
3030
3328
|
* when it cannot be determined (no such PID, platform lacks a readable source).
|
|
3031
3329
|
*
|
|
3032
3330
|
* - macOS / Linux: `ps -o lstart=` prints a local-time "EEE MMM DD HH:MM:SS YYYY";
|
|
3033
3331
|
* `Date.parse` resolves it against the local clock, which matches how
|
|
3034
3332
|
* `registeredAt` (a `Date.now()` absolute value) is expressed.
|
|
3035
|
-
* - Windows:
|
|
3036
|
-
*
|
|
3333
|
+
* - Windows: `wmic` prints a CIM_DATETIME `CreationDate` — local fields plus a
|
|
3334
|
+
* signed minute offset (see {@link parseWmiCreationDate}); the epoch it
|
|
3335
|
+
* yields is comparable to `registeredAt`.
|
|
3037
3336
|
*
|
|
3038
3337
|
* Failures return `undefined` so callers can fall back to plain PID liveness
|
|
3039
3338
|
* rather than mis-report a running host as dead.
|
|
@@ -3041,7 +3340,7 @@ async function readHostHeartbeat() {
|
|
|
3041
3340
|
function processStartTimeMs(pid) {
|
|
3042
3341
|
try {
|
|
3043
3342
|
if (process.platform === "win32") {
|
|
3044
|
-
const
|
|
3343
|
+
const token = execFileSync("wmic", [
|
|
3045
3344
|
"process",
|
|
3046
3345
|
"where",
|
|
3047
3346
|
`processid=${pid}`,
|
|
@@ -3050,11 +3349,9 @@ function processStartTimeMs(pid) {
|
|
|
3050
3349
|
], {
|
|
3051
3350
|
encoding: "utf8",
|
|
3052
3351
|
windowsHide: true
|
|
3053
|
-
}).match(/(\d{
|
|
3054
|
-
if (
|
|
3055
|
-
|
|
3056
|
-
const ms = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(h), Number(mi), Number(s));
|
|
3057
|
-
return Number.isFinite(ms) ? ms : void 0;
|
|
3352
|
+
}).match(/(\d{14})\.(\d+)([+-]\d{3})(?=\s|$)/)?.[0];
|
|
3353
|
+
if (token === void 0) return void 0;
|
|
3354
|
+
return parseWmiCreationDate(token);
|
|
3058
3355
|
}
|
|
3059
3356
|
const out = execFileSync("ps", [
|
|
3060
3357
|
"-o",
|
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.1",
|
|
6
6
|
"author": "corrinehu",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dsh-plugin",
|