diff --git a/README.md b/README.md index 239a605..86221d4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Lingma IPC Proxy +# Lingma Proxy [English](./README.md) | [简体中文](./README.zh-CN.md) -Lingma IPC Proxy exposes Tongyi Lingma's local IDE plugin capability as standard **OpenAI-compatible** and **Anthropic-compatible** HTTP APIs. It can be used as a CLI proxy service or as a cross-platform desktop app for macOS and Windows. +Lingma Proxy exposes Tongyi Lingma as standard **OpenAI-compatible** and **Anthropic-compatible** HTTP APIs. It can use either the local IDE plugin IPC channel or an experimental remote API backend, and ships as both a CLI proxy service and a cross-platform desktop app for macOS and Windows. The project is designed for tools such as Claude Code, Cline, Continue, OpenCode, custom agents, and any client that can talk to OpenAI or Anthropic style APIs. @@ -13,7 +13,7 @@ The proxy now supports two backend modes: ## Current Version -The current desktop line is `v1.4.0`. +The current desktop line is `v1.4.1`. Release builds are produced by GitHub Actions for: @@ -156,9 +156,9 @@ flowchart LR | Platform | Default transport | Detection | | --- | --- | --- | -| macOS | WebSocket | reads Lingma `SharedClientCache` files under user application support paths | -| Windows | Named Pipe / WebSocket | scans Lingma named pipes and shared cache hints | -| Linux | WebSocket | manual `--ws-url` is recommended | +| macOS | WebSocket | reads Lingma `SharedClientCache` files under user application support paths and `~/.lingma` fallbacks | +| Windows | Named Pipe / WebSocket | scans Lingma named pipes plus `%APPDATA%`, `%LOCALAPPDATA%`, `%ProgramData%`, and `%USERPROFILE%\.lingma` shared cache hints | +| Linux | WebSocket | reads `~/.lingma` / XDG hints when present; manual `--ws-url` is still recommended | If auto detection fails, set the path manually in the desktop Settings page or pass CLI flags: @@ -193,6 +193,9 @@ By default it reads the local Lingma login cache in read-only mode: ~/.lingma/cache/user ~/.lingma/cache/id ~/.lingma/logs/lingma.log +%APPDATA%\Lingma\cache\user +%LOCALAPPDATA%\Lingma\cache\user +XDG config/state Lingma cache paths when present ``` You can also pass an explicit credential file: @@ -222,7 +225,8 @@ Credential file format: Notes: - Remote mode does not write or migrate login state. It only reads the local Lingma cache or the credential file you provide. -- If your Lingma plugin uses a dedicated domain, set `--remote-base-url`, `LINGMA_REMOTE_BASE_URL`, or the JSON config field explicitly. +- If your Lingma plugin uses a dedicated domain, remote mode first uses `--remote-base-url`, `LINGMA_REMOTE_BASE_URL`, or the JSON config field. If those are empty, it scans Lingma's local logs on macOS, Windows, and Linux for endpoint hints such as `endpoint config:` and marketplace service URLs. +- The desktop Settings page shows the resolved remote domain and detection source without exposing tokens. - `/v1/models` in remote mode returns remote API model keys, which may not match the IPC plugin display IDs such as `MiniMax-M2.7` or `Kimi-K2.6`. - Local validation passed `/health`, `/v1/models`, OpenAI streaming/non-streaming chat, and Claude Code Anthropic + Bash tool use. Claude Code full tool runs are much slower than simple OpenAI requests because the client sends a large context and performs a second tool-result turn. - This mode is inspired by the remote API and credential-signing research in [ZipperCode/lingma2api](https://github.com/ZipperCode/lingma2api), integrated here as a switchable backend under the existing OpenAI / Anthropic / desktop app architecture. diff --git a/README.zh-CN.md b/README.zh-CN.md index 253433d..bab1d81 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,8 +1,8 @@ -# Lingma IPC Proxy +# Lingma Proxy [English](./README.md) | [简体中文](./README.zh-CN.md) -**Lingma IPC Proxy** 是一个通义灵码 IDE 插件 API 适配层。它把 Lingma 插件的本地私有 IPC / WebSocket 能力转换成标准 **OpenAI 兼容接口** 和 **Anthropic 兼容接口**,让 Claude Code、Cline、Continue、OpenCode、自研 Agent 等第三方客户端可以直接调用 Lingma 后端模型。 +**Lingma Proxy** 是一个通义灵码 API 适配层。它既可以把 Lingma 插件的本地私有 IPC / WebSocket 能力转换成标准 **OpenAI 兼容接口** 和 **Anthropic 兼容接口**,也可以使用实验性的远端 API 模式直接调用 Lingma 远端接口,让 Claude Code、Cline、Continue、OpenCode、自研 Agent 等第三方客户端可以直接调用 Lingma 后端模型。 项目同时提供两种使用方式: @@ -16,7 +16,7 @@ ## 当前版本 -当前桌面端版本线:`v1.4.0` +当前桌面端版本线:`v1.4.1` GitHub Actions 会在 Release 中产出: @@ -216,9 +216,9 @@ flowchart LR | 平台 | 优先传输 | 探测方式 | | --- | --- | --- | -| macOS | WebSocket | 扫描用户目录下 Lingma `SharedClientCache` 配置 | -| Windows | Named Pipe / WebSocket | 扫描 Lingma 命名管道和共享缓存信息 | -| Linux | WebSocket | 建议手动指定 `--ws-url` | +| macOS | WebSocket | 扫描 Lingma `SharedClientCache`、`~/.lingma` 等用户目录 | +| Windows | Named Pipe / WebSocket | 扫描 Lingma 命名管道,以及 `%APPDATA%`、`%LOCALAPPDATA%`、`%ProgramData%`、`%USERPROFILE%\.lingma` 下的共享缓存信息 | +| Linux | WebSocket | 尝试读取 `~/.lingma` / XDG 目录,仍建议必要时手动指定 `--ws-url` | 如果自动探测失败,桌面端会提供兜底说明。可以在设置里手动填写: @@ -259,6 +259,9 @@ lingma-ipc-proxy --backend remote --port 8095 ~/.lingma/cache/user ~/.lingma/cache/id ~/.lingma/logs/lingma.log +%APPDATA%\Lingma\cache\user +%LOCALAPPDATA%\Lingma\cache\user +存在时也会尝试 XDG 配置 / 状态目录 ``` 也可以指定显式凭据文件: @@ -288,7 +291,8 @@ lingma-ipc-proxy \ 说明: - 远端模式不会写入或迁移你的登录态,只会读取本机 Lingma 缓存或你指定的凭据文件。 -- 如果 Lingma 插件配置过专属域名,可以通过 `--remote-base-url`、`LINGMA_REMOTE_BASE_URL` 或配置文件显式指定。 +- 如果 Lingma 插件配置过专属域名,远端模式会优先使用 `--remote-base-url`、`LINGMA_REMOTE_BASE_URL` 或配置文件;这些为空时,会扫描 macOS、Windows、Linux 上 Lingma 本地日志里的 `endpoint config:`、Marketplace service URL 等线索。 +- 桌面端设置页会展示当前解析到的远端域名和来源,但不会展示 token / key 明文。 - 远端模式的 `/v1/models` 返回的是远端接口模型 key,不一定等同于 IPC 插件模式里看到的 `MiniMax-M2.7`、`Kimi-K2.6` 等展示名。 - 当前本机实测:`/health`、`/v1/models`、OpenAI 流式 / 非流式、Claude Code Anthropic + Bash 工具调用均可用;Claude Code 完整工具链耗时明显高于简单 OpenAI 请求。 - 该模式参考了 [ZipperCode/lingma2api](https://github.com/ZipperCode/lingma2api) 对 Lingma 远端接口、签名和登录态结构的探索,本仓库将其作为可切换后端集成到现有 OpenAI / Anthropic / 桌面 App 架构中。 diff --git a/desktop/app.go b/desktop/app.go index aa53deb..6badec5 100644 --- a/desktop/app.go +++ b/desktop/app.go @@ -15,6 +15,7 @@ import ( "lingma-ipc-proxy/internal/httpapi" "lingma-ipc-proxy/internal/lingmaipc" + "lingma-ipc-proxy/internal/remote" "lingma-ipc-proxy/internal/service" "github.com/wailsapp/wails/v2/pkg/options" @@ -58,11 +59,32 @@ type ModelInfo struct { type ProxyStatus struct { Running bool `json:"running"` Addr string `json:"addr"` + Backend string `json:"backend"` Models int `json:"models"` Model string `json:"model,omitempty"` StartedAt string `json:"startedAt,omitempty"` } +// DetectionInfo exposes non-sensitive resolved connection details for the UI. +type DetectionInfo struct { + ListenURL string `json:"listenUrl"` + Backend string `json:"backend"` + BackendLabel string `json:"backendLabel"` + IPCSuccess bool `json:"ipcSuccess"` + IPCTransport string `json:"ipcTransport,omitempty"` + IPCEndpoint string `json:"ipcEndpoint,omitempty"` + IPCError string `json:"ipcError,omitempty"` + RemoteBaseURL string `json:"remoteBaseUrl"` + RemoteBaseURLSource string `json:"remoteBaseUrlSource,omitempty"` + RemoteCredentialSuccess bool `json:"remoteCredentialSuccess"` + RemoteCredentialSource string `json:"remoteCredentialSource,omitempty"` + RemoteUserID string `json:"remoteUserId,omitempty"` + RemoteMachineID string `json:"remoteMachineId,omitempty"` + RemoteTokenExpireAt string `json:"remoteTokenExpireAt,omitempty"` + RemoteTokenExpired bool `json:"remoteTokenExpired"` + RemoteCredentialError string `json:"remoteCredentialError,omitempty"` +} + // NewApp creates a new App application struct func NewApp() *App { return &App{} @@ -201,6 +223,7 @@ func (a *App) GetStatus() ProxyStatus { return ProxyStatus{ Running: a.running, Addr: a.addr, + Backend: string(a.cfg.Backend), Models: len(a.models), Model: a.cfg.Model, StartedAt: startedAt, @@ -217,6 +240,54 @@ func (a *App) GetConfig() service.Config { return cfg } +// GetDetectionInfo returns resolved IPC/remote details without exposing tokens. +func (a *App) GetDetectionInfo() DetectionInfo { + a.mu.RLock() + cfg := a.cfg + addr := a.addr + a.mu.RUnlock() + + if strings.TrimSpace(addr) == "" { + addr = fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) + } + baseURL := remote.ResolveBaseURLWithSource(cfg.RemoteBaseURL) + info := DetectionInfo{ + ListenURL: "http://" + addr, + Backend: string(cfg.Backend), + BackendLabel: backendLabel(cfg.Backend), + RemoteBaseURL: baseURL.URL, + RemoteBaseURLSource: baseURL.Source, + } + + if opts, err := lingmaipc.ResolveDialOptions(cfg.Transport, cfg.Pipe, cfg.WebSocketURL); err == nil { + info.IPCSuccess = true + info.IPCTransport = string(opts.Transport) + switch opts.Transport { + case lingmaipc.TransportPipe: + info.IPCEndpoint = opts.PipePath + case lingmaipc.TransportWebSocket: + info.IPCEndpoint = opts.WebSocketURL + } + } else { + info.IPCError = err.Error() + } + + if cred, err := remote.LoadCredential(cfg.RemoteAuthFile); err == nil { + info.RemoteCredentialSuccess = true + info.RemoteCredentialSource = cred.Source + info.RemoteUserID = maskIdentifier(cred.UserID) + info.RemoteMachineID = maskIdentifier(cred.MachineID) + info.RemoteTokenExpired = remote.IsExpired(cred, 0) + if cred.TokenExpireTime > 0 { + info.RemoteTokenExpireAt = time.UnixMilli(cred.TokenExpireTime).Format(time.RFC3339) + } + } else { + info.RemoteCredentialError = err.Error() + } + + return info +} + // UpdateConfig updates the configuration, saves to file, and restarts the proxy if running. // Frontend sends Timeout in seconds; we convert to time.Duration. func (a *App) UpdateConfig(cfg service.Config) error { @@ -607,6 +678,27 @@ func defaultConfig() service.Config { return cfg } +func backendLabel(backend service.BackendMode) string { + switch backend { + case service.BackendRemote: + return "远端 API" + default: + return "IPC 插件" + } +} + +func maskIdentifier(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return "" + } + runes := []rune(value) + if len(runes) <= 8 { + return string(runes[:1]) + "***" + } + return string(runes[:4]) + "..." + string(runes[len(runes)-4:]) +} + func configSearchPaths() []string { var paths []string // 1. Executable directory (for dev / portable mode) diff --git a/desktop/frontend/src/App.vue b/desktop/frontend/src/App.vue index 3a37c5f..fea40aa 100644 --- a/desktop/frontend/src/App.vue +++ b/desktop/frontend/src/App.vue @@ -222,7 +222,7 @@ onUnmounted(() => {
{{ status.running ? 'Proxy Running' : 'Proxy Stopped' }} - v1.4.0 + v1.4.1
diff --git a/desktop/frontend/src/style.css b/desktop/frontend/src/style.css index 92ad1b1..7673c2a 100644 --- a/desktop/frontend/src/style.css +++ b/desktop/frontend/src/style.css @@ -1266,6 +1266,83 @@ button:disabled { font-size: 12px; } +.detect-card { + display: grid; + gap: 10px; + margin-top: 14px; + padding: 12px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(255, 255, 255, 0.54); +} + +:root[data-theme="dark"] .detect-card { + background: rgba(15, 23, 42, 0.52); +} + +.detect-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.detect-title strong { + color: var(--text); + font-size: 13px; +} + +.detect-title button { + min-height: 28px; + padding: 0 10px; + color: var(--text); + border: 1px solid var(--line); + border-radius: 7px; + background: var(--surface-strong); + cursor: pointer; +} + +.detect-card dl { + display: grid; + gap: 8px; + margin: 0; +} + +.detect-card dl > div { + display: grid; + grid-template-columns: 82px minmax(0, 1fr); + gap: 10px; + align-items: start; +} + +.detect-card dt { + color: var(--muted); + font-size: 12px; + font-weight: 680; +} + +.detect-card dd { + min-width: 0; + margin: 0; + color: var(--text); + overflow-wrap: anywhere; + font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace; + font-size: 12px; + line-height: 1.45; +} + +.muted-inline { + display: block; + margin-top: 4px; + color: var(--muted); + font-size: 12px; + font-weight: 650; +} + +.warn-text { + color: var(--danger-text) !important; +} + .form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); diff --git a/desktop/frontend/src/views/Settings.vue b/desktop/frontend/src/views/Settings.vue index 793091b..ada94cc 100644 --- a/desktop/frontend/src/views/Settings.vue +++ b/desktop/frontend/src/views/Settings.vue @@ -1,10 +1,11 @@