diff --git a/docs/06-troubleshooting.md b/docs/06-troubleshooting.md index 5861294..813d2c7 100644 --- a/docs/06-troubleshooting.md +++ b/docs/06-troubleshooting.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Troubleshooting wiki_url: https://git.ilapage.cn/chengma/cmsp/wiki/Troubleshooting -wiki_revision: 75c52d039b32fa90b24e594d49df79eb5a7302ad -synchronized_at: 2026-10-07T08:13:41Z +wiki_revision: bb5e884ffc4c08b7b1872b251239268358a5f4ff +synchronized_at: 2026-10-07T08:35:19Z # 故障排查 @@ -92,14 +92,14 @@ SYNC_IN_PROGRESS 表示已有商品同步,请等待结束;Go侧拒绝重叠 `docs/` 只能由同步工具生成。任何时候都不要先改本地镜像再反向覆盖 Wiki。 -### 3. 检查专属 Chrome(计划,Go 实现后适用) +### 3. 检查专属 Chrome 1. 状态文件中的 PID 是否还存在。 2. 该进程的命令行是否包含本程序的专属 `--user-data-dir`。 3. `http://127.0.0.1:<端口>/json/version` 是否可访问。 4. `http://127.0.0.1:<端口>/json/list` 是否存在 `type` 为 `page` 的目标。 -四项任一不通过,就关闭专属 Chrome 后重新启动。不要在同一个 Profile 上再启动第二个实例。 +有效状态可复用现有浏览器。新启动时,配置范围内的端口若由其他程序或其他 Profile Chrome 占用,程序会顺位尝试下一个空闲端口;范围全部占用才报端口不可用。如果本项目专属 Profile 已有 Chrome 进程,但状态文件丢失或无法复用,程序会拒绝启动第二实例;先手动关闭该专属 Chrome 再重试。无法读取 Chrome 进程归属时也会停止,避免共用 Profile。不要关闭其他 Profile 的浏览器。 ### 4. 检查淘宝登录 diff --git a/internal/taobao/chrome.go b/internal/taobao/chrome.go index 28e1736..1839525 100644 --- a/internal/taobao/chrome.go +++ b/internal/taobao/chrome.go @@ -62,20 +62,19 @@ func EnsureBrowser(ctx context.Context, cfg config.TaobaoConfig, openURL string) return state, nil } + // 状态文件缺失或失效时,检查真正使用本项目 Profile 的 Chrome。 + // 其他 Profile 占用的端口由 scanFreePort 跳过,不应阻止启动。 + commands, err := chromeProcessCommandLines() + if err != nil { + return BrowserState{}, fmt.Errorf("无法确认专属 Chrome 是否已运行,未启动第二个实例:%w", err) + } + if hasProfileChrome(commands, profile) { + return BrowserState{}, fmt.Errorf("专属 Chrome 已运行,但浏览器运行状态无法复用;请先关闭该专属浏览器后重试") + } port, err := scanFreePort(cfg.DebugPortStart, cfg.DebugPortEnd, portIsFree) if err != nil { return BrowserState{}, err } - // 状态文件丢失时无法证明已运行的调试浏览器属于哪个 Profile。 - // 宁可让使用者手动关闭它,也不能冒险启动同一 Profile 的第二个实例。 - for candidate := cfg.DebugPortStart; candidate <= cfg.DebugPortEnd; candidate++ { - if candidate == port { - continue - } - if cdpVersionAvailable(candidate) { - return BrowserState{}, fmt.Errorf("检测到端口 %d 已有浏览器,但缺少可验证归属的运行状态。请先关闭该专用浏览器后重试。", candidate) - } - } args := []string{ "--user-data-dir=" + profile, @@ -350,6 +349,37 @@ func scanFreePort(start, end int, isFree func(int) bool) (int, error) { return 0, fmt.Errorf("调试端口范围 %d—%d 已全部占用,请关闭不用的程序后重试", start, end) } +func chromeProcessCommandLines() ([]string, error) { + // PowerShell 的输出可能使用系统代码页,因此以 UTF-8 Base64 传递命令行。 + const script = `$ErrorActionPreference='Stop'; ` + + `$processes=Get-CimInstance Win32_Process -Filter "Name='chrome.exe'"; ` + + `foreach ($process in $processes) { ` + + `if ([string]::IsNullOrWhiteSpace($process.CommandLine)) { throw 'Chrome command line unavailable' }; ` + + `[Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($process.CommandLine)) }` + output, err := hiddenCommand("powershell", "-NoProfile", "-Command", script).Output() + if err != nil { + return nil, fmt.Errorf("读取 Chrome 进程列表失败:%w", err) + } + var commands []string + for _, encoded := range strings.Fields(string(output)) { + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return nil, fmt.Errorf("解析 Chrome 进程命令行失败:%w", err) + } + commands = append(commands, string(decoded)) + } + return commands, nil +} + +func hasProfileChrome(commands []string, profile string) bool { + for _, command := range commands { + if commandBelongsToProfile(command, profile) { + return true + } + } + return false +} + // commandBelongsToProfile 只比较启动参数中的 --user-data-dir。 // Windows 路径不区分大小写,命令行还可能混用斜杠或给路径加引号。 func commandBelongsToProfile(commandLine, profile string) bool { diff --git a/internal/taobao/chrome_test.go b/internal/taobao/chrome_test.go index aa2d858..448d75b 100644 --- a/internal/taobao/chrome_test.go +++ b/internal/taobao/chrome_test.go @@ -191,6 +191,26 @@ func Test端口全占满时返回错误(t *testing.T) { } } +func Test专属Profile进程检查(t *testing.T) { + profile := `C:\Users\测试\淘宝浏览器` + cases := []struct { + name string + commands []string + want bool + }{ + {"其他Chrome", []string{`chrome.exe --user-data-dir=C:\Users\其他\Chrome --remote-debugging-port=19666`}, false}, + {"专属Chrome", []string{`chrome.exe --user-data-dir="C:\Users\测试\淘宝浏览器" --remote-debugging-port=19666`}, true}, + {"专属Chrome在范围外", []string{`chrome.exe --user-data-dir=C:\Users\测试\淘宝浏览器 --remote-debugging-port=30000`}, true}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := hasProfileChrome(tc.commands, profile); got != tc.want { + t.Fatalf("专属 Profile 进程判断应为 %v,实际为 %v", tc.want, got) + } + }) + } +} + // 防回归:profile 路径含中文时的归属判断。 // // 曾经真实发生过:PowerShell 输出用系统 ANSI 代码页(GBK),