diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c202a5..aebe752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +## v1.4.8 - 2026-05-06 + +- Fixed Remote API base URL auto-detection so Lingma OSS/static asset hosts are rejected and cannot be used as API endpoints. +- Improved Remote API model-list 404 errors with a clear hint to manually set the official or enterprise remote API domain. +- Restored desktop input editing shortcuts by using the native Wails edit menu, fixing copy, paste, cut, undo, redo, and select-all in app input fields. +- Added regression tests for Windows/Lingma log URL parsing, missing leading `h` repair, and OSS-host rejection. + ## v1.4.7 - 2026-05-06 - Renamed user-facing product, desktop app, release assets, and documentation from Lingma IPC Proxy to Lingma Proxy. diff --git a/README.md b/README.md index 9ccdd82..f9f715f 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.7`. +The current desktop line is `v1.4.8`. See [CHANGELOG.md](./CHANGELOG.md) for release history. diff --git a/README.zh-CN.md b/README.zh-CN.md index b2a331e..83486cc 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -16,7 +16,7 @@ ## 当前版本 -当前桌面端版本线:`v1.4.7` +当前桌面端版本线:`v1.4.8` 版本更新记录见 [CHANGELOG.md](./CHANGELOG.md)。 diff --git a/desktop/app.go b/desktop/app.go index d2e1df4..0ca47e1 100644 --- a/desktop/app.go +++ b/desktop/app.go @@ -427,10 +427,10 @@ func (a *App) StartProxy() error { warmupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) if err := svc.Warmup(warmupCtx); err != nil { runtime.LogWarningf(a.ctx, "warmup failed: %v", err) - a.emitLog("warn", fmt.Sprintf("Lingma IPC warmup failed: %v. %s", err, transportFallbackHint())) + a.emitLog("warn", fmt.Sprintf("%s warmup failed: %v. %s", backendLabel(cfg.Backend), err, warmupFallbackHint(cfg.Backend))) } else { - runtime.LogInfo(a.ctx, "Lingma IPC warmup completed") - a.emitLog("info", "Lingma IPC warmup completed") + runtime.LogInfof(a.ctx, "%s warmup completed", backendLabel(cfg.Backend)) + a.emitLog("info", fmt.Sprintf("%s warmup completed", backendLabel(cfg.Backend))) } cancel() @@ -1095,5 +1095,12 @@ func defaultShellType() string { } func transportFallbackHint() string { + return "请确认 Lingma 插件已启动并登录;如果自动探测失败,请到设置页手动填写:远端 API 官方默认域名 https://lingma.alibabacloud.com,企业版请填写你的专属域名;macOS WebSocket 示例 ws://127.0.0.1:36510/,Windows Named Pipe 示例 \\\\.\\pipe\\lingma-xxxx,或 Windows WebSocket 示例 ws://127.0.0.1:36510/。" +} + +func warmupFallbackHint(backend service.BackendMode) string { + if backend == service.BackendRemote { + return "请检查设置页“当前解析结果”里的远端域名是否为官方或企业专属 API 域名;如果出现 OSS/静态资源域名或模型列表 404,请手动填写远端 API 官方默认域名 https://lingma.alibabacloud.com,企业版请填写你的专属域名,并确认登录态未过期。" + } return "请确认 Lingma 插件已启动并登录;如果自动探测失败,请到设置页手动填写:macOS WebSocket 示例 ws://127.0.0.1:36510/,Windows Named Pipe 示例 \\\\.\\pipe\\lingma-xxxx,或 Windows WebSocket 示例 ws://127.0.0.1:36510/。" } diff --git a/desktop/frontend/src/App.vue b/desktop/frontend/src/App.vue index ac6b6dc..3e3a18c 100644 --- a/desktop/frontend/src/App.vue +++ b/desktop/frontend/src/App.vue @@ -252,7 +252,7 @@ onUnmounted(() => {
{{ status.running ? 'Proxy Running' : 'Proxy Stopped' }} - v1.4.7 + v1.4.8
diff --git a/desktop/frontend/src/views/Settings.vue b/desktop/frontend/src/views/Settings.vue index 206a1d6..29333a9 100644 --- a/desktop/frontend/src/views/Settings.vue +++ b/desktop/frontend/src/views/Settings.vue @@ -263,7 +263,6 @@ async function save() {
登录态有效期
{{ formattedTokenExpireAt || '未提供' }} - 原始 {{ detection.remoteTokenExpireAt }} (已过期)
diff --git a/desktop/main.go b/desktop/main.go index 582b6df..4f4b7c7 100644 --- a/desktop/main.go +++ b/desktop/main.go @@ -90,17 +90,8 @@ func appMenu(app *App) *menu.Menu { app.RequestQuitShortcut() }) - editMenu := menu.NewMenu() - editMenu.AddText("撤销", keys.CmdOrCtrl("z"), func(_ *menu.CallbackData) {}) - editMenu.AddText("重做", keys.CmdOrCtrl("shift+z"), func(_ *menu.CallbackData) {}) - editMenu.AddSeparator() - editMenu.AddText("剪切", keys.CmdOrCtrl("x"), func(_ *menu.CallbackData) {}) - editMenu.AddText("复制", keys.CmdOrCtrl("c"), func(_ *menu.CallbackData) {}) - editMenu.AddText("粘贴", keys.CmdOrCtrl("v"), func(_ *menu.CallbackData) {}) - editMenu.AddText("全选", keys.CmdOrCtrl("a"), func(_ *menu.CallbackData) {}) - return menu.NewMenuFromItems( menu.SubMenu("Lingma Proxy", appMenu), - menu.SubMenu("编辑", editMenu), + menu.EditMenu(), ) } diff --git a/desktop/wails.json b/desktop/wails.json index de33b13..09a0abd 100644 --- a/desktop/wails.json +++ b/desktop/wails.json @@ -11,6 +11,6 @@ "email": "lutc5@asiainfo.com" }, "info": { - "productVersion": "1.4.7" + "productVersion": "1.4.8" } } diff --git a/internal/remote/client.go b/internal/remote/client.go index 1e4b0d4..e76695e 100644 --- a/internal/remote/client.go +++ b/internal/remote/client.go @@ -135,7 +135,7 @@ func (c *Client) ListModels(ctx context.Context) ([]Model, error) { defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode >= 400 { - return nil, fmt.Errorf("remote model list status %d: %s", resp.StatusCode, truncate(string(body), 500)) + return nil, c.modelListStatusError(resp.StatusCode, string(body)) } var payload struct { Chat []Model `json:"chat"` @@ -147,6 +147,14 @@ func (c *Client) ListModels(ctx context.Context) ([]Model, error) { return append(payload.Chat, payload.Inline...), nil } +func (c *Client) modelListStatusError(statusCode int, body string) error { + message := fmt.Sprintf("remote model list status %d from %s: %s", statusCode, c.cfg.BaseURL, truncate(body, 500)) + if statusCode == http.StatusNotFound || strings.Contains(body, "NoSuchKey") { + message += "。这通常表示远端 API 域名自动探测命中了错误地址,请到设置页手动填写 Lingma 官方或企业专属远端 API 域名;官方默认域名为 https://lingma.alibabacloud.com。" + } + return fmt.Errorf("%s", message) +} + func (c *Client) Chat(ctx context.Context, request ChatRequest, onDelta func(string)) (*ChatResult, error) { cred, err := LoadCredential(c.cfg.AuthFile) if err != nil { @@ -592,12 +600,29 @@ func normalizeRemoteBaseURLHint(raw string) string { return "" } host := strings.ToLower(parsed.Host) - if !strings.Contains(host, "lingma") && !strings.Contains(host, "rdc.aliyuncs.com") { + if !isRemoteAPIHost(host) { return "" } return parsed.Scheme + "://" + parsed.Host } +func isRemoteAPIHost(host string) bool { + if host == "" { + return false + } + if strings.Contains(host, ".oss-") || strings.Contains(host, "oss-rg-") || strings.Contains(host, ".oss.") { + return false + } + switch host { + case "lingma.alibabacloud.com", "lingma-api.tongyi.aliyun.com": + return true + } + if strings.HasSuffix(host, ".rdc.aliyuncs.com") { + return true + } + return false +} + func estimateTokens(text string) int { text = strings.TrimSpace(text) if text == "" { diff --git a/internal/remote/client_test.go b/internal/remote/client_test.go index 03824a9..aee3f89 100644 --- a/internal/remote/client_test.go +++ b/internal/remote/client_test.go @@ -3,6 +3,7 @@ package remote import ( "os" "path/filepath" + "strings" "testing" "time" ) @@ -22,43 +23,76 @@ func TestNewKeepsPositiveTimeout(t *testing.T) { } func TestExtractBaseURLFromEndpointLog(t *testing.T) { - got := extractBaseURLFromText(`2026-04-10 INFO Update endpoint success. endpoint config: https://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com`) - want := "https://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com" + got := extractBaseURLFromText(`2026-04-10 INFO Update endpoint success. endpoint config: https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com`) + want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com" if got != want { t.Fatalf("got %q, want %q", got, want) } } func TestExtractBaseURLFromMarketplaceLog(t *testing.T) { - got := extractBaseURLFromText(`2026-04-30 [info] [Marketplace] Using service url: https://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com/marketplace/_apis/public/gallery`) - want := "https://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com" + got := extractBaseURLFromText(`2026-04-30 [info] [Marketplace] Using service url: https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com/marketplace/_apis/public/gallery`) + want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com" if got != want { t.Fatalf("got %q, want %q", got, want) } } func TestExtractBaseURLFromRawWindowsLogURL(t *testing.T) { - got := extractBaseURLFromText(`2026-05-06T12:00:00 endpoint=https://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com/algo/api/v2/model/list`) - want := "https://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com" + got := extractBaseURLFromText(`2026-05-06T12:00:00 endpoint=https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com/algo/api/v2/model/list`) + want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com" + if got != want { + t.Fatalf("got %q, want %q", got, want) + } +} + +func TestExtractBaseURLIgnoresLingmaOSSAssetHost(t *testing.T) { + got := extractBaseURLFromText(`2026-05-06 endpoint config: https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com +2026-05-06 Download asset from: https://lingma-ide.oss-rg-china-mainland.aliyuncs.com/lingma-extension/download?name=plugin.zip`) + want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com" if got != want { t.Fatalf("got %q, want %q", got, want) } } func TestNormalizeBaseURLRepairsMissingLeadingH(t *testing.T) { - got := normalizeRemoteBaseURLHint(`ttps://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com`) - want := "https://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com" + got := normalizeRemoteBaseURLHint(`ttps://ai-lingma-example-cn-beijing.rdc.aliyuncs.com`) + want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com" if got != want { t.Fatalf("got %q, want %q", got, want) } } -func TestNormalizeBaseURLRejectsUnsupportedScheme(t *testing.T) { - if got := normalizeRemoteBaseURLHint(`ftp://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com`); got != "" { +func TestNormalizeBaseURLRejectsLingmaOSSAssetHost(t *testing.T) { + if got := normalizeRemoteBaseURLHint(`https://lingma-ide.oss-rg-china-mainland.aliyuncs.com/lingma-extension/download`); got != "" { t.Fatalf("got %q, want empty", got) } } +func TestNormalizeBaseURLRejectsUnsupportedScheme(t *testing.T) { + if got := normalizeRemoteBaseURLHint(`ftp://ai-lingma-example-cn-beijing.rdc.aliyuncs.com`); got != "" { + t.Fatalf("got %q, want empty", got) + } +} + +func TestModelListStatusErrorSuggestsManualRemoteBaseURLOn404(t *testing.T) { + client := New(Config{BaseURL: "https://lingma-ide.oss-rg-china-mainland.aliyuncs.com"}) + err := client.modelListStatusError(404, `NoSuchKey`) + if err == nil { + t.Fatal("expected error") + } + text := err.Error() + for _, want := range []string{ + "https://lingma-ide.oss-rg-china-mainland.aliyuncs.com", + "远端 API 域名自动探测命中了错误地址", + "https://lingma.alibabacloud.com", + } { + if !strings.Contains(text, want) { + t.Fatalf("error %q missing %q", text, want) + } + } +} + func TestExtractMachineIDFromTextMarkers(t *testing.T) { got := extractMachineIDFromText(`2026-05-06 info using machine id from file: abcdef1234567890abcdef`) if got != "abcdef1234567890abcdef" {