dsh-workbuddy-xdpool 1.7.3 → 1.7.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/CHANGELOG.md CHANGED
@@ -4,6 +4,51 @@
4
4
 
5
5
  版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
6
6
 
7
+ ## 1.7.4 — 装在中文文件夹里的 WorkBuddy,终于找得到了
8
+
9
+ > 「腾讯」两个字,让插件整整看不见一个装好、登录着、正在运行的 WorkBuddy。
10
+
11
+ **你会看到**:插件卡片报「找不到 WorkBuddy 桌面端,无法取得密钥」,可你明明:程序装着、还开着、凭据文件就在那儿躺着。中文 Windows 上,只要**安装路径里带中文**(比如 `D:\ruanjian\anzhuang\腾讯\WorkBuddy\`)就会中招——国内用户把软件装进中文目录太常见了。
12
+
13
+ **为什么**:插件去注册表里问「WorkBuddy 装哪儿了」,用的是 `reg.exe`,而 `reg.exe` 按**系统控制台代码页**输出——中文系统就是 GBK(936)。我们这边却当成 UTF-8 去读。于是:
14
+
15
+ ```
16
+ 实际路径:D:\ruanjian\anzhuang\腾讯\WorkBuddy\WorkBuddy.exe
17
+ 读出来: D:\ruanjian\anzhuang\��Ѷ\WorkBuddy\WorkBuddy.exe ← 存的什么鬼
18
+ ```
19
+
20
+ `腾讯` 变成 `��Ѷ`,这个路径当然**不存在**。所有候选路径全部落空,插件就得出「没装」的结论。可笑的是它其实**探到了**注册表记录,只是**读成了乱码**。
21
+
22
+ 这个坑和之前那个「装到非系统盘」是两回事——那个是找不到记录,这个是找到了但读不对。
23
+
24
+ **怎么修的**:
25
+
26
+ - **改走 PowerShell 读注册表**(主路径)。`Get-ItemProperty` 直接调注册表 API,**根本不过控制台代码页那一关**,中文路径原样拿到;
27
+ - **保留 `reg.exe` 作后备**(万一机器上 PowerShell 不可用),但**不再假定 UTF-8**:按字节读进来,逐个尝试各种编码,并且**用「这个路径是否真的存在」来判定哪个解码是对的**——这是唯一能区分「正确字节」和「看着像那么回事的乱码」的办法。
28
+
29
+ **顺带修了报告里提到的两点**:
30
+
31
+ - `DisplayIcon` 和 `InstallLocation` **两个字段都读**(不少安装包的 `InstallLocation` 是空的,只有 `DisplayIcon` 有值),并且清理 `",0` 后缀的逻辑保持不变;
32
+ - `doctor` **现在会把探测到的每一条路径原样列出来**,前面标 `✓`/`·` 表示存不存在。以前它压根不显示这些路径——而这恰恰是排查「装了却找不到」最该看的东西。**乱码路径现在一眼就能看见**,不用再靠猜。
33
+
34
+ ### 验证
35
+
36
+ - 类型检查全绿,测试 **278 项全绿**(+2)。
37
+ - 新增 `tests/windows-registry-encoding.test.ts`:在本机**真实创建**一个含中文的目录(`D:\...\腾讯\WorkBuddy\WorkBuddy.exe`)并写进真实注册表,然后跑插件的真实探测逻辑,断言**拿到的路径与真实路径完全一致、且文件确实存在**。
38
+ - **反向验证**:把修复撤掉、退回「`reg.exe` + UTF-8」,含中文的路径**读不出来**(0 条),测试变红——确认这个测试真的能抓住这个 bug。
39
+ - 实测 `doctor` 输出:`✓ D:\DSH\_doc_check\腾讯\WorkBuddy\WorkBuddy.exe`,中文完整、存在性正确。
40
+
41
+ ### 给中招的人(在你升级之前,或者万一还有别的编码)
42
+
43
+ 临时指路,设完**完全退出 DSH 再启动**:
44
+
45
+ ```powershell
46
+ [Environment]::SetEnvironmentVariable("WORKBUDDY_APP_EXECUTABLE",
47
+ "D:\ruanjian\anzhuang\腾讯\WorkBuddy\WorkBuddy.exe", "User")
48
+ ```
49
+
50
+ 写进 `$DSH_HOME/.env` 也一样有效(不用注销,更省事)。
51
+
7
52
  ## 1.7.3 — 模型不会因为一次网络抖动就消失
8
53
 
9
54
  > 开机那一秒网络还没起来,插件就把模型列表「降级」了,然后一整天都不再试第二次。
package/lib/bin.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createDecipheriv, createHash, randomUUID } from "node:crypto";
2
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
3
  import { copyFile, mkdir, readFile, readdir, rename, writeFile, writeFile as writeFile$1 } from "node:fs/promises";
