diff --git a/CHANGELOG.md b/CHANGELOG.md index 3953412..e444a68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ - Nothing yet. +## v1.4.5 - 2026-05-06 + +- Improved Windows remote credential detection for Lingma App installations. +- Remote API mode now checks `cache/user` before machine-id lookup so missing-login errors are more accurate. +- Expanded machine-id discovery to recursive Lingma app logs and VS Code Lingma plugin logs instead of only `logs/lingma.log`. +- Added support for additional machine-id log formats such as `machine_id`, `machineId`, and JSON-style fields. + ## v1.4.4 - 2026-05-05 - Enabled real SSE streaming for OpenAI `/v1/chat/completions` and Anthropic `/v1/messages` requests that include tools. diff --git a/README.md b/README.md index 94a0725..758287f 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The proxy now supports two backend modes: ## Current Version -The current desktop line is `v1.4.4`. +The current desktop line is `v1.4.5`. See [CHANGELOG.md](./CHANGELOG.md) for release history. diff --git a/README.zh-CN.md b/README.zh-CN.md index a6f5ef3..4efa9ea 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -16,7 +16,7 @@ ## 当前版本 -当前桌面端版本线:`v1.4.4` +当前桌面端版本线:`v1.4.5` 版本更新记录见 [CHANGELOG.md](./CHANGELOG.md)。 diff --git a/desktop/frontend/src/App.vue b/desktop/frontend/src/App.vue index 08a8641..c44c3a5 100644 --- a/desktop/frontend/src/App.vue +++ b/desktop/frontend/src/App.vue @@ -239,7 +239,7 @@ onUnmounted(() => {
{{ status.running ? 'Proxy Running' : 'Proxy Stopped' }} - v1.4.4 + v1.4.5
diff --git a/desktop/wails.json b/desktop/wails.json index e269219..b215595 100644 --- a/desktop/wails.json +++ b/desktop/wails.json @@ -11,6 +11,6 @@ "email": "lutc5@asiainfo.com" }, "info": { - "productVersion": "1.4.4" + "productVersion": "1.4.5" } } diff --git a/internal/remote/client_test.go b/internal/remote/client_test.go index 24ac9e0..31bbb0e 100644 --- a/internal/remote/client_test.go +++ b/internal/remote/client_test.go @@ -17,3 +17,17 @@ func TestExtractBaseURLFromMarketplaceLog(t *testing.T) { t.Fatalf("got %q, want %q", got, want) } } + +func TestExtractMachineIDFromTextMarkers(t *testing.T) { + got := extractMachineIDFromText(`2026-05-06 info using machine id from file: abcdef1234567890abcdef`) + if got != "abcdef1234567890abcdef" { + t.Fatalf("machine id = %q", got) + } +} + +func TestExtractMachineIDFromTextJSON(t *testing.T) { + got := extractMachineIDFromText(`{"machineId":"windows-machine-id-1234567890","other":true}`) + if got != "windows-machine-id-1234567890" { + t.Fatalf("machine id = %q", got) + } +} diff --git a/internal/remote/credentials.go b/internal/remote/credentials.go index dc649fb..c9b95ab 100644 --- a/internal/remote/credentials.go +++ b/internal/remote/credentials.go @@ -9,7 +9,9 @@ import ( "fmt" "os" "path/filepath" + "regexp" "runtime" + "sort" "strconv" "strings" "time" @@ -78,15 +80,15 @@ func importLingmaCacheCredential() (Credential, error) { } func importLingmaCacheCredentialFromDir(lingmaDir string) (Credential, error) { - machineID, err := loadMachineID(lingmaDir) - if err != nil { - return Credential{}, err - } userPath := filepath.Join(lingmaDir, "cache", "user") encrypted, err := os.ReadFile(userPath) if err != nil { return Credential{}, fmt.Errorf("read %s: %w", userPath, err) } + machineID, err := loadMachineID(lingmaDir) + if err != nil { + return Credential{}, err + } ciphertext, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(encrypted))) if err != nil { return Credential{}, fmt.Errorf("decode %s: %w", userPath, err) @@ -148,14 +150,82 @@ func loadMachineID(lingmaDir string) (string, error) { return value, nil } } - logBody, err := os.ReadFile(filepath.Join(lingmaDir, "logs", "lingma.log")) - if err != nil { - return "", fmt.Errorf("remote credential requires cache/id or lingma.log machine id: %w", err) + + for _, path := range candidateMachineIDLogFiles(lingmaDir) { + body, err := os.ReadFile(path) + if err != nil { + continue + } + if value := extractMachineIDFromText(string(body)); value != "" { + return value, nil + } } - markers := []string{"using machine id from file:", "machine id:"} - text := string(logBody) + + return "", errors.New("remote credential requires cache/id or Lingma log machine id; checked cache/id, Lingma app logs, and VS Code Lingma plugin logs") +} + +func candidateMachineIDLogFiles(lingmaDir string) []string { + paths := []string{ + filepath.Join(lingmaDir, "logs", "lingma.log"), + filepath.Join(lingmaDir, "logs", "Lingma.log"), + filepath.Join(lingmaDir, "logs", "main.log"), + filepath.Join(lingmaDir, "logs", "renderer.log"), + filepath.Join(lingmaDir, "logs", "sharedprocess.log"), + } + paths = append(paths, recursiveLogFiles(filepath.Join(lingmaDir, "logs"), 24)...) + + if home, err := os.UserHomeDir(); err == nil { + for _, root := range lingmaLogRoots(home) { + paths = append(paths, recentLingmaAppLogs(root)...) + paths = append(paths, recursiveLogFiles(root, 24)...) + } + } + return uniquePathStrings(paths) +} + +func recursiveLogFiles(root string, limit int) []string { + type item struct { + path string + modTime int64 + } + items := make([]item, 0) + _ = filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error { + if err != nil || entry.IsDir() { + return nil + } + name := strings.ToLower(entry.Name()) + if !strings.HasSuffix(name, ".log") && !strings.Contains(name, "lingma") { + return nil + } + info, err := entry.Info() + if err != nil { + return nil + } + items = append(items, item{path: path, modTime: info.ModTime().UnixNano()}) + return nil + }) + sort.Slice(items, func(i, j int) bool { return items[i].modTime > items[j].modTime }) + if limit > 0 && len(items) > limit { + items = items[:limit] + } + out := make([]string, 0, len(items)) + for _, item := range items { + out = append(out, item.path) + } + return out +} + +func extractMachineIDFromText(text string) string { + markers := []string{ + "using machine id from file:", + "machine id:", + "machine_id:", + "machineId:", + "machine-id:", + } + lowerText := strings.ToLower(text) for _, marker := range markers { - index := strings.LastIndex(strings.ToLower(text), marker) + index := strings.LastIndex(lowerText, strings.ToLower(marker)) if index < 0 { continue } @@ -163,11 +233,34 @@ func loadMachineID(lingmaDir string) (string, error) { if newline := strings.IndexByte(line, '\n'); newline >= 0 { line = line[:newline] } - if value := strings.TrimSpace(line); value != "" { - return value, nil + if value := normalizeMachineID(line); value != "" { + return value } } - return "", errors.New("machine id not found in Lingma cache") + + re := regexp.MustCompile(`(?i)"?(machine[_-]?id|machineId)"?\s*[:=]\s*"?([A-Za-z0-9._:-]{16,})"?`) + matches := re.FindAllStringSubmatch(text, -1) + for i := len(matches) - 1; i >= 0; i-- { + if len(matches[i]) >= 3 { + if value := normalizeMachineID(matches[i][2]); value != "" { + return value + } + } + } + return "" +} + +func normalizeMachineID(value string) string { + value = strings.TrimSpace(value) + value = strings.Trim(value, ` "'<>),]}`) + if idx := strings.IndexAny(value, " \t\r\n,;"); idx >= 0 { + value = value[:idx] + } + value = strings.Trim(value, ` "'<>),]}`) + if len(value) < aes.BlockSize { + return "" + } + return value } func decryptCacheUser(machineID string, ciphertext []byte) ([]byte, error) {