dsh-workbuddy-connect 0.6.3 → 0.6.4
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 -1
- package/README.md +1 -1
- package/lib/bin.js +5 -7
- package/lib/client.js +845 -38
- package/lib/{variants-D_oxWP_K.js → host-heartbeat-D9DvyLWz.js} +237 -139
- package/lib/index.d.ts +67 -13
- package/lib/index.js +313 -8
- package/package.json +1 -1
|
@@ -629,6 +629,7 @@ const SESSION_DEAD_MARKERS = ["Offline user session not found", "12153"];
|
|
|
629
629
|
/** Classify an upstream failure from its HTTP status and body excerpt. */
|
|
630
630
|
function classifyUpstreamError(status, body) {
|
|
631
631
|
if (status === 402) return "hard_credit";
|
|
632
|
+
if (status === 401) return "session_dead";
|
|
632
633
|
const lower = body.toLowerCase();
|
|
633
634
|
for (const marker of HARD_CREDIT_MARKERS) if (lower.includes(marker.toLowerCase()) || body.includes(marker)) return "hard_credit";
|
|
634
635
|
for (const marker of SESSION_DEAD_MARKERS) if (body.includes(marker)) return "session_dead";
|
|
@@ -638,6 +639,26 @@ function classifyUpstreamError(status, body) {
|
|
|
638
639
|
if (status >= 400) return "client";
|
|
639
640
|
return "client";
|
|
640
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* Extract a user-facing error message from an upstream JSON response body.
|
|
644
|
+
* Prefers WorkBuddy's localized `displayMsg.zh` / `displayMsg.en`, falling back to `msg`.
|
|
645
|
+
*/
|
|
646
|
+
function extractDisplayErrorMessage(body) {
|
|
647
|
+
const trimmed = body.trim();
|
|
648
|
+
if (!trimmed.startsWith("{")) return void 0;
|
|
649
|
+
try {
|
|
650
|
+
const parsed = JSON.parse(trimmed);
|
|
651
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
652
|
+
const obj = parsed;
|
|
653
|
+
const displayMsg = obj["displayMsg"];
|
|
654
|
+
if (typeof displayMsg === "object" && displayMsg !== null && !Array.isArray(displayMsg)) {
|
|
655
|
+
const localized = displayMsg;
|
|
656
|
+
if (typeof localized["zh"] === "string" && localized["zh"].trim() !== "") return localized["zh"].trim();
|
|
657
|
+
if (typeof localized["en"] === "string" && localized["en"].trim() !== "") return localized["en"].trim();
|
|
658
|
+
}
|
|
659
|
+
if (typeof obj["msg"] === "string" && obj["msg"].trim() !== "") return obj["msg"].trim();
|
|
660
|
+
} catch {}
|
|
661
|
+
}
|
|
641
662
|
/** Region for a login domain; an empty domain means CN (matching upstream tooling). */
|
|
642
663
|
function regionOf(domain) {
|
|
643
664
|
const lowered = domain.trim().toLowerCase();
|
|
@@ -1453,15 +1474,143 @@ const INTERNATIONAL_SYSTEM_PROMPT = "You are a helpful assistant.";
|
|
|
1453
1474
|
*/
|
|
1454
1475
|
const INTERNATIONAL_PROBE_MAX_TOKENS = 16;
|
|
1455
1476
|
//#endregion
|
|
1477
|
+
//#region src/status-paths.ts
|
|
1478
|
+
/** Node-free constants and types shared by the Host and browser halves. */
|
|
1479
|
+
/** Plugin-owned status endpoint consumed by its browser half. */
|
|
1480
|
+
const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-connect/status";
|
|
1481
|
+
/**
|
|
1482
|
+
* Plugin-owned probe control endpoint.
|
|
1483
|
+
*
|
|
1484
|
+
* Separate from the status route because it accepts writes: the status route's
|
|
1485
|
+
* loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
|
|
1486
|
+
* not the same as authorizing a state-changing action. This route therefore
|
|
1487
|
+
* also requires the in-process key the browser half receives with the status
|
|
1488
|
+
* document.
|
|
1489
|
+
*/
|
|
1490
|
+
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-connect/probe";
|
|
1491
|
+
/**
|
|
1492
|
+
* The international (WorkBuddy AI) variant's own pair of routes.
|
|
1493
|
+
*
|
|
1494
|
+
* Kept as separate constants rather than a computed suffix so both halves
|
|
1495
|
+
* reference literal strings: the browser bundle and the host bundle are built
|
|
1496
|
+
* independently, and a shared expression is one build-config drift away from
|
|
1497
|
+
* the desk asking a route the host never mounted.
|
|
1498
|
+
*/
|
|
1499
|
+
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
|
|
1500
|
+
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
|
|
1501
|
+
/**
|
|
1502
|
+
* Same-origin route backing the browser's update reminder.
|
|
1503
|
+
*
|
|
1504
|
+
* Read-only like the status route, so the same loopback Host/Origin gate
|
|
1505
|
+
* applies; it answers public npm/GitHub metadata only and never token
|
|
1506
|
+
* material.
|
|
1507
|
+
*/
|
|
1508
|
+
const WORKBUDDY_UPDATE_PATH = "/plugins/dsh-workbuddy-connect/update";
|
|
1509
|
+
//#endregion
|
|
1510
|
+
//#region src/variants.ts
|
|
1511
|
+
/**
|
|
1512
|
+
* The two WorkBuddy desktop apps this one plugin serves.
|
|
1513
|
+
*
|
|
1514
|
+
* Both products are the same client framework in different regions, and both
|
|
1515
|
+
* write their sign-in into the *same* shared `CodeBuddyExtension` auth
|
|
1516
|
+
* directory — they differ by file basename, base URL, catalog endpoint, and
|
|
1517
|
+
* display identity. Everything that varies between them is collected here as
|
|
1518
|
+
* one descriptor, so no module has to carry its own `if (international)`
|
|
1519
|
+
* branch and a third variant would be a data change rather than a refactor.
|
|
1520
|
+
*
|
|
1521
|
+
* This module is host-side (it names files and env vars). The browser half
|
|
1522
|
+
* takes the same ids and routes from the Node-free `status-paths.ts`, which
|
|
1523
|
+
* stays the single source shared by both halves.
|
|
1524
|
+
*
|
|
1525
|
+
* @module dsh-workbuddy-connect/variants
|
|
1526
|
+
*/
|
|
1527
|
+
/** CN WorkBuddy first: the existing provider keeps its id, paths, and copy. */
|
|
1528
|
+
const WORKBUDDY_VARIANTS = [{
|
|
1529
|
+
id: "workbuddy",
|
|
1530
|
+
displayName: "WorkBuddy",
|
|
1531
|
+
appName: "WorkBuddy",
|
|
1532
|
+
region: "cn",
|
|
1533
|
+
env: "WORKBUDDY_AUTH_FILE",
|
|
1534
|
+
electron: {
|
|
1535
|
+
productName: "WorkBuddy",
|
|
1536
|
+
envVar: "WORKBUDDY_ELECTRON_BIN",
|
|
1537
|
+
macOS: {
|
|
1538
|
+
bundleId: "com.tencent.workbuddy.mac",
|
|
1539
|
+
defaultPath: "/Applications/WorkBuddy.app/Contents/MacOS/Electron"
|
|
1540
|
+
},
|
|
1541
|
+
windows: {
|
|
1542
|
+
displayNamePattern: /^WorkBuddy(?:\s+\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.-]+)?)?$/u,
|
|
1543
|
+
exeBasename: "workbuddy.exe",
|
|
1544
|
+
defaultPathSegments: [
|
|
1545
|
+
"Programs",
|
|
1546
|
+
"WorkBuddy",
|
|
1547
|
+
"WorkBuddy.exe"
|
|
1548
|
+
]
|
|
1549
|
+
}
|
|
1550
|
+
},
|
|
1551
|
+
desktopFilename: "workbuddy-desktop.info",
|
|
1552
|
+
ownFilename: ".workbuddy-auth.json",
|
|
1553
|
+
probeFilename: ".workbuddy-probe.json",
|
|
1554
|
+
catalogFilename: ".workbuddy-catalog.json",
|
|
1555
|
+
visibilityFilename: ".workbuddy-model-visibility.json",
|
|
1556
|
+
statusPath: WORKBUDDY_STATUS_PATH,
|
|
1557
|
+
probePath: WORKBUDDY_PROBE_PATH
|
|
1558
|
+
}, {
|
|
1559
|
+
id: "workbuddy-ai",
|
|
1560
|
+
displayName: "WorkBuddy AI",
|
|
1561
|
+
appName: "WorkBuddy AI",
|
|
1562
|
+
region: "global",
|
|
1563
|
+
env: "WORKBUDDY_AI_AUTH_FILE",
|
|
1564
|
+
electron: {
|
|
1565
|
+
productName: "WorkBuddy AI",
|
|
1566
|
+
envVar: "WORKBUDDY_AI_ELECTRON_BIN",
|
|
1567
|
+
macOS: {
|
|
1568
|
+
bundleId: "com.workbuddy.workbuddy-ai",
|
|
1569
|
+
defaultPath: "/Applications/WorkBuddy AI.app/Contents/MacOS/Electron"
|
|
1570
|
+
},
|
|
1571
|
+
windows: {
|
|
1572
|
+
displayNamePattern: /^WorkBuddy AI(?:\s+\d+(?:\.\d+)+(?:[-+][0-9A-Za-z.-]+)?)?$/u,
|
|
1573
|
+
exeBasename: "workbuddyai.exe"
|
|
1574
|
+
}
|
|
1575
|
+
},
|
|
1576
|
+
desktopFilename: "workbuddy-desktop-ai.info",
|
|
1577
|
+
ownFilename: ".workbuddy-ai-auth.json",
|
|
1578
|
+
probeFilename: ".workbuddy-ai-probe.json",
|
|
1579
|
+
catalogFilename: ".workbuddy-ai-catalog.json",
|
|
1580
|
+
visibilityFilename: ".workbuddy-ai-model-visibility.json",
|
|
1581
|
+
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
1582
|
+
probePath: WORKBUDDY_AI_PROBE_PATH
|
|
1583
|
+
}];
|
|
1584
|
+
/** The CN variant; the plugin's long-standing default and compatibility anchor. */
|
|
1585
|
+
const CN_VARIANT = WORKBUDDY_VARIANTS[0];
|
|
1586
|
+
/** The international variant. */
|
|
1587
|
+
const AI_VARIANT = WORKBUDDY_VARIANTS[1];
|
|
1588
|
+
/** Look up a variant by provider id. */
|
|
1589
|
+
function variantFor(id) {
|
|
1590
|
+
return WORKBUDDY_VARIANTS.find((variant) => variant.id === id);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* The Electron profile a variant resolves with.
|
|
1594
|
+
*
|
|
1595
|
+
* Callers inside the plugin pass their known-complete variants; descriptors
|
|
1596
|
+
* assembled outside (the type is public and predates the profile) fall back
|
|
1597
|
+
* by variant id, so an id-less or unknown custom variant stays on the CN
|
|
1598
|
+
* product — the same default the store's other legacy fields assume.
|
|
1599
|
+
*/
|
|
1600
|
+
function electronProfileFor(variant) {
|
|
1601
|
+
return variant?.electron ?? (variant?.id === AI_VARIANT.id ? AI_VARIANT : CN_VARIANT).electron;
|
|
1602
|
+
}
|
|
1603
|
+
//#endregion
|
|
1456
1604
|
//#region src/desktop-credential-protection.ts
|
|
1457
1605
|
/**
|
|
1458
1606
|
* WorkBuddy 5.6.x at-rest credential protection: classification, key
|
|
1459
|
-
* resolution, and field decryption for the desktop
|
|
1607
|
+
* resolution, and field decryption for the desktop apps' encrypted auth files.
|
|
1460
1608
|
*
|
|
1461
|
-
* Since WorkBuddy 5.6
|
|
1462
|
-
* `auth.refreshToken` at rest
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
1609
|
+
* Since WorkBuddy 5.6 both desktop apps (CN and international, which followed
|
|
1610
|
+
* in 5.6.2) encrypt `auth.accessToken` and `auth.refreshToken` at rest
|
|
1611
|
+
* (`buildPolicy: "fields"`, on by default), so the plugin reads
|
|
1612
|
+
* `{$wbEncrypted:1, envelope}` wrappers instead of token strings (issues
|
|
1613
|
+
* #39/#40, #59/#60). Everything needed to open them lives on the same machine:
|
|
1465
1614
|
*
|
|
1466
1615
|
* - the sealed payload (`{version:1, atRestSecretKey}`) comes from the
|
|
1467
1616
|
* WorkBuddy-modified Electron's private `workbuddyStorage` binding, reached
|
|
@@ -1479,21 +1628,17 @@ const INTERNATIONAL_PROBE_MAX_TOKENS = 16;
|
|
|
1479
1628
|
*
|
|
1480
1629
|
* @module dsh-workbuddy-connect/desktop-credential-protection
|
|
1481
1630
|
*/
|
|
1482
|
-
/** Env variable that overrides the WorkBuddy Electron binary used as the key helper. */
|
|
1483
|
-
const WORKBUDDY_ELECTRON_BIN_ENV = "WORKBUDDY_ELECTRON_BIN";
|
|
1484
|
-
/** Platform-default Electron binaries confirmed for the CN app (5.6.2). */
|
|
1485
|
-
const MACOS_ELECTRON_PATH = "/Applications/WorkBuddy.app/Contents/MacOS/Electron";
|
|
1486
1631
|
/**
|
|
1487
|
-
* The Electron binary
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1632
|
+
* The platform-default Electron binary for one product, or `undefined` where
|
|
1633
|
+
* none is verified. macOS defaults come from each product's measured layout;
|
|
1634
|
+
* the Windows default needs `LOCALAPPDATA`, and the international app has no
|
|
1635
|
+
* verified default location at all — registry discovery only, never a guess.
|
|
1491
1636
|
*/
|
|
1492
|
-
function defaultWorkBuddyElectronPath(platform = process.platform) {
|
|
1493
|
-
if (platform === "darwin") return
|
|
1494
|
-
if (platform !== "win32") return void 0;
|
|
1637
|
+
function defaultWorkBuddyElectronPath(product, platform = process.platform) {
|
|
1638
|
+
if (platform === "darwin") return product.macOS.defaultPath;
|
|
1639
|
+
if (platform !== "win32" || product.windows.defaultPathSegments === void 0) return void 0;
|
|
1495
1640
|
const localAppData = process.env.LOCALAPPDATA?.trim();
|
|
1496
|
-
return localAppData === void 0 || localAppData === "" ? void 0 : join(localAppData,
|
|
1641
|
+
return localAppData === void 0 || localAppData === "" ? void 0 : join(localAppData, ...product.windows.defaultPathSegments);
|
|
1497
1642
|
}
|
|
1498
1643
|
/** Distinct key ids across the wrapped fields, in field order. */
|
|
1499
1644
|
function keyIdsOf(fields) {
|
|
@@ -1674,15 +1819,27 @@ function parseAtRestPayload(text) {
|
|
|
1674
1819
|
function deriveProtectorKey(secret) {
|
|
1675
1820
|
return createHash("sha256").update(secret, "utf8").digest();
|
|
1676
1821
|
}
|
|
1677
|
-
/**
|
|
1678
|
-
|
|
1679
|
-
|
|
1822
|
+
/**
|
|
1823
|
+
* Select discovery by platform. Both products have verified layouts on macOS
|
|
1824
|
+
* and Windows (the international app is registry-only there), so discovery no
|
|
1825
|
+
* longer gates on region; Linux and others stay `none`.
|
|
1826
|
+
*/
|
|
1827
|
+
function electronDiscoveryFor(platform = process.platform) {
|
|
1680
1828
|
if (platform === "darwin") return "macos-workbuddy";
|
|
1681
1829
|
if (platform === "win32") return "windows-workbuddy";
|
|
1682
1830
|
return "none";
|
|
1683
1831
|
}
|
|
1684
|
-
/**
|
|
1685
|
-
|
|
1832
|
+
/**
|
|
1833
|
+
* The at-rest key provider one variant's store should use. Shared by the
|
|
1834
|
+
* plugin host and the CLI entry so the browser card and `doctor`/`status` can
|
|
1835
|
+
* never disagree about which binary a variant resolves.
|
|
1836
|
+
*/
|
|
1837
|
+
function atRestKeyProviderFor(variant) {
|
|
1838
|
+
return new WorkBuddyAtRestKeyProvider({
|
|
1839
|
+
product: electronProfileFor(variant),
|
|
1840
|
+
discovery: electronDiscoveryFor()
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1686
1843
|
/** Absolute tool paths: never resolved through PATH, which a user can change. */
|
|
1687
1844
|
const MDFIND_BIN = "/usr/bin/mdfind";
|
|
1688
1845
|
const PLUTIL_BIN = "/usr/bin/plutil";
|
|
@@ -1693,7 +1850,6 @@ const WINDOWS_REGISTRY_ROOTS = [
|
|
|
1693
1850
|
];
|
|
1694
1851
|
const WINDOWS_REGISTRY_OUTPUT_MAX_BYTES = 1048576;
|
|
1695
1852
|
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;
|
|
1697
1853
|
/** One discovery subprocess's own limits; see {@link WorkBuddyAtRestKeyProviderOptions}. */
|
|
1698
1854
|
const WORKBUDDY_DISCOVERY_STEP_TIMEOUT_MS = 3e3;
|
|
1699
1855
|
const MDFIND_MAX_OUTPUT_BYTES = 1048576;
|
|
@@ -1710,8 +1866,12 @@ var DiscoveryIncompleteError = class extends Error {};
|
|
|
1710
1866
|
* identity. Every failure that means "we could not tell" — a missing tool, a
|
|
1711
1867
|
* timeout, an oversized answer — is raised as {@link DiscoveryIncompleteError}
|
|
1712
1868
|
* so it can never be silently read as "no such app".
|
|
1869
|
+
*
|
|
1870
|
+
* `bundleId` is the product being searched for; the Spotlight query and the
|
|
1871
|
+
* caller's identity comparison both use it, so the two can never disagree
|
|
1872
|
+
* about which app they are looking for.
|
|
1713
1873
|
*/
|
|
1714
|
-
function workBuddyDiscoveryTools() {
|
|
1874
|
+
function workBuddyDiscoveryTools(bundleId) {
|
|
1715
1875
|
const runTool = (bin, args, maxBytes, signal) => new Promise((resolve, reject) => {
|
|
1716
1876
|
if (signal.aborted) {
|
|
1717
1877
|
reject(new DiscoveryIncompleteError(`${bin} was not started: the discovery budget was already spent`));
|
|
@@ -1743,7 +1903,7 @@ function workBuddyDiscoveryTools() {
|
|
|
1743
1903
|
});
|
|
1744
1904
|
return {
|
|
1745
1905
|
findApps: async (signal) => {
|
|
1746
|
-
return (await runTool(MDFIND_BIN, [`kMDItemCFBundleIdentifier == '${
|
|
1906
|
+
return (await runTool(MDFIND_BIN, [`kMDItemCFBundleIdentifier == '${bundleId}'`], MDFIND_MAX_OUTPUT_BYTES, signal)).split("\n").map((line) => line.trim()).filter((line) => line.endsWith(".app"));
|
|
1747
1907
|
},
|
|
1748
1908
|
bundleIdentifier: async (bundlePath, signal) => {
|
|
1749
1909
|
try {
|
|
@@ -1840,6 +2000,7 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
1840
2000
|
* a different app.
|
|
1841
2001
|
*/
|
|
1842
2002
|
explicitPath;
|
|
2003
|
+
product;
|
|
1843
2004
|
defaultPath;
|
|
1844
2005
|
discovery;
|
|
1845
2006
|
tools;
|
|
@@ -1857,14 +2018,15 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
1857
2018
|
discoveredPath;
|
|
1858
2019
|
cache;
|
|
1859
2020
|
inflight;
|
|
1860
|
-
constructor(options
|
|
1861
|
-
|
|
2021
|
+
constructor(options) {
|
|
2022
|
+
this.product = options.product;
|
|
2023
|
+
const fromEnv = process.env[options.product.envVar]?.trim();
|
|
1862
2024
|
const envPath = fromEnv === void 0 || fromEnv === "" ? void 0 : fromEnv;
|
|
1863
2025
|
this.explicitPath = options.electronPath ?? envPath;
|
|
1864
2026
|
this.discovery = options.discovery ?? "none";
|
|
1865
2027
|
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;
|
|
1867
|
-
this.tools = options.tools ?? workBuddyDiscoveryTools();
|
|
2028
|
+
this.defaultPath = this.discovery === "none" ? void 0 : options.defaultElectronPath === void 0 ? defaultWorkBuddyElectronPath(options.product, this.platform) : options.defaultElectronPath ?? void 0;
|
|
2029
|
+
this.tools = options.tools ?? workBuddyDiscoveryTools(options.product.macOS.bundleId);
|
|
1868
2030
|
this.windowsTools = options.windowsTools ?? workBuddyWindowsDiscoveryTools();
|
|
1869
2031
|
this.discoveryBudgetMs = options.discoveryBudgetMs ?? 1e4;
|
|
1870
2032
|
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
@@ -1923,10 +2085,10 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
1923
2085
|
*/
|
|
1924
2086
|
async resolveElectronPath() {
|
|
1925
2087
|
if (this.explicitPath !== void 0) {
|
|
1926
|
-
if (!isExecutable(this.explicitPath)) throw new WorkBuddyElectronPathError("electron-path-invalid", `the configured
|
|
2088
|
+
if (!isExecutable(this.explicitPath)) throw new WorkBuddyElectronPathError("electron-path-invalid", `the configured ${this.product.productName} Electron binary is not available at ${this.explicitPath}; check ${this.product.envVar} or unset it to let the plugin look for the app itself`);
|
|
1927
2089
|
return this.explicitPath;
|
|
1928
2090
|
}
|
|
1929
|
-
if (this.discovery === "none") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no
|
|
2091
|
+
if (this.discovery === "none") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`);
|
|
1930
2092
|
if (this.defaultPath !== void 0 && isExecutable(this.defaultPath)) return this.defaultPath;
|
|
1931
2093
|
if (this.discoveredPath !== void 0) {
|
|
1932
2094
|
if (isExecutable(this.discoveredPath)) return this.discoveredPath;
|
|
@@ -1937,15 +2099,15 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
1937
2099
|
return found;
|
|
1938
2100
|
}
|
|
1939
2101
|
/**
|
|
1940
|
-
* Resolve
|
|
1941
|
-
* before it can be executed.
|
|
2102
|
+
* Resolve this product's app through Spotlight, then prove each candidate's
|
|
2103
|
+
* identity before it can be executed.
|
|
1942
2104
|
*
|
|
1943
2105
|
* The whole flow shares one budget: a hang in one candidate must not extend
|
|
1944
2106
|
* the wait for the others, and running out of budget is reported as an
|
|
1945
2107
|
* unfinished check rather than an absent app.
|
|
1946
2108
|
*/
|
|
1947
2109
|
async discoverMacosApp() {
|
|
1948
|
-
if (this.platform !== "darwin") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no
|
|
2110
|
+
if (this.platform !== "darwin") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`);
|
|
1949
2111
|
const controller = new AbortController();
|
|
1950
2112
|
const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
|
|
1951
2113
|
try {
|
|
@@ -1953,7 +2115,7 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
1953
2115
|
try {
|
|
1954
2116
|
candidates = await this.tools.findApps(controller.signal);
|
|
1955
2117
|
} catch {
|
|
1956
|
-
throw discoveryIncomplete("the app search did not complete");
|
|
2118
|
+
throw discoveryIncomplete(this.product.productName, "the app search did not complete");
|
|
1957
2119
|
}
|
|
1958
2120
|
const seen = /* @__PURE__ */ new Map();
|
|
1959
2121
|
let unresolved = false;
|
|
@@ -1976,7 +2138,7 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
1976
2138
|
unresolved = true;
|
|
1977
2139
|
continue;
|
|
1978
2140
|
}
|
|
1979
|
-
if (bundleIdentifier !==
|
|
2141
|
+
if (bundleIdentifier !== this.product.macOS.bundleId) continue;
|
|
1980
2142
|
const electronPath = join(candidate, "Contents", "MacOS", "Electron");
|
|
1981
2143
|
if (!isExecutable(electronPath)) continue;
|
|
1982
2144
|
let identity;
|
|
@@ -1998,9 +2160,12 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
1998
2160
|
...version === void 0 ? {} : { version }
|
|
1999
2161
|
});
|
|
2000
2162
|
}
|
|
2001
|
-
if (seen.size > 1)
|
|
2002
|
-
|
|
2003
|
-
|
|
2163
|
+
if (seen.size > 1) {
|
|
2164
|
+
const listed = [...seen.values()].map((app) => ` - ${app.bundlePath}${app.version === void 0 ? "" : ` (${app.version})`}`).join("\n");
|
|
2165
|
+
throw new WorkBuddyElectronPathError("electron-binary-ambiguous", `more than one ${this.product.productName} application was found, so none was chosen:\n${listed}\n set ${this.product.envVar} to the one to use`);
|
|
2166
|
+
}
|
|
2167
|
+
if (unresolved) throw discoveryIncomplete(this.product.productName, "some candidates could not be checked");
|
|
2168
|
+
if (seen.size === 0) throw new WorkBuddyElectronPathError("electron-binary-not-found", `no ${this.product.productName} application was found in the default location or the system index; if it is installed elsewhere, it may not be indexed yet; set ${this.product.envVar} to the app's Electron binary`);
|
|
2004
2169
|
return [...seen.values()][0].electronPath;
|
|
2005
2170
|
} finally {
|
|
2006
2171
|
clearTimeout(timer);
|
|
@@ -2008,12 +2173,13 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
2008
2173
|
}
|
|
2009
2174
|
}
|
|
2010
2175
|
/**
|
|
2011
|
-
* Resolve
|
|
2012
|
-
* provide hints, not trust: every DisplayIcon candidate must still
|
|
2013
|
-
*
|
|
2176
|
+
* Resolve this product's app through Windows uninstall records. Registry
|
|
2177
|
+
* entries provide hints, not trust: every DisplayIcon candidate must still
|
|
2178
|
+
* be the product's Electron binary with the known Electron layout before
|
|
2179
|
+
* execution.
|
|
2014
2180
|
*/
|
|
2015
2181
|
async discoverWindowsApp() {
|
|
2016
|
-
if (this.platform !== "win32") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no
|
|
2182
|
+
if (this.platform !== "win32") throw new WorkBuddyElectronPathError("electron-binary-unavailable", `no ${this.product.productName} Electron binary is configured for this platform; set ${this.product.envVar} to the app's Electron binary`);
|
|
2017
2183
|
const controller = new AbortController();
|
|
2018
2184
|
const timer = setTimeout(() => controller.abort(), this.discoveryBudgetMs);
|
|
2019
2185
|
try {
|
|
@@ -2027,13 +2193,13 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
2027
2193
|
unresolved = true;
|
|
2028
2194
|
continue;
|
|
2029
2195
|
}
|
|
2030
|
-
const parsed = parseWindowsRegistryOutput(output);
|
|
2196
|
+
const parsed = parseWindowsRegistryOutput(output, this.product.windows.displayNamePattern);
|
|
2031
2197
|
candidates.push(...parsed.candidates);
|
|
2032
2198
|
unresolved ||= parsed.incomplete;
|
|
2033
2199
|
}
|
|
2034
2200
|
const seen = /* @__PURE__ */ new Map();
|
|
2035
2201
|
for (const candidate of new Set(candidates)) {
|
|
2036
|
-
const inspection = inspectWindowsElectronCandidate(candidate, this.platform);
|
|
2202
|
+
const inspection = inspectWindowsElectronCandidate(candidate, this.platform, this.product.windows.exeBasename);
|
|
2037
2203
|
if (inspection === "unresolved") {
|
|
2038
2204
|
unresolved = true;
|
|
2039
2205
|
continue;
|
|
@@ -2041,9 +2207,12 @@ function workBuddyWindowsDiscoveryTools() {
|
|
|
2041
2207
|
if (inspection === void 0) continue;
|
|
2042
2208
|
seen.set(inspection.identity, inspection.electronPath);
|
|
2043
2209
|
}
|
|
2044
|
-
if (unresolved) throw discoveryIncomplete("some registry entries or candidates could not be checked");
|
|
2045
|
-
if (seen.size > 1)
|
|
2046
|
-
|
|
2210
|
+
if (unresolved) throw discoveryIncomplete(this.product.productName, "some registry entries or candidates could not be checked");
|
|
2211
|
+
if (seen.size > 1) {
|
|
2212
|
+
const listed = [...seen.values()].map((path) => ` - ${path}`).join("\n");
|
|
2213
|
+
throw new WorkBuddyElectronPathError("electron-binary-ambiguous", `more than one ${this.product.productName} application was found, so none was chosen:\n${listed}\n set ${this.product.envVar} to the one to use`);
|
|
2214
|
+
}
|
|
2215
|
+
if (seen.size === 0) throw new WorkBuddyElectronPathError("electron-binary-not-found", `no usable ${this.product.productName} Electron binary was found in the default location or Windows uninstall records; set ${this.product.envVar} to the app's Electron binary`);
|
|
2047
2216
|
return [...seen.values()][0];
|
|
2048
2217
|
} finally {
|
|
2049
2218
|
clearTimeout(timer);
|
|
@@ -2083,8 +2252,15 @@ function windowsRegistryKeyMissing(stderr) {
|
|
|
2083
2252
|
const detail = stderr.trim();
|
|
2084
2253
|
return /^ERROR:\s*The system was unable to find the specified registry key or value\.?$/iu.test(detail) || /^错误[::]\s*系统找不到指定的注册表项或值[。.]?$/u.test(detail);
|
|
2085
2254
|
}
|
|
2086
|
-
/**
|
|
2087
|
-
|
|
2255
|
+
/**
|
|
2256
|
+
* Parse the value columns emitted by `reg query ... /s`.
|
|
2257
|
+
*
|
|
2258
|
+
* Entries are filtered by the product's DisplayName pattern *before* the
|
|
2259
|
+
* DisplayIcon is judged, so another product's records — however broken — are
|
|
2260
|
+
* excluded as decisively not ours and can never mark this product's search
|
|
2261
|
+
* incomplete.
|
|
2262
|
+
*/
|
|
2263
|
+
function parseWindowsRegistryOutput(output, displayNamePattern) {
|
|
2088
2264
|
const entries = /* @__PURE__ */ new Map();
|
|
2089
2265
|
let currentKey;
|
|
2090
2266
|
for (const line of output.split(/\r?\n/u)) {
|
|
@@ -2107,7 +2283,7 @@ function parseWindowsRegistryOutput(output) {
|
|
|
2107
2283
|
let incomplete = false;
|
|
2108
2284
|
for (const entry of entries.values()) {
|
|
2109
2285
|
if (entry.displayName === void 0) continue;
|
|
2110
|
-
if (!
|
|
2286
|
+
if (!displayNamePattern.test(entry.displayName.trim())) continue;
|
|
2111
2287
|
const displayIcon = entry.displayIcon === void 0 ? void 0 : parseWindowsDisplayIcon(entry.displayIcon);
|
|
2112
2288
|
if (displayIcon === void 0) incomplete = true;
|
|
2113
2289
|
else candidates.push(displayIcon);
|
|
@@ -2136,11 +2312,12 @@ function parseWindowsDisplayIcon(value) {
|
|
|
2136
2312
|
return /\.exe$/iu.test(path) ? path : void 0;
|
|
2137
2313
|
}
|
|
2138
2314
|
/**
|
|
2139
|
-
* Validate the known
|
|
2140
|
-
* exclusion; `unresolved` is reserved for errors that prevent
|
|
2315
|
+
* Validate the known Windows layout for the product's exe. `undefined` is a
|
|
2316
|
+
* decidable exclusion; `unresolved` is reserved for errors that prevent
|
|
2317
|
+
* checking.
|
|
2141
2318
|
*/
|
|
2142
|
-
function inspectWindowsElectronCandidate(electronPath, platform) {
|
|
2143
|
-
if (platform !== "win32" || basename(electronPath).toLowerCase() !==
|
|
2319
|
+
function inspectWindowsElectronCandidate(electronPath, platform, exeBasename) {
|
|
2320
|
+
if (platform !== "win32" || basename(electronPath).toLowerCase() !== exeBasename) return void 0;
|
|
2144
2321
|
let binaryStat;
|
|
2145
2322
|
try {
|
|
2146
2323
|
binaryStat = statSync(electronPath);
|
|
@@ -2209,8 +2386,8 @@ function reasonCodeOf(error) {
|
|
|
2209
2386
|
function isENOENT$1(error) {
|
|
2210
2387
|
return error?.code === "ENOENT";
|
|
2211
2388
|
}
|
|
2212
|
-
function discoveryIncomplete(detail) {
|
|
2213
|
-
return new WorkBuddyElectronPathError("electron-discovery-incomplete", `the
|
|
2389
|
+
function discoveryIncomplete(productName, detail) {
|
|
2390
|
+
return new WorkBuddyElectronPathError("electron-discovery-incomplete", `the ${productName} application search did not finish (${detail}); this is not proof that the app is missing`);
|
|
2214
2391
|
}
|
|
2215
2392
|
/**
|
|
2216
2393
|
* The helper: run inside WorkBuddy's Electron as plain Node, where the
|
|
@@ -2427,7 +2604,7 @@ var WorkBuddyCredentialStore = class {
|
|
|
2427
2604
|
this.refresh = options.refresh;
|
|
2428
2605
|
this.refreshMarginMs = options.refreshMarginMs ?? 3e5;
|
|
2429
2606
|
this.ownPath = options.ownPath ?? (options.variant ? join(resolveDshHome(), options.variant.ownFilename) : workbuddyOwnAuthPath());
|
|
2430
|
-
this.keyProvider = options.keyProvider ?? new WorkBuddyAtRestKeyProvider();
|
|
2607
|
+
this.keyProvider = options.keyProvider ?? new WorkBuddyAtRestKeyProvider({ product: electronProfileFor(options.variant) });
|
|
2431
2608
|
this.desktopPathOverride = options.desktopPath;
|
|
2432
2609
|
}
|
|
2433
2610
|
/**
|
|
@@ -3431,33 +3608,8 @@ var WorkBuddyCatalog = class {
|
|
|
3431
3608
|
}
|
|
3432
3609
|
};
|
|
3433
3610
|
//#endregion
|
|
3434
|
-
//#region src/status-paths.ts
|
|
3435
|
-
/** Node-free constants and types shared by the Host and browser halves. */
|
|
3436
|
-
/** Plugin-owned status endpoint consumed by its browser half. */
|
|
3437
|
-
const WORKBUDDY_STATUS_PATH = "/plugins/dsh-workbuddy-connect/status";
|
|
3438
|
-
/**
|
|
3439
|
-
* Plugin-owned probe control endpoint.
|
|
3440
|
-
*
|
|
3441
|
-
* Separate from the status route because it accepts writes: the status route's
|
|
3442
|
-
* loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
|
|
3443
|
-
* not the same as authorizing a state-changing action. This route therefore
|
|
3444
|
-
* also requires the in-process key the browser half receives with the status
|
|
3445
|
-
* document.
|
|
3446
|
-
*/
|
|
3447
|
-
const WORKBUDDY_PROBE_PATH = "/plugins/dsh-workbuddy-connect/probe";
|
|
3448
|
-
/**
|
|
3449
|
-
* The international (WorkBuddy AI) variant's own pair of routes.
|
|
3450
|
-
*
|
|
3451
|
-
* Kept as separate constants rather than a computed suffix so both halves
|
|
3452
|
-
* reference literal strings: the browser bundle and the host bundle are built
|
|
3453
|
-
* independently, and a shared expression is one build-config drift away from
|
|
3454
|
-
* the desk asking a route the host never mounted.
|
|
3455
|
-
*/
|
|
3456
|
-
const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
|
|
3457
|
-
const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
|
|
3458
|
-
//#endregion
|
|
3459
3611
|
//#region src/version.ts
|
|
3460
|
-
const WORKBUDDY_CONNECT_VERSION = "0.6.
|
|
3612
|
+
const WORKBUDDY_CONNECT_VERSION = "0.6.4";
|
|
3461
3613
|
//#endregion
|
|
3462
3614
|
//#region src/host-heartbeat.ts
|
|
3463
3615
|
/**
|
|
@@ -3628,58 +3780,4 @@ function isHeartbeatProcessAlive(heartbeat) {
|
|
|
3628
3780
|
return startAtMs <= heartbeat.registeredAt;
|
|
3629
3781
|
}
|
|
3630
3782
|
//#endregion
|
|
3631
|
-
|
|
3632
|
-
/**
|
|
3633
|
-
* The two WorkBuddy desktop apps this one plugin serves.
|
|
3634
|
-
*
|
|
3635
|
-
* Both products are the same client framework in different regions, and both
|
|
3636
|
-
* write their sign-in into the *same* shared `CodeBuddyExtension` auth
|
|
3637
|
-
* directory — they differ by file basename, base URL, catalog endpoint, and
|
|
3638
|
-
* display identity. Everything that varies between them is collected here as
|
|
3639
|
-
* one descriptor, so no module has to carry its own `if (international)`
|
|
3640
|
-
* branch and a third variant would be a data change rather than a refactor.
|
|
3641
|
-
*
|
|
3642
|
-
* This module is host-side (it names files and env vars). The browser half
|
|
3643
|
-
* takes the same ids and routes from the Node-free `status-paths.ts`, which
|
|
3644
|
-
* stays the single source shared by both halves.
|
|
3645
|
-
*
|
|
3646
|
-
* @module dsh-workbuddy-connect/variants
|
|
3647
|
-
*/
|
|
3648
|
-
/** CN WorkBuddy first: the existing provider keeps its id, paths, and copy. */
|
|
3649
|
-
const WORKBUDDY_VARIANTS = [{
|
|
3650
|
-
id: "workbuddy",
|
|
3651
|
-
displayName: "WorkBuddy",
|
|
3652
|
-
appName: "WorkBuddy",
|
|
3653
|
-
region: "cn",
|
|
3654
|
-
env: "WORKBUDDY_AUTH_FILE",
|
|
3655
|
-
desktopFilename: "workbuddy-desktop.info",
|
|
3656
|
-
ownFilename: ".workbuddy-auth.json",
|
|
3657
|
-
probeFilename: ".workbuddy-probe.json",
|
|
3658
|
-
catalogFilename: ".workbuddy-catalog.json",
|
|
3659
|
-
visibilityFilename: ".workbuddy-model-visibility.json",
|
|
3660
|
-
statusPath: WORKBUDDY_STATUS_PATH,
|
|
3661
|
-
probePath: WORKBUDDY_PROBE_PATH
|
|
3662
|
-
}, {
|
|
3663
|
-
id: "workbuddy-ai",
|
|
3664
|
-
displayName: "WorkBuddy AI",
|
|
3665
|
-
appName: "WorkBuddy AI",
|
|
3666
|
-
region: "global",
|
|
3667
|
-
env: "WORKBUDDY_AI_AUTH_FILE",
|
|
3668
|
-
desktopFilename: "workbuddy-desktop-ai.info",
|
|
3669
|
-
ownFilename: ".workbuddy-ai-auth.json",
|
|
3670
|
-
probeFilename: ".workbuddy-ai-probe.json",
|
|
3671
|
-
catalogFilename: ".workbuddy-ai-catalog.json",
|
|
3672
|
-
visibilityFilename: ".workbuddy-ai-model-visibility.json",
|
|
3673
|
-
statusPath: WORKBUDDY_AI_STATUS_PATH,
|
|
3674
|
-
probePath: WORKBUDDY_AI_PROBE_PATH
|
|
3675
|
-
}];
|
|
3676
|
-
/** The CN variant; the plugin's long-standing default and compatibility anchor. */
|
|
3677
|
-
const CN_VARIANT = WORKBUDDY_VARIANTS[0];
|
|
3678
|
-
/** The international variant. */
|
|
3679
|
-
const AI_VARIANT = WORKBUDDY_VARIANTS[1];
|
|
3680
|
-
/** Look up a variant by provider id. */
|
|
3681
|
-
function variantFor(id) {
|
|
3682
|
-
return WORKBUDDY_VARIANTS.find((variant) => variant.id === id);
|
|
3683
|
-
}
|
|
3684
|
-
//#endregion
|
|
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 };
|
|
3783
|
+
export { extractDisplayErrorMessage as A, CN_APP_VERSION_FILENAME as B, WORKBUDDY_VARIANTS as C, WORKBUDDY_UPDATE_PATH as D, WORKBUDDY_STATUS_PATH as E, prepareInternationalChatBody as F, resolveChatIdentity as G, chatUserAgent as H, regionOf as I, appUserAgent as J, validCliVersion as K, PROBE_EFFORT_CANDIDATES as L, normalizeCredits as M, parseModelCatalog as N, WorkBuddyUpstreamClient as O, prepareChatBody as P, validAppVersion as Q, probeModel as R, CN_VARIANT as S, WORKBUDDY_PROBE_PATH as T, fallbackChatIdentity as U, FALLBACK_CN_APP_VERSION as V, readCliVersion as W, readBundleVersion as X, installedAppVersion as Y, resolveAppVersion as Z, desktopAuthCandidatesFor as _, readHostHeartbeat as a, atRestKeyProviderFor as b, WORKBUDDY_CONNECT_VERSION as c, WorkBuddyCatalog as d, WORKBUDDY_AUTH_FILENAME as f, defaultDesktopAuthPath as g, defaultDesktopAuthCandidates as h, processStartTimeMs as i, modelWithCurrentPromotion as j, classifyUpstreamError as k, FALLBACK_WORKBUDDY_AI_MODELS as l, WorkBuddyCredentialStore as m, clearHostHeartbeat as n, workbuddyHostHeartbeatPath as o, WORKBUDDY_AUTH_FILE_ENV as p, WORKBUDDY_APP_VERSION_FILENAME as q, isHeartbeatProcessAlive as r, writeHostHeartbeat as s, WORKBUDDY_HOST_HEARTBEAT_FILENAME as t, FALLBACK_WORKBUDDY_MODELS as u, parseWorkBuddyAuth as v, variantFor as w, AI_VARIANT as x, workbuddyOwnAuthPath as y, randomSentinel as z };
|