Release v1.4.5
This commit is contained in:
@@ -4,6 +4,13 @@
|
|||||||
|
|
||||||
- Nothing yet.
|
- 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
|
## v1.4.4 - 2026-05-05
|
||||||
|
|
||||||
- Enabled real SSE streaming for OpenAI `/v1/chat/completions` and Anthropic `/v1/messages` requests that include tools.
|
- Enabled real SSE streaming for OpenAI `/v1/chat/completions` and Anthropic `/v1/messages` requests that include tools.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ The proxy now supports two backend modes:
|
|||||||
|
|
||||||
## Current Version
|
## 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.
|
See [CHANGELOG.md](./CHANGELOG.md) for release history.
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
## 当前版本
|
## 当前版本
|
||||||
|
|
||||||
当前桌面端版本线:`v1.4.4`
|
当前桌面端版本线:`v1.4.5`
|
||||||
|
|
||||||
版本更新记录见 [CHANGELOG.md](./CHANGELOG.md)。
|
版本更新记录见 [CHANGELOG.md](./CHANGELOG.md)。
|
||||||
|
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ onUnmounted(() => {
|
|||||||
<span class="status-dot" :class="{ running: status.running }"></span>
|
<span class="status-dot" :class="{ running: status.running }"></span>
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ status.running ? 'Proxy Running' : 'Proxy Stopped' }}</strong>
|
<strong>{{ status.running ? 'Proxy Running' : 'Proxy Stopped' }}</strong>
|
||||||
<small>v1.4.4</small>
|
<small>v1.4.5</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -11,6 +11,6 @@
|
|||||||
"email": "lutc5@asiainfo.com"
|
"email": "lutc5@asiainfo.com"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"productVersion": "1.4.4"
|
"productVersion": "1.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,3 +17,17 @@ func TestExtractBaseURLFromMarketplaceLog(t *testing.T) {
|
|||||||
t.Fatalf("got %q, want %q", got, want)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -78,15 +80,15 @@ func importLingmaCacheCredential() (Credential, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func importLingmaCacheCredentialFromDir(lingmaDir string) (Credential, error) {
|
func importLingmaCacheCredentialFromDir(lingmaDir string) (Credential, error) {
|
||||||
machineID, err := loadMachineID(lingmaDir)
|
|
||||||
if err != nil {
|
|
||||||
return Credential{}, err
|
|
||||||
}
|
|
||||||
userPath := filepath.Join(lingmaDir, "cache", "user")
|
userPath := filepath.Join(lingmaDir, "cache", "user")
|
||||||
encrypted, err := os.ReadFile(userPath)
|
encrypted, err := os.ReadFile(userPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Credential{}, fmt.Errorf("read %s: %w", userPath, err)
|
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)))
|
ciphertext, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(encrypted)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Credential{}, fmt.Errorf("decode %s: %w", userPath, err)
|
return Credential{}, fmt.Errorf("decode %s: %w", userPath, err)
|
||||||
@@ -148,14 +150,82 @@ func loadMachineID(lingmaDir string) (string, error) {
|
|||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logBody, err := os.ReadFile(filepath.Join(lingmaDir, "logs", "lingma.log"))
|
|
||||||
if err != nil {
|
for _, path := range candidateMachineIDLogFiles(lingmaDir) {
|
||||||
return "", fmt.Errorf("remote credential requires cache/id or lingma.log machine id: %w", err)
|
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 {
|
for _, marker := range markers {
|
||||||
index := strings.LastIndex(strings.ToLower(text), marker)
|
index := strings.LastIndex(lowerText, strings.ToLower(marker))
|
||||||
if index < 0 {
|
if index < 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -163,11 +233,34 @@ func loadMachineID(lingmaDir string) (string, error) {
|
|||||||
if newline := strings.IndexByte(line, '\n'); newline >= 0 {
|
if newline := strings.IndexByte(line, '\n'); newline >= 0 {
|
||||||
line = line[:newline]
|
line = line[:newline]
|
||||||
}
|
}
|
||||||
if value := strings.TrimSpace(line); value != "" {
|
if value := normalizeMachineID(line); value != "" {
|
||||||
return value, nil
|
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) {
|
func decryptCacheUser(machineID string, ciphertext []byte) ([]byte, error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user