Improve remote endpoint detection
This commit is contained in:
@@ -194,30 +194,68 @@ func resolveSharedClientInfo() (sharedClientInfo, error) {
|
||||
}
|
||||
|
||||
func defaultSharedClientInfoPaths() []string {
|
||||
bases := make([]string, 0, 2)
|
||||
if appData := strings.TrimSpace(os.Getenv("APPDATA")); appData != "" {
|
||||
bases = append(bases, appData)
|
||||
if explicit := strings.TrimSpace(os.Getenv("LINGMA_SHARED_CLIENT_INFO")); explicit != "" {
|
||||
return []string{explicit}
|
||||
}
|
||||
|
||||
bases := make([]string, 0, 8)
|
||||
if userConfigDir, err := os.UserConfigDir(); err == nil && strings.TrimSpace(userConfigDir) != "" {
|
||||
bases = append(bases, userConfigDir)
|
||||
}
|
||||
if home, err := os.UserHomeDir(); err == nil && strings.TrimSpace(home) != "" {
|
||||
bases = append(bases,
|
||||
filepath.Join(home, ".lingma", "vscode"),
|
||||
filepath.Join(home, ".lingma"),
|
||||
)
|
||||
}
|
||||
for _, envName := range []string{"APPDATA", "LOCALAPPDATA", "ProgramData"} {
|
||||
if value := strings.TrimSpace(os.Getenv(envName)); value != "" {
|
||||
bases = append(bases, value)
|
||||
}
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
paths := make([]string, 0, len(bases)*2)
|
||||
for _, base := range bases {
|
||||
cacheDir := filepath.Join(base, "Lingma", "SharedClientCache")
|
||||
for _, name := range []string{".info.json", ".info"} {
|
||||
path := filepath.Join(cacheDir, name)
|
||||
if _, ok := seen[path]; ok {
|
||||
continue
|
||||
for _, base := range uniquePathStrings(bases) {
|
||||
cacheDirs := []string{
|
||||
filepath.Join(base, "Lingma", "SharedClientCache"),
|
||||
filepath.Join(base, "Lingma", "sharedClientCache"),
|
||||
filepath.Join(base, "SharedClientCache"),
|
||||
filepath.Join(base, "sharedClientCache"),
|
||||
}
|
||||
for _, cacheDir := range cacheDirs {
|
||||
for _, name := range []string{".info.json", ".info"} {
|
||||
path := filepath.Join(cacheDir, name)
|
||||
if _, ok := seen[path]; ok {
|
||||
continue
|
||||
}
|
||||
seen[path] = struct{}{}
|
||||
paths = append(paths, path)
|
||||
}
|
||||
seen[path] = struct{}{}
|
||||
paths = append(paths, path)
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func uniquePathStrings(values []string) []string {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
out := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
cleaned := filepath.Clean(value)
|
||||
key := strings.ToLower(cleaned)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, cleaned)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func resolveSharedClientInfoFromPaths(paths []string) (sharedClientInfo, error) {
|
||||
var parseErrors []string
|
||||
foundFile := false
|
||||
|
||||
@@ -9,8 +9,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -35,6 +37,11 @@ type Client struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
type BaseURLHint struct {
|
||||
URL string
|
||||
Source string
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
Key string `json:"key"`
|
||||
DisplayName string `json:"display_name"`
|
||||
@@ -76,18 +83,22 @@ func New(cfg Config) *Client {
|
||||
}
|
||||
|
||||
func ResolveBaseURL(explicit string) string {
|
||||
return ResolveBaseURLWithSource(explicit).URL
|
||||
}
|
||||
|
||||
func ResolveBaseURLWithSource(explicit string) BaseURLHint {
|
||||
if strings.TrimSpace(explicit) != "" {
|
||||
return strings.TrimRight(strings.TrimSpace(explicit), "/")
|
||||
return BaseURLHint{URL: strings.TrimRight(strings.TrimSpace(explicit), "/"), Source: "explicit config"}
|
||||
}
|
||||
if value := strings.TrimSpace(os.Getenv("LINGMA_REMOTE_BASE_URL")); value != "" {
|
||||
return strings.TrimRight(value, "/")
|
||||
return BaseURLHint{URL: strings.TrimRight(value, "/"), Source: "LINGMA_REMOTE_BASE_URL"}
|
||||
}
|
||||
for _, path := range candidateConfigFiles() {
|
||||
if value := readBaseURLHint(path); value != "" {
|
||||
return strings.TrimRight(value, "/")
|
||||
return BaseURLHint{URL: strings.TrimRight(value, "/"), Source: path}
|
||||
}
|
||||
}
|
||||
return DefaultBaseURL
|
||||
return BaseURLHint{URL: DefaultBaseURL, Source: "default"}
|
||||
}
|
||||
|
||||
func (c *Client) Warmup(ctx context.Context) error {
|
||||
@@ -292,7 +303,7 @@ func (c *Client) headers(cred Credential, path string, body string) (map[string]
|
||||
"Cosy-User": cred.UserID,
|
||||
"Cosy-Clientip": "198.18.0.1",
|
||||
"Cosy-Clienttype": "2",
|
||||
"Cosy-Machineos": "x86_64_windows",
|
||||
"Cosy-Machineos": MachineOSHeader(),
|
||||
"Cosy-Machinetoken": "",
|
||||
"Cosy-Machinetype": "",
|
||||
"Cosy-Version": c.cfg.CosyVersion,
|
||||
@@ -381,12 +392,20 @@ func candidateConfigFiles() []string {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return []string{
|
||||
paths := []string{
|
||||
filepath.Join(home, ".lingma", "extension", "server", "config.json"),
|
||||
filepath.Join(home, ".lingma", "extension", "local", "config.json"),
|
||||
filepath.Join(home, ".lingma", "bin", "config.json"),
|
||||
filepath.Join(home, ".config", "lingma-ipc-proxy", "config.json"),
|
||||
filepath.Join(home, ".lingma", "logs", "lingma.log"),
|
||||
filepath.Join(home, ".lingma", "logs", "lingma-extension.log"),
|
||||
filepath.Join(home, ".lingma", "vscode", "sharedClientCache", "logs", "lingma.log"),
|
||||
filepath.Join(home, ".lingma", "vscode", "sharedClientCache", "logs", "lingma-extension.log"),
|
||||
}
|
||||
for _, root := range lingmaLogRoots(home) {
|
||||
paths = append(paths, recentLingmaAppLogs(root)...)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func readBaseURLHint(path string) string {
|
||||
@@ -396,13 +415,12 @@ func readBaseURLHint(path string) string {
|
||||
}
|
||||
var value any
|
||||
if err := json.Unmarshal(body, &value); err != nil {
|
||||
text := string(body)
|
||||
if strings.Contains(text, "lingma.alibabacloud.com") {
|
||||
return DefaultBaseURL
|
||||
}
|
||||
return ""
|
||||
return extractBaseURLFromText(string(body))
|
||||
}
|
||||
return findBaseURL(value)
|
||||
if value := findBaseURL(value); value != "" {
|
||||
return value
|
||||
}
|
||||
return extractBaseURLFromText(string(body))
|
||||
}
|
||||
|
||||
func findBaseURL(value any) string {
|
||||
@@ -429,6 +447,146 @@ func findBaseURL(value any) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func lingmaLogRoots(home string) []string {
|
||||
roots := []string{
|
||||
filepath.Join(home, ".lingma", "logs"),
|
||||
filepath.Join(home, ".lingma", "vscode", "sharedClientCache", "logs"),
|
||||
filepath.Join(home, "Library", "Application Support", "Lingma", "logs"),
|
||||
}
|
||||
for _, envName := range []string{"APPDATA", "LOCALAPPDATA", "ProgramData"} {
|
||||
if value := strings.TrimSpace(os.Getenv(envName)); value != "" {
|
||||
roots = append(roots,
|
||||
filepath.Join(value, "Lingma", "logs"),
|
||||
filepath.Join(value, "Code", "User", "globalStorage", "alibaba-cloud.tongyi-lingma", "logs"),
|
||||
)
|
||||
}
|
||||
}
|
||||
if value := strings.TrimSpace(os.Getenv("XDG_CONFIG_HOME")); value != "" {
|
||||
roots = append(roots, filepath.Join(value, "Lingma", "logs"))
|
||||
}
|
||||
if value := strings.TrimSpace(os.Getenv("XDG_STATE_HOME")); value != "" {
|
||||
roots = append(roots, filepath.Join(value, "Lingma", "logs"))
|
||||
}
|
||||
roots = append(roots,
|
||||
filepath.Join(home, ".config", "Lingma", "logs"),
|
||||
filepath.Join(home, ".local", "state", "Lingma", "logs"),
|
||||
)
|
||||
return uniqueStrings(roots)
|
||||
}
|
||||
|
||||
func recentLingmaAppLogs(root string) []string {
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
type logDir struct {
|
||||
path string
|
||||
modTime int64
|
||||
}
|
||||
dirs := make([]logDir, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dirs = append(dirs, logDir{path: filepath.Join(root, entry.Name()), modTime: info.ModTime().UnixNano()})
|
||||
}
|
||||
sort.Slice(dirs, func(i, j int) bool { return dirs[i].modTime > dirs[j].modTime })
|
||||
if len(dirs) > 5 {
|
||||
dirs = dirs[:5]
|
||||
}
|
||||
paths := make([]string, 0, len(dirs)*4)
|
||||
for _, dir := range dirs {
|
||||
_ = filepath.WalkDir(dir.path, func(path string, entry os.DirEntry, err error) error {
|
||||
if err != nil || entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
name := entry.Name()
|
||||
lowerName := strings.ToLower(name)
|
||||
if lowerName == "renderer.log" ||
|
||||
lowerName == "sharedprocess.log" ||
|
||||
lowerName == "main.log" ||
|
||||
strings.HasSuffix(name, "Lingma.log") ||
|
||||
strings.Contains(lowerName, "lingma") && strings.HasSuffix(lowerName, ".log") {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func uniqueStrings(values []string) []string {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
out := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[value]; ok {
|
||||
continue
|
||||
}
|
||||
seen[value] = struct{}{}
|
||||
out = append(out, value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func extractBaseURLFromText(text string) string {
|
||||
for _, marker := range []string{
|
||||
"endpoint config:",
|
||||
"Using service url:",
|
||||
"Download asset from:",
|
||||
"https://ai-lingma",
|
||||
"https://lingma",
|
||||
} {
|
||||
if value := extractBaseURLAfterMarker(text, marker); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractBaseURLAfterMarker(text, marker string) string {
|
||||
lowerText := strings.ToLower(text)
|
||||
lowerMarker := strings.ToLower(marker)
|
||||
index := strings.LastIndex(lowerText, lowerMarker)
|
||||
if index < 0 {
|
||||
return ""
|
||||
}
|
||||
tail := text[index+len(marker):]
|
||||
if strings.HasPrefix(lowerMarker, "https://") {
|
||||
tail = marker + tail
|
||||
}
|
||||
for _, field := range strings.Fields(tail) {
|
||||
field = strings.Trim(field, `"'<>),]}`)
|
||||
if value := normalizeRemoteBaseURLHint(field); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeRemoteBaseURLHint(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return ""
|
||||
}
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return ""
|
||||
}
|
||||
host := strings.ToLower(parsed.Host)
|
||||
if !strings.Contains(host, "lingma") && !strings.Contains(host, "rdc.aliyuncs.com") {
|
||||
return ""
|
||||
}
|
||||
return parsed.Scheme + "://" + parsed.Host
|
||||
}
|
||||
|
||||
func estimateTokens(text string) int {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
|
||||
19
internal/remote/client_test.go
Normal file
19
internal/remote/client_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package remote
|
||||
|
||||
import "testing"
|
||||
|
||||
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"
|
||||
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"
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -62,22 +63,33 @@ func loadCredentialFile(path string) (Credential, error) {
|
||||
}
|
||||
|
||||
func importLingmaCacheCredential() (Credential, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return Credential{}, err
|
||||
var attempts []string
|
||||
for _, lingmaDir := range candidateLingmaCacheDirs() {
|
||||
cred, err := importLingmaCacheCredentialFromDir(lingmaDir)
|
||||
if err == nil {
|
||||
return cred, nil
|
||||
}
|
||||
attempts = append(attempts, fmt.Sprintf("%s: %v", lingmaDir, err))
|
||||
}
|
||||
lingmaDir := filepath.Join(home, ".lingma")
|
||||
if len(attempts) == 0 {
|
||||
return Credential{}, errors.New("no Lingma cache directory candidate was found")
|
||||
}
|
||||
return Credential{}, fmt.Errorf("load Lingma login cache: %s", strings.Join(attempts, "; "))
|
||||
}
|
||||
|
||||
func importLingmaCacheCredentialFromDir(lingmaDir string) (Credential, error) {
|
||||
machineID, err := loadMachineID(lingmaDir)
|
||||
if err != nil {
|
||||
return Credential{}, err
|
||||
}
|
||||
encrypted, err := os.ReadFile(filepath.Join(lingmaDir, "cache", "user"))
|
||||
userPath := filepath.Join(lingmaDir, "cache", "user")
|
||||
encrypted, err := os.ReadFile(userPath)
|
||||
if err != nil {
|
||||
return Credential{}, fmt.Errorf("read ~/.lingma/cache/user: %w", err)
|
||||
return Credential{}, fmt.Errorf("read %s: %w", userPath, err)
|
||||
}
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(encrypted)))
|
||||
if err != nil {
|
||||
return Credential{}, fmt.Errorf("decode ~/.lingma/cache/user: %w", err)
|
||||
return Credential{}, fmt.Errorf("decode %s: %w", userPath, err)
|
||||
}
|
||||
plaintext, err := decryptCacheUser(machineID, ciphertext)
|
||||
if err != nil {
|
||||
@@ -90,19 +102,46 @@ func importLingmaCacheCredential() (Credential, error) {
|
||||
ExpireTime any `json:"expire_time"`
|
||||
}
|
||||
if err := json.Unmarshal(plaintext, &payload); err != nil {
|
||||
return Credential{}, fmt.Errorf("parse ~/.lingma/cache/user: %w", err)
|
||||
return Credential{}, fmt.Errorf("parse %s: %w", userPath, err)
|
||||
}
|
||||
cred := Credential{
|
||||
CosyKey: payload.Key,
|
||||
EncryptUserInfo: payload.EncryptUserInfo,
|
||||
UserID: payload.UserID,
|
||||
MachineID: machineID,
|
||||
Source: "~/.lingma/cache/user",
|
||||
Source: userPath,
|
||||
TokenExpireTime: parseExpireAny(payload.ExpireTime),
|
||||
}
|
||||
return cred, validateCredential(cred)
|
||||
}
|
||||
|
||||
func candidateLingmaCacheDirs() []string {
|
||||
if explicit := strings.TrimSpace(os.Getenv("LINGMA_CACHE_DIR")); explicit != "" {
|
||||
return []string{expandHome(explicit)}
|
||||
}
|
||||
|
||||
var dirs []string
|
||||
if home, err := os.UserHomeDir(); err == nil && strings.TrimSpace(home) != "" {
|
||||
dirs = append(dirs,
|
||||
filepath.Join(home, ".lingma"),
|
||||
filepath.Join(home, ".config", "Lingma"),
|
||||
filepath.Join(home, ".local", "share", "Lingma"),
|
||||
)
|
||||
}
|
||||
for _, envName := range []string{"APPDATA", "LOCALAPPDATA", "ProgramData"} {
|
||||
if value := strings.TrimSpace(os.Getenv(envName)); value != "" {
|
||||
dirs = append(dirs,
|
||||
filepath.Join(value, "Lingma"),
|
||||
filepath.Join(value, "lingma"),
|
||||
)
|
||||
}
|
||||
}
|
||||
if value := strings.TrimSpace(os.Getenv("XDG_CONFIG_HOME")); value != "" {
|
||||
dirs = append(dirs, filepath.Join(value, "Lingma"))
|
||||
}
|
||||
return uniquePathStrings(dirs)
|
||||
}
|
||||
|
||||
func loadMachineID(lingmaDir string) (string, error) {
|
||||
if body, err := os.ReadFile(filepath.Join(lingmaDir, "cache", "id")); err == nil {
|
||||
if value := strings.TrimSpace(string(body)); value != "" {
|
||||
@@ -111,7 +150,7 @@ func loadMachineID(lingmaDir string) (string, error) {
|
||||
}
|
||||
logBody, err := os.ReadFile(filepath.Join(lingmaDir, "logs", "lingma.log"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("remote credential requires ~/.lingma/cache/id or lingma.log machine id: %w", err)
|
||||
return "", fmt.Errorf("remote credential requires cache/id or lingma.log machine id: %w", err)
|
||||
}
|
||||
markers := []string{"using machine id from file:", "machine id:"}
|
||||
text := string(logBody)
|
||||
@@ -128,7 +167,7 @@ func loadMachineID(lingmaDir string) (string, error) {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("machine id not found in ~/.lingma cache")
|
||||
return "", errors.New("machine id not found in Lingma cache")
|
||||
}
|
||||
|
||||
func decryptCacheUser(machineID string, ciphertext []byte) ([]byte, error) {
|
||||
@@ -203,3 +242,44 @@ func parseExpireAny(value any) int64 {
|
||||
func IsExpired(cred Credential, margin time.Duration) bool {
|
||||
return cred.TokenExpireTime > 0 && time.Now().Add(margin).UnixMilli() > cred.TokenExpireTime
|
||||
}
|
||||
|
||||
func MachineOSHeader() string {
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
if runtime.GOARCH == "arm64" {
|
||||
return "arm64_darwin"
|
||||
}
|
||||
return "x86_64_darwin"
|
||||
case "windows":
|
||||
if runtime.GOARCH == "arm64" {
|
||||
return "arm64_windows"
|
||||
}
|
||||
return "x86_64_windows"
|
||||
case "linux":
|
||||
if runtime.GOARCH == "arm64" {
|
||||
return "arm64_linux"
|
||||
}
|
||||
return "x86_64_linux"
|
||||
default:
|
||||
return runtime.GOARCH + "_" + runtime.GOOS
|
||||
}
|
||||
}
|
||||
|
||||
func uniquePathStrings(values []string) []string {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
out := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
cleaned := filepath.Clean(value)
|
||||
key := strings.ToLower(cleaned)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, cleaned)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user