Improve remote endpoint detection
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -222,7 +222,7 @@ onUnmounted(() => {
|
||||
<span class="status-dot" :class="{ running: status.running }"></span>
|
||||
<div>
|
||||
<strong>{{ status.running ? 'Proxy Running' : 'Proxy Stopped' }}</strong>
|
||||
<small>v1.4.0</small>
|
||||
<small>v1.4.1</small>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { GetConfig, UpdateConfig } from '../../wailsjs/go/main/App.js'
|
||||
import { GetConfig, GetDetectionInfo, UpdateConfig } from '../../wailsjs/go/main/App.js'
|
||||
|
||||
const emit = defineEmits(['log', 'status-refresh'])
|
||||
|
||||
const config = ref({})
|
||||
const detection = ref(null)
|
||||
const saving = ref(false)
|
||||
const openSelect = ref('')
|
||||
|
||||
@@ -47,20 +48,31 @@ function toggleSelect(field) {
|
||||
function chooseOption(field, value) {
|
||||
config.value[field] = value
|
||||
openSelect.value = ''
|
||||
refreshDetection()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
config.value = await GetConfig()
|
||||
await refreshDetection()
|
||||
} catch (e) {
|
||||
emit('log', 'error', '配置加载失败:' + (e.message || String(e)))
|
||||
}
|
||||
})
|
||||
|
||||
async function refreshDetection() {
|
||||
try {
|
||||
detection.value = await GetDetectionInfo()
|
||||
} catch (e) {
|
||||
emit('log', 'warn', '探测信息加载失败:' + (e.message || String(e)))
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
await UpdateConfig(config.value)
|
||||
await refreshDetection()
|
||||
emit('log', 'info', '配置已保存,代理已按需重启')
|
||||
emit('status-refresh')
|
||||
} catch (e) {
|
||||
@@ -169,6 +181,50 @@ async function save() {
|
||||
<strong>自动探测失败时</strong>
|
||||
<span>IPC 模式先确认 VS Code / Lingma 插件已启动并登录。远端 API 模式会优先读取认证文件;留空时只读 <code>~/.lingma/cache/user</code>,不会写入或上传登录态。</span>
|
||||
</div>
|
||||
<div v-if="detection" class="detect-card">
|
||||
<div class="detect-title">
|
||||
<strong>当前解析结果</strong>
|
||||
<button type="button" @click="refreshDetection">刷新</button>
|
||||
</div>
|
||||
<dl>
|
||||
<div>
|
||||
<dt>监听地址</dt>
|
||||
<dd>{{ detection.listenUrl || '未启动' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>当前后端</dt>
|
||||
<dd>{{ detection.backendLabel || detection.backend }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>IPC 地址</dt>
|
||||
<dd v-if="detection.ipcSuccess">{{ detection.ipcTransport }} · {{ detection.ipcEndpoint }}</dd>
|
||||
<dd v-else class="warn-text">{{ detection.ipcError || '未探测到' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>远端域名</dt>
|
||||
<dd>
|
||||
{{ detection.remoteBaseUrl }}
|
||||
<span v-if="detection.remoteBaseUrlSource" class="muted-inline">来自 {{ detection.remoteBaseUrlSource }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>登录态来源</dt>
|
||||
<dd v-if="detection.remoteCredentialSuccess">{{ detection.remoteCredentialSource }}</dd>
|
||||
<dd v-else class="warn-text">{{ detection.remoteCredentialError || '未探测到' }}</dd>
|
||||
</div>
|
||||
<div v-if="detection.remoteCredentialSuccess">
|
||||
<dt>账号 / 机器</dt>
|
||||
<dd>{{ detection.remoteUserId || '未知用户' }} · {{ detection.remoteMachineId || '未知机器' }}</dd>
|
||||
</div>
|
||||
<div v-if="detection.remoteCredentialSuccess">
|
||||
<dt>登录态有效期</dt>
|
||||
<dd :class="{ 'warn-text': detection.remoteTokenExpired }">
|
||||
{{ detection.remoteTokenExpireAt || '未提供' }}
|
||||
<span v-if="detection.remoteTokenExpired">(已过期)</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="glass-panel">
|
||||
|
||||
2
desktop/frontend/wailsjs/go/main/App.d.ts
vendored
2
desktop/frontend/wailsjs/go/main/App.d.ts
vendored
@@ -9,6 +9,8 @@ export function ClearRequests():Promise<void>;
|
||||
|
||||
export function GetConfig():Promise<service.Config>;
|
||||
|
||||
export function GetDetectionInfo():Promise<main.DetectionInfo>;
|
||||
|
||||
export function GetModels():Promise<Array<main.ModelInfo>>;
|
||||
|
||||
export function GetRequests():Promise<Array<main.RequestRecord>>;
|
||||
|
||||
@@ -14,6 +14,10 @@ export function GetConfig() {
|
||||
return window['go']['main']['App']['GetConfig']();
|
||||
}
|
||||
|
||||
export function GetDetectionInfo() {
|
||||
return window['go']['main']['App']['GetDetectionInfo']();
|
||||
}
|
||||
|
||||
export function GetModels() {
|
||||
return window['go']['main']['App']['GetModels']();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
export namespace main {
|
||||
|
||||
export class DetectionInfo {
|
||||
listenUrl: string;
|
||||
backend: string;
|
||||
backendLabel: string;
|
||||
ipcSuccess: boolean;
|
||||
ipcTransport?: string;
|
||||
ipcEndpoint?: string;
|
||||
ipcError?: string;
|
||||
remoteBaseUrl: string;
|
||||
remoteBaseUrlSource?: string;
|
||||
remoteCredentialSuccess: boolean;
|
||||
remoteCredentialSource?: string;
|
||||
remoteUserId?: string;
|
||||
remoteMachineId?: string;
|
||||
remoteTokenExpireAt?: string;
|
||||
remoteTokenExpired: boolean;
|
||||
remoteCredentialError?: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new DetectionInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.listenUrl = source["listenUrl"];
|
||||
this.backend = source["backend"];
|
||||
this.backendLabel = source["backendLabel"];
|
||||
this.ipcSuccess = source["ipcSuccess"];
|
||||
this.ipcTransport = source["ipcTransport"];
|
||||
this.ipcEndpoint = source["ipcEndpoint"];
|
||||
this.ipcError = source["ipcError"];
|
||||
this.remoteBaseUrl = source["remoteBaseUrl"];
|
||||
this.remoteBaseUrlSource = source["remoteBaseUrlSource"];
|
||||
this.remoteCredentialSuccess = source["remoteCredentialSuccess"];
|
||||
this.remoteCredentialSource = source["remoteCredentialSource"];
|
||||
this.remoteUserId = source["remoteUserId"];
|
||||
this.remoteMachineId = source["remoteMachineId"];
|
||||
this.remoteTokenExpireAt = source["remoteTokenExpireAt"];
|
||||
this.remoteTokenExpired = source["remoteTokenExpired"];
|
||||
this.remoteCredentialError = source["remoteCredentialError"];
|
||||
}
|
||||
}
|
||||
export class ModelInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -17,6 +59,7 @@ export namespace main {
|
||||
export class ProxyStatus {
|
||||
running: boolean;
|
||||
addr: string;
|
||||
backend: string;
|
||||
models: number;
|
||||
model?: string;
|
||||
startedAt?: string;
|
||||
@@ -29,6 +72,7 @@ export namespace main {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.running = source["running"];
|
||||
this.addr = source["addr"];
|
||||
this.backend = source["backend"];
|
||||
this.models = source["models"];
|
||||
this.model = source["model"];
|
||||
this.startedAt = source["startedAt"];
|
||||
|
||||
Reference in New Issue
Block a user