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 +45 -0
- package/lib/bin.js +176 -29
- package/lib/index.js +150 -28
- package/package.json +1 -1
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:\
|
|
1899
|
-
* `%ProgramFiles%\WorkBuddy\WorkBuddy.exe` probe cannot see
|
|
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
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
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
|
|
1919
|
-
|
|
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
|
-
|
|
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 =
|
|
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"])
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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
|
-
|
|
1954
|
-
|
|
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:\
|
|
2006
|
-
* `%ProgramFiles%\WorkBuddy\WorkBuddy.exe` probe cannot see
|
|
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
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
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
|
|
2026
|
-
|
|
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
|
-
|
|
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 =
|
|
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"])
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
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
|
-
|
|
2061
|
-
|
|
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.
|
|
5
|
+
"version": "1.7.4",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "XDTrees",
|
|
8
8
|
"homepage": "https://github.com/XDTrees/dsh-workbuddy-xdpool#readme",
|