3
4
  import { homedir, platform } from "node:os";
4
5
  import { basename, dirname, join, resolve } from "node:path";
5
6
  import { execFile, execFileSync } from "node:child_process";
6
- import { existsSync, readFileSync, readdirSync } from "node:fs";
7
7
  import z from "@deepseek-ai/schemastery";
8
8
  import "@earendil-works/pi-ai";
9
9
  import "@earendil-works/pi-ai/api/openai-completions.lazy";
@@ -1890,42 +1890,155 @@ function macosBundleExecutable(bundle) {
1890
1890
  return join(bundle, "Contents", "MacOS", name);
1891
1891
  }
1892
1892
  /**
1893
+ * Encodings a Windows console child process may emit in, most likely first.
1894
+ *
1895
+ * `reg.exe` writes with the ACTIVE CODE PAGE, never UTF-8: a Chinese Windows
1896
+ * (code page 936) returns a path containing 「腾讯」 as GBK bytes. Decoding
1897
+ * those as UTF-8 mangles them into `��Ѷ`, every `existsSync` then fails, and the
1898
+ * plugin concludes no desktop app is installed while one is running fine.
1899
+ *
1900
+ * The list is ordered and each entry is TRIED rather than guessed: the first
1901
+ * decoding that yields usable text wins, and the decision is validated against
1902
+ * the filesystem rather than by sniffing bytes.
1903
+ */
1904
+ const WINDOWS_CONSOLE_ENCODINGS = [
1905
+ "utf-8",
1906
+ "gbk",
1907
+ "big5",
1908
+ "shift_jis",
1909
+ "euc-kr",
1910
+ "windows-1252"
1911
+ ];
1912
+ /**
1913
+ * Decode bytes from a Windows console child process.
1914
+ *
1915
+ * `accept` decides whether a decoding is usable — the callers pass "this text
1916
+ * contains a path that actually exists". Bytes that are already UTF-8 pass on
1917
+ * the first attempt, so the healthy case pays nothing.
1918
+ */
1919
+ function decodeWindowsConsole(buf, accept) {
1920
+ for (const label of WINDOWS_CONSOLE_ENCODINGS) {
1921
+ let text;
1922
+ try {
1923
+ text = new TextDecoder(label, { fatal: false }).decode(buf);
1924
+ } catch {
1925
+ continue;
1926
+ }
1927
+ if (accept(text)) return text;
1928
+ }
1929
+ return new TextDecoder("utf-8", { fatal: false }).decode(buf);
1930
+ }
1931
+ /** Strip the `"<path>",<index>` decoration DisplayIcon carries. */
1932
+ function cleanIconPath(raw) {
1933
+ return raw.replace(/^"/u, "").replace(/",-?\d+$/u, "").replace(/,-?\d+$/u, "").trim();
1934
+ }
1935
+ /** Registry locations the installer records its own product under. */
1936
+ const UNINSTALL_KEY_ROOTS = [
1937
+ "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
1938
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
1939
+ "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
1940
+ ];
1941
+ /**
1893
1942
  * Windows install locations recorded by the app's own uninstaller.
1894
1943
  *
1895
1944
  * The registry is the authoritative answer: it survives a non-default drive, a
1896
1945
  * renamed folder and a differently-named executable, none of which any fixed
1897
1946
  * path list can predict. Real machines put the app at `D:\workbuddy\WorkBuddy.exe`
1898
- * and `D:\workbuddyai\WorkBuddyAI.exe` — exactly the layouts a
1899
- * `%ProgramFiles%\WorkBuddy\WorkBuddy.exe` probe cannot see, which is why the
1900
- * plugin reported "the desktop app could not provide the key" for an app that was
1901
- * installed and running.
1947
+ * and `D:\ruanjian\anzhuang\腾讯\WorkBuddy\WorkBuddy.exe` — layouts a
1948
+ * `%ProgramFiles%\WorkBuddy\WorkBuddy.exe` probe cannot see.
1902
1949
  *
1903
1950
  * `DisplayIcon` is the field that actually carries the path (observed as
1904
1951
  * `D:\workbuddy\WorkBuddy.exe,0`); `InstallLocation` is usually empty for these
1905
1952
  * installers, so both are read and either may contribute.
1906
1953
  *
1954
+ * Two readers, because neither alone is enough:
1955
+ *
1956
+ * - **PowerShell first.** `Get-ItemProperty` talks to the registry API, so no
1957
+ * console code page is involved and non-ASCII paths come back intact. This
1958
+ * is the encoding-proof path.
1959
+ * - **`reg.exe` as a fallback**, for a machine where PowerShell is unavailable
1960
+ * (locked-down or stripped images). Its raw bytes are decoded through
1961
+ * {@link decodeWindowsConsole}, which is what the original code got wrong.
1962
+ *
1907
1963
  * Returns [] on any failure — a missing registry key is the normal case on
1908
1964
  * non-Windows, not an error.
1909
1965
  */
