Release v1.4.8 remote detection fixes
This commit is contained in:
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
## Unreleased
|
## 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
|
## v1.4.7 - 2026-05-06
|
||||||
|
|
||||||
- Renamed user-facing product, desktop app, release assets, and documentation from Lingma IPC Proxy to Lingma Proxy.
|
- Renamed user-facing product, desktop app, release assets, and documentation from Lingma IPC Proxy to Lingma Proxy.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ The proxy now supports two backend modes:
|
|||||||
|
|
||||||
## Current Version
|
## 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.
|
See [CHANGELOG.md](./CHANGELOG.md) for release history.
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
## 当前版本
|
## 当前版本
|
||||||
|
|
||||||
当前桌面端版本线:`v1.4.7`
|
当前桌面端版本线:`v1.4.8`
|
||||||
|
|
||||||
版本更新记录见 [CHANGELOG.md](./CHANGELOG.md)。
|
版本更新记录见 [CHANGELOG.md](./CHANGELOG.md)。
|
||||||
|
|
||||||
|
|||||||
@@ -427,10 +427,10 @@ func (a *App) StartProxy() error {
|
|||||||
warmupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
warmupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
if err := svc.Warmup(warmupCtx); err != nil {
|
if err := svc.Warmup(warmupCtx); err != nil {
|
||||||
runtime.LogWarningf(a.ctx, "warmup failed: %v", err)
|
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 {
|
} else {
|
||||||
runtime.LogInfo(a.ctx, "Lingma IPC warmup completed")
|
runtime.LogInfof(a.ctx, "%s warmup completed", backendLabel(cfg.Backend))
|
||||||
a.emitLog("info", "Lingma IPC warmup completed")
|
a.emitLog("info", fmt.Sprintf("%s warmup completed", backendLabel(cfg.Backend)))
|
||||||
}
|
}
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
@@ -1095,5 +1095,12 @@ func defaultShellType() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func transportFallbackHint() 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/。"
|
return "请确认 Lingma 插件已启动并登录;如果自动探测失败,请到设置页手动填写:macOS WebSocket 示例 ws://127.0.0.1:36510/,Windows Named Pipe 示例 \\\\.\\pipe\\lingma-xxxx,或 Windows WebSocket 示例 ws://127.0.0.1:36510/。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,7 +252,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.7</small>
|
<small>v1.4.8</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -263,7 +263,6 @@ async function save() {
|
|||||||
<dt>登录态有效期</dt>
|
<dt>登录态有效期</dt>
|
||||||
<dd :class="{ 'warn-text': detection.remoteTokenExpired }">
|
<dd :class="{ 'warn-text': detection.remoteTokenExpired }">
|
||||||
{{ formattedTokenExpireAt || '未提供' }}
|
{{ formattedTokenExpireAt || '未提供' }}
|
||||||
<span v-if="formattedTokenExpireAt && detection.remoteTokenExpireAt" class="muted-inline">原始 {{ detection.remoteTokenExpireAt }}</span>
|
|
||||||
<span v-if="detection.remoteTokenExpired">(已过期)</span>
|
<span v-if="detection.remoteTokenExpired">(已过期)</span>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -90,17 +90,8 @@ func appMenu(app *App) *menu.Menu {
|
|||||||
app.RequestQuitShortcut()
|
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(
|
return menu.NewMenuFromItems(
|
||||||
menu.SubMenu("Lingma Proxy", appMenu),
|
menu.SubMenu("Lingma Proxy", appMenu),
|
||||||
menu.SubMenu("编辑", editMenu),
|
menu.EditMenu(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,6 @@
|
|||||||
"email": "lutc5@asiainfo.com"
|
"email": "lutc5@asiainfo.com"
|
||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"productVersion": "1.4.7"
|
"productVersion": "1.4.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ func (c *Client) ListModels(ctx context.Context) ([]Model, error) {
|
|||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
if resp.StatusCode >= 400 {
|
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 {
|
var payload struct {
|
||||||
Chat []Model `json:"chat"`
|
Chat []Model `json:"chat"`
|
||||||
@@ -147,6 +147,14 @@ func (c *Client) ListModels(ctx context.Context) ([]Model, error) {
|
|||||||
return append(payload.Chat, payload.Inline...), nil
|
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) {
|
func (c *Client) Chat(ctx context.Context, request ChatRequest, onDelta func(string)) (*ChatResult, error) {
|
||||||
cred, err := LoadCredential(c.cfg.AuthFile)
|
cred, err := LoadCredential(c.cfg.AuthFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -592,12 +600,29 @@ func normalizeRemoteBaseURLHint(raw string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
host := strings.ToLower(parsed.Host)
|
host := strings.ToLower(parsed.Host)
|
||||||
if !strings.Contains(host, "lingma") && !strings.Contains(host, "rdc.aliyuncs.com") {
|
if !isRemoteAPIHost(host) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return parsed.Scheme + "://" + parsed.Host
|
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 {
|
func estimateTokens(text string) int {
|
||||||
text = strings.TrimSpace(text)
|
text = strings.TrimSpace(text)
|
||||||
if text == "" {
|
if text == "" {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package remote
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -22,43 +23,76 @@ func TestNewKeepsPositiveTimeout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractBaseURLFromEndpointLog(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`)
|
got := extractBaseURLFromText(`2026-04-10 INFO Update endpoint success. endpoint config: https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com`)
|
||||||
want := "https://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com"
|
want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com"
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Fatalf("got %q, want %q", got, want)
|
t.Fatalf("got %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractBaseURLFromMarketplaceLog(t *testing.T) {
|
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`)
|
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-cmb01-cn-beijing.rdc.aliyuncs.com"
|
want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com"
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Fatalf("got %q, want %q", got, want)
|
t.Fatalf("got %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractBaseURLFromRawWindowsLogURL(t *testing.T) {
|
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`)
|
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-cmb01-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 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 {
|
if got != want {
|
||||||
t.Fatalf("got %q, want %q", got, want)
|
t.Fatalf("got %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeBaseURLRepairsMissingLeadingH(t *testing.T) {
|
func TestNormalizeBaseURLRepairsMissingLeadingH(t *testing.T) {
|
||||||
got := normalizeRemoteBaseURLHint(`ttps://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com`)
|
got := normalizeRemoteBaseURLHint(`ttps://ai-lingma-example-cn-beijing.rdc.aliyuncs.com`)
|
||||||
want := "https://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com"
|
want := "https://ai-lingma-example-cn-beijing.rdc.aliyuncs.com"
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Fatalf("got %q, want %q", got, want)
|
t.Fatalf("got %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNormalizeBaseURLRejectsUnsupportedScheme(t *testing.T) {
|
func TestNormalizeBaseURLRejectsLingmaOSSAssetHost(t *testing.T) {
|
||||||
if got := normalizeRemoteBaseURLHint(`ftp://ai-lingma-cmb01-cn-beijing.rdc.aliyuncs.com`); got != "" {
|
if got := normalizeRemoteBaseURLHint(`https://lingma-ide.oss-rg-china-mainland.aliyuncs.com/lingma-extension/download`); got != "" {
|
||||||
t.Fatalf("got %q, want empty", 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, `<Error><Code>NoSuchKey</Code></Error>`)
|
||||||
|
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) {
|
func TestExtractMachineIDFromTextMarkers(t *testing.T) {
|
||||||
got := extractMachineIDFromText(`2026-05-06 info using machine id from file: abcdef1234567890abcdef`)
|
got := extractMachineIDFromText(`2026-05-06 info using machine id from file: abcdef1234567890abcdef`)
|
||||||
if got != "abcdef1234567890abcdef" {
|
if got != "abcdef1234567890abcdef" {
|
||||||
|
|||||||
Reference in New Issue
Block a user