1910
1966
  function windowsRegistryAppPaths() {
1911
1967
  if (process.platform !== "win32") return [];
1912
- const roots = [
1913
- ["HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/**"],
1914
- ["HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/**"],
1915
- ["HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/**"]
1916
- ];
1968
+ const viaPowerShell = windowsRegistryAppPathsViaPowerShell();
1969
+ if (viaPowerShell.length > 0) return viaPowerShell;
1970
+ return windowsRegistryAppPathsViaReg();
1971
+ }
1972
+ /**
1973
+ * Registry read through PowerShell, immune to the console code page.
1974
+ *
1975
+ * `[Console]::OutputEncoding` is set to UTF-8 so the bytes Node receives are
1976
+ * UTF-8 regardless of the machine's code page. Values are printed one per line
1977
+ * and matched by the caller, so a Chinese path survives the round trip.
1978
+ *
1979
+ * Best-effort: any failure (no PowerShell, no keys, a policy restriction)
1980
+ * returns [], and the `reg.exe` reader takes over.
1981
+ */
1982
+ function windowsRegistryAppPathsViaPowerShell() {
1983
+ const script = [
1984
+ "$ErrorActionPreference='SilentlyContinue'",
1985
+ "try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}",
1986
+ "$roots=@('HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*')",
1987
+ "foreach($r in $roots){",
1988
+ " Get-ItemProperty -Path $r | ForEach-Object {",
1989
+ " $dn=[string]$_.DisplayName; $di=[string]$_.DisplayIcon; $il=[string]$_.InstallLocation",
1990
+ " if($dn -match 'WorkBuddy|CodeBuddy' -or $di -match 'WorkBuddy|CodeBuddy'){",
1991
+ " if($di -ne ''){ Write-Output $di }",
1992
+ " if($il -ne ''){ Write-Output $il }",
1993
+ " }",
1994
+ " }",
1995
+ "}"
1996
+ ].join("\n");
1997
+ let stdout;
1998
+ try {
1999
+ stdout = execFileSync("powershell", [
2000
+ "-NoProfile",
2001
+ "-NonInteractive",
2002
+ "-ExecutionPolicy",
2003
+ "Bypass",
2004
+ "-Command",
2005
+ script
2006
+ ], {
2007
+ encoding: "utf8",
2008
+ timeout: 2e4,
2009
+ windowsHide: true,
2010
+ maxBuffer: 8388608
2011
+ });
2012
+ } catch {
2013
+ return [];
2014
+ }
1917
2015
  const out = [];
1918
- for (const [root] of roots) {
1919
- let listing;
2016
+ for (const line of stdout.split(/\r?\n/u)) {
2017
+ const cleaned = cleanIconPath(line.trim());
2018
+ if (cleaned !== "") out.push(cleaned);
2019
+ }
2020
+ return out;
2021
+ }
2022
+ /**
2023
+ * Registry read through `reg.exe`, with console-code-page detection.
2024
+ *
2025
+ * Kept as the fallback for machines without PowerShell. The decode step is the
2026
+ * fix: the output is captured as BYTES and decoded with whichever encoding
2027
+ * yields a path that exists, instead of assuming UTF-8 and quietly producing
2028
+ * mojibake that no existence check can ever match.
2029
+ */
2030
+ function windowsRegistryAppPathsViaReg() {
2031
+ const out = [];
2032
+ for (const root of UNINSTALL_KEY_ROOTS) {
2033
+ let listingBuf;
1920
2034
  try {
1921
- listing = execFileSync("reg", [
2035
+ listingBuf = execFileSync("reg", [
1922
2036
  "query",
1923
2037
  root,
1924
2038
  "/s",
1925
2039
  "/v",
1926
2040
  "DisplayName"
1927
2041
  ], {
1928
- encoding: "utf8",
1929
2042
  timeout: 1e4,
1930
2043
  windowsHide: true,
1931
2044
  maxBuffer: 8388608
@@ -1933,26 +2046,35 @@ function windowsRegistryAppPaths() {
1933
2046
  } catch {
1934
2047
  continue;
1935
2048
  }
1936
- const keys = listing.split(/\r?\n(?=HKEY_)/u).filter((block) => /WorkBuddy|CodeBuddy/iu.test(block));
2049
+ const keys = decodeWindowsConsole(listingBuf, (text) => /WorkBuddy|CodeBuddy/iu.test(text)).split(/\r?\n(?=HKEY_)/u).filter((block) => /WorkBuddy|CodeBuddy/iu.test(block));
1937
2050
  for (const key of keys) {
1938
2051
  const keyPath = /^(HKEY_[^\r\n]+)/u.exec(key)?.[1]?.trim();
1939
2052
  if (keyPath === void 0) continue;
1940
- for (const name of ["DisplayIcon", "InstallLocation"]) try {
1941
- const value = execFileSync("reg", [
1942
- "query",
1943
- keyPath,
1944
- "/v",
1945
- name
1946
- ], {
1947
- encoding: "utf8",
1948
- timeout: 5e3,
1949
- windowsHide: true
2053
+ for (const name of ["DisplayIcon", "InstallLocation"]) {
2054
+ let valueBuf;
2055
+ try {
2056
+ valueBuf = execFileSync("reg", [
2057
+ "query",
2058
+ keyPath,
2059
+ "/v",
2060
+ name
2061
+ ], {
2062
+ timeout: 5e3,
2063
+ windowsHide: true
2064
+ });
2065
+ } catch {
2066
+ continue;
2067
+ }
2068
+ const value = decodeWindowsConsole(valueBuf, (text) => {
2069
+ const candidate = /REG_(?:SZ|EXPAND_SZ)\s+(.+)$/mu.exec(text)?.[1]?.trim();
2070
+ if (candidate === void 0) return false;
2071
+ const path = cleanIconPath(candidate);
2072
+ return path !== "" && existsSync(path);
1950
2073
  });
1951
2074
  const raw = /REG_(?:SZ|EXPAND_SZ)\s+(.+)$/mu.exec(value)?.[1]?.trim();
1952
2075
  if (raw === void 0 || raw === "") continue;
1953
- const cleaned = raw.replace(/^"/u, "").replace(/",-?\d+$/u, "").replace(/,-?\d+$/u, "").trim();
1954
- out.push(cleaned);
1955
- } catch {}
2076
+ out.push(cleanIconPath(raw));
2077
+ }
1956
2078
  }
1957
2079
  }
1958
2080
  return out;
@@ -2221,7 +2343,7 @@ const ENCRYPTED_CREDENTIAL_CODE = "ENCRYPTED_CREDENTIAL";
2221
2343
  var WorkBuddyEncryptedCredentialError = class extends Error {
2222
2344
  code = ENCRYPTED_CREDENTIAL_CODE;
2223
2345
  constructor(sourcePath) {
2224
- super(`workbuddy: ${sourcePath} holds encrypted credentials, but no WorkBuddy desktop app could be located to provide the key. If the app IS installed, it is simply outside the paths this plugin probes — set WORKBUDDY_APP_EXECUTABLE to its full .exe path (then restart DSH) and the credential will open. Signing in again will not help: the credential itself is intact. Run \`dsh-workbuddy-xdpool doctor\` to see which paths were probed.`);
2346
+ super(`workbuddy: ${sourcePath} holds encrypted credentials, but no WorkBuddy desktop app could be located to provide the key. If the app IS installed, it is simply outside the paths this plugin probes — set WORKBUDDY_APP_EXECUTABLE to its full .exe path (then restart DSH) and the credential will open. Signing in again will not help: the credential itself is intact. Run \`dsh-workbuddy-xdpool doctor\` to see which paths were probed — if one of them looks like mojibake (e.g. "??" where a Chinese folder name should be), the registry value could not be decoded on this machine; report that line and use the override above in the meantime.`);
2225
2347
  this.name = "WorkBuddyEncryptedCredentialError";
2226
2348
  }
2227
2349
  };
@@ -5111,6 +5233,31 @@ async function commandDoctor() {
5111
5233
  imported = (await readdir2(accountDir())).filter((name) => name.endsWith(".json"));
5112
5234
  } catch {}
5113
5235
  lines.push(imported.length === 0 ? " (none)" : imported.map((name) => ` ✓ ${name}`).join("\n"));
5236
+ lines.push("");
5237
+ lines.push("WorkBuddy desktop app:");
5238
+ const override = process.env[WORKBUDDY_APP_EXECUTABLE_ENV]?.trim();
5239
+ if (override !== void 0 && override !== "") lines.push(` env ${WORKBUDDY_APP_EXECUTABLE_ENV} = ${override}`);
5240
+ let candidates = [];
5241
+ try {
5242
+ candidates = workbuddyAppExecutableCandidates();
5243
+ } catch (error) {
5244
+ lines.push(` ✗ candidate probe threw: ${error instanceof Error ? error.message : String(error)}`);
5245
+ }
5246
+ let firstExisting;
5247
+ for (const candidate of candidates) {
5248
+ const exists = existsSync(candidate);
5249
+ if (exists && firstExisting === void 0) firstExisting = candidate;
5250
+ lines.push(` ${exists ? "✓" : "·"} ${candidate}`);
5251
+ }
5252
+ lines.push(` probed ${candidates.length} candidate path(s)`);
5253
+ if (firstExisting === void 0) {
5254
+ healthy = false;
5255
+ lines.push(" ✗ no WorkBuddy desktop executable found at any probed path.");
5256
+ lines.push(" If the app IS installed, set WORKBUDDY_APP_EXECUTABLE to its full .exe path");
5257
+ lines.push(" (or put that line in $DSH_HOME/.env) and restart DSH. A path shown above that");
5258
+ lines.push(" looks like mojibake (e.g. \"???\" for a Chinese folder) indicates the registry");
5259
+ lines.push(" value could not be decoded on this machine — please report it with the line.");
5260
+ } else lines.push(` ✓ using ${firstExisting}`);
5114
5261
  console.log(lines.join("\n"));
5115
5262
  return healthy ? 0 : 1;
5116
5263
  }
package/lib/index.js CHANGED
@@ -1997,42 +1997,155 @@ function macosBundleExecutable(bundle) {
1997
1997
  return join(bundle, "Contents", "MacOS", name);
1998
1998
  }
1999
1999
  /**
2000
+ * Encodings a Windows console child process may emit in, most likely first.
2001
+ *
2002
+ * `reg.exe` writes with the ACTIVE CODE PAGE, never UTF-8: a Chinese Windows
2003
+ * (code page 936) returns a path containing 「腾讯」 as GBK bytes. Decoding
2004
+ * those as UTF-8 mangles them into `��Ѷ`, every `existsSync` then fails, and the
2005
+ * plugin concludes no desktop app is installed while one is running fine.
2006
+ *
2007
+ * The list is ordered and each entry is TRIED rather than guessed: the first
2008
+ * decoding that yields usable text wins, and the decision is validated against
2009
+ * the filesystem rather than by sniffing bytes.
2010
+ */
2011
+ const WINDOWS_CONSOLE_ENCODINGS = [
2012
+ "utf-8",
2013
+ "gbk",
2014
+ "big5",
2015
+ "shift_jis",
2016
+ "euc-kr",
2017
+ "windows-1252"
2018
+ ];
2019
+ /**
2020
+ * Decode bytes from a Windows console child process.
2021
+ *
2022
+ * `accept` decides whether a decoding is usable — the callers pass "this text
2023
+ * contains a path that actually exists". Bytes that are already UTF-8 pass on
2024
+ * the first attempt, so the healthy case pays nothing.
2025
+ */
2026
+ function decodeWindowsConsole(buf, accept) {
2027
+ for (const label of WINDOWS_CONSOLE_ENCODINGS) {
2028
+ let text;
2029
+ try {
2030
+ text = new TextDecoder(label, { fatal: false }).decode(buf);
2031
+ } catch {
2032
+ continue;
2033
+ }
2034
+ if (accept(text)) return text;
2035
+ }
2036
+ return new TextDecoder("utf-8", { fatal: false }).decode(buf);
2037
+ }
2038
+ /** Strip the `"<path>",<index>` decoration DisplayIcon carries. */
2039
+ function cleanIconPath(raw) {
2040
+ return raw.replace(/^"/u, "").replace(/",-?\d+$/u, "").replace(/,-?\d+$/u, "").trim();
2041
+ }
2042
+ /** Registry locations the installer records its own product under. */
2043
+ const UNINSTALL_KEY_ROOTS = [
2044
+ "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
2045
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
2046
+ "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall"
2047
+ ];
2048
+ /**
2000
2049
  * Windows install locations recorded by the app's own uninstaller.
2001
2050
  *
2002
2051
  * The registry is the authoritative answer: it survives a non-default drive, a
2003
2052
  * renamed folder and a differently-named executable, none of which any fixed
2004
2053
  * path list can predict. Real machines put the app at `D:\workbuddy\WorkBuddy.exe`
2005
- * and `D:\workbuddyai\WorkBuddyAI.exe` — exactly the layouts a
2006
- * `%ProgramFiles%\WorkBuddy\WorkBuddy.exe` probe cannot see, which is why the
2007
- * plugin reported "the desktop app could not provide the key" for an app that was
2008
- * installed and running.
2054
+ * and `D:\ruanjian\anzhuang\腾讯\WorkBuddy\WorkBuddy.exe` — layouts a
2055
+ * `%ProgramFiles%\WorkBuddy\WorkBuddy.exe` probe cannot see.
2009
2056
  *
2010
2057
  * `DisplayIcon` is the field that actually carries the path (observed as
2011
2058
  * `D:\workbuddy\WorkBuddy.exe,0`); `InstallLocation` is usually empty for these
2012
2059
  * installers, so both are read and either may contribute.
2013
2060
  *
2061
+ * Two readers, because neither alone is enough:
2062
+ *
2063
+ * - **PowerShell first.** `Get-ItemProperty` talks to the registry API, so no
2064
+ * console code page is involved and non-ASCII paths come back intact. This
2065
+ * is the encoding-proof path.
2066
+ * - **`reg.exe` as a fallback**, for a machine where PowerShell is unavailable
2067
+ * (locked-down or stripped images). Its raw bytes are decoded through
2068
+ * {@link decodeWindowsConsole}, which is what the original code got wrong.
2069
+ *
2014
2070
  * Returns [] on any failure — a missing registry key is the normal case on
2015
2071
  * non-Windows, not an error.
2016
2072
  */
2017
2073
  function windowsRegistryAppPaths() {
2018
2074
  if (process.platform !== "win32") return [];
2019
- const roots = [
2020
- ["HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/**"],
2021
- ["HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/**"],
2022
- ["HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "/**"]
2023
- ];
2075
+ const viaPowerShell = windowsRegistryAppPathsViaPowerShell();
2076
+ if (viaPowerShell.length > 0) return viaPowerShell;
2077
+ return windowsRegistryAppPathsViaReg();
2078
+ }
2079
+ /**
2080
+ * Registry read through PowerShell, immune to the console code page.
2081
+ *
2082
+ * `[Console]::OutputEncoding` is set to UTF-8 so the bytes Node receives are
2083
+ * UTF-8 regardless of the machine's code page. Values are printed one per line
2084
+ * and matched by the caller, so a Chinese path survives the round trip.
2085
+ *
2086
+ * Best-effort: any failure (no PowerShell, no keys, a policy restriction)
2087
+ * returns [], and the `reg.exe` reader takes over.
2088
+ */
2089
+ function windowsRegistryAppPathsViaPowerShell() {
2090
+ const script = [
2091
+ "$ErrorActionPreference='SilentlyContinue'",
2092
+ "try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}",
2093
+ "$roots=@('HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*')",
2094
+ "foreach($r in $roots){",
2095
+ " Get-ItemProperty -Path $r | ForEach-Object {",
2096
+ " $dn=[string]$_.DisplayName; $di=[string]$_.DisplayIcon; $il=[string]$_.InstallLocation",
2097
+ " if($dn -match 'WorkBuddy|CodeBuddy' -or $di -match 'WorkBuddy|CodeBuddy'){",
2098
+ " if($di -ne ''){ Write-Output $di }",
2099
+ " if($il -ne ''){ Write-Output $il }",
2100
+ " }",
2101
+ " }",
2102
+ "}"
2103
+ ].join("\n");
2104
+ let stdout;
2105
+ try {
2106
+ stdout = execFileSync("powershell", [
2107
+ "-NoProfile",
2108
+ "-NonInteractive",
2109
+ "-ExecutionPolicy",
2110
+ "Bypass",
2111
+ "-Command",
2112
+ script
2113
+ ], {
2114
+ encoding: "utf8",
2115
+ timeout: 2e4,
2116
+ windowsHide: true,
2117
+ maxBuffer: 8388608
2118
+ });
2119
+ } catch {
2120
+ return [];
2121
+ }
2024
2122
  const out = [];
2025
- for (const [root] of roots) {
2026
- let listing;
2123
+ for (const line of stdout.split(/\r?\n/u)) {
2124
+ const cleaned = cleanIconPath(line.trim());
2125
+ if (cleaned !== "") out.push(cleaned);
2126
+ }
2127
+ return out;
2128
+ }
2129
+ /**
2130
+ * Registry read through `reg.exe`, with console-code-page detection.
2131
+ *
2132
+ * Kept as the fallback for machines without PowerShell. The decode step is the
2133
+ * fix: the output is captured as BYTES and decoded with whichever encoding
2134
+ * yields a path that exists, instead of assuming UTF-8 and quietly producing
2135
+ * mojibake that no existence check can ever match.
2136
+ */
2137
+ function windowsRegistryAppPathsViaReg() {
2138
+ const out = [];
2139
+ for (const root of UNINSTALL_KEY_ROOTS) {
2140
+ let listingBuf;
2027
2141
  try {
2028
- listing = execFileSync("reg", [
2142
+ listingBuf = execFileSync("reg", [
2029
2143
  "query",
2030
2144
  root,
2031
2145
  "/s",
2032
2146
  "/v",
2033
2147
  "DisplayName"
2034
2148
  ], {
2035
- encoding: "utf8",
2036
2149
  timeout: 1e4,
2037
2150
  windowsHide: true,
2038
2151
  maxBuffer: 8388608
@@ -2040,26 +2153,35 @@ function windowsRegistryAppPaths() {
2040
2153
  } catch {
2041
2154
  continue;
2042
2155
  }
2043
- const keys = listing.split(/\r?\n(?=HKEY_)/u).filter((block) => /WorkBuddy|CodeBuddy/iu.test(block));
2156
+ const keys = decodeWindowsConsole(listingBuf, (text) => /WorkBuddy|CodeBuddy/iu.test(text)).split(/\r?\n(?=HKEY_)/u).filter((block) => /WorkBuddy|CodeBuddy/iu.test(block));
2044
2157
  for (const key of keys) {
2045
2158
  const keyPath = /^(HKEY_[^\r\n]+)/u.exec(key)?.[1]?.trim();
2046
2159
  if (keyPath === void 0) continue;
2047
- for (const name of ["DisplayIcon", "InstallLocation"]) try {
2048
- const value = execFileSync("reg", [
2049
- "query",
2050
- keyPath,
2051
- "/v",
2052
- name
2053
- ], {
2054
- encoding: "utf8",
2055
- timeout: 5e3,
2056
- windowsHide: true
2160
+ for (const name of ["DisplayIcon", "InstallLocation"]) {
2161
+ let valueBuf;
2162
+ try {
2163
+ valueBuf = execFileSync("reg", [
2164
+ "query",
2165
+ keyPath,
2166
+ "/v",
2167
+ name
2168
+ ], {
2169
+ timeout: 5e3,
2170
+ windowsHide: true
2171
+ });
2172
+ } catch {
2173
+ continue;
2174
+ }
2175
+ const value = decodeWindowsConsole(valueBuf, (text) => {
2176
+ const candidate = /REG_(?:SZ|EXPAND_SZ)\s+(.+)$/mu.exec(text)?.[1]?.trim();
2177
+ if (candidate === void 0) return false;
2178
+ const path = cleanIconPath(candidate);
2179
+ return path !== "" && existsSync(path);
2057
2180
  });
2058
2181
  const raw = /REG_(?:SZ|EXPAND_SZ)\s+(.+)$/mu.exec(value)?.[1]?.trim();
2059
2182
  if (raw === void 0 || raw === "") continue;
2060
- const cleaned = raw.replace(/^"/u, "").replace(/",-?\d+$/u, "").replace(/,-?\d+$/u, "").trim();
2061
- out.push(cleaned);
2062
- } catch {}
2183
+ out.push(cleanIconPath(raw));
2184
+ }
2063
2185
  }
2064
2186
  }
2065
2187
  return out;
@@ -2328,7 +2450,7 @@ const ENCRYPTED_CREDENTIAL_CODE = "ENCRYPTED_CREDENTIAL";
2328
2450
  var WorkBuddyEncryptedCredentialError = class extends Error {
2329
2451
  code = ENCRYPTED_CREDENTIAL_CODE;
2330
2452
  constructor(sourcePath) {
2331
- super(`workbuddy: ${sourcePath} holds encrypted credentials, but no WorkBuddy desktop app could be located to provide the key. If the app IS installed, it is simply outside the paths this plugin probes — set WORKBUDDY_APP_EXECUTABLE to its full .exe path (then restart DSH) and the credential will open. Signing in again will not help: the credential itself is intact. Run \`dsh-workbuddy-xdpool doctor\` to see which paths were probed.`);
2453
+ super(`workbuddy: ${sourcePath} holds encrypted credentials, but no WorkBuddy desktop app could be located to provide the key. If the app IS installed, it is simply outside the paths this plugin probes — set WORKBUDDY_APP_EXECUTABLE to its full .exe path (then restart DSH) and the credential will open. Signing in again will not help: the credential itself is intact. Run \`dsh-workbuddy-xdpool doctor\` to see which paths were probed — if one of them looks like mojibake (e.g. "??" where a Chinese folder name should be), the registry value could not be decoded on this machine; report that line and use the override above in the meantime.`);
2332
2454
  this.name = "WorkBuddyEncryptedCredentialError";
2333
2455
  }
2334
2456
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-workbuddy-xdpool",
3
3
  "displayName": "DSH WorkBuddy XD Pool",
4
4
  "description": "Merge every locally signed-in WorkBuddy account into DeepSeek Harness as one auto-failing-over model pool (multi-account rotation, live credits, daily check-in and model catalog).",
5
- "version": "1.7.3",
5
+ "version": "1.7.4",
6
6
  "license": "MIT",
7
7
  "author": "XDTrees",
8
8
  "homepage": "https://github.com/XDTrees/dsh-workbuddy-xdpool#readme",