Files
2026-05-07 16:44:59 +08:00

962 lines
24 KiB
Go

package remote
import (
"bufio"
"context"
"crypto/md5"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
"lingma-ipc-proxy/internal/toolemulation"
)
const (
DefaultBaseURL = "https://lingma.alibabacloud.com"
chatPath = "/algo/api/v2/service/pro/sse/agent_chat_generation"
chatQuery = "?FetchKeys=llm_model_result&AgentId=agent_common"
modelListPath = "/algo/api/v2/model/list"
)
var remoteBaseURLPattern = regexp.MustCompile(`https?://[^\s"'<>),\]}]+`)
type Config struct {
BaseURL string
AuthFile string
CosyVersion string
Timeout time.Duration
}
type Client struct {
cfg Config
client *http.Client
}
type BaseURLHint struct {
URL string
Source string
}
type Model struct {
Key string `json:"key"`
DisplayName string `json:"display_name"`
Model string `json:"model"`
Enable bool `json:"enable"`
}
type ChatRequest struct {
Model string
Prompt string
Messages []Message
Images []Image
Stream bool
Temperature *float64
Tools []toolemulation.ToolDef
ToolChoice toolemulation.ToolChoice
}
type Image struct {
MediaType string
Data string
URL string
}
type Message struct {
Role string
Content string
Images []Image
Name string
ToolCallID string
ToolCalls []toolemulation.ToolCall
}
type ChatResult struct {
Text string
InputTokens int
OutputTokens int
RequestID string
CredentialSrc string
ToolCalls []toolemulation.ToolCall
}
type StreamEvent struct {
Delta string
}
func New(cfg Config) *Client {
if cfg.BaseURL == "" {
cfg.BaseURL = ResolveBaseURL("")
}
if cfg.CosyVersion == "" {
cfg.CosyVersion = "2.11.2"
}
cfg.BaseURL = strings.TrimRight(cfg.BaseURL, "/")
return &Client{cfg: cfg, client: &http.Client{Timeout: cfg.Timeout}}
}
func ResolveBaseURL(explicit string) string {
return ResolveBaseURLWithSource(explicit).URL
}
func ResolveBaseURLWithSource(explicit string) BaseURLHint {
if 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 BaseURLHint{URL: strings.TrimRight(value, "/"), Source: "LINGMA_REMOTE_BASE_URL"}
}
for _, path := range candidateConfigFiles() {
if value := readBaseURLHint(path); value != "" {
return BaseURLHint{URL: strings.TrimRight(value, "/"), Source: path}
}
}
return BaseURLHint{URL: DefaultBaseURL, Source: "default"}
}
func (c *Client) Warmup(ctx context.Context) error {
_, err := LoadCredential(c.cfg.AuthFile)
if err != nil {
return err
}
ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
defer cancel()
_, err = c.ListModels(ctx)
return err
}
func (c *Client) ListModels(ctx context.Context) ([]Model, error) {
cred, err := LoadCredential(c.cfg.AuthFile)
if err != nil {
return nil, err
}
headers, err := c.headers(cred, modelListPath, "")
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.cfg.BaseURL+modelListPath, nil)
if err != nil {
return nil, err
}
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
return nil, c.modelListStatusError(resp.StatusCode, string(body))
}
var payload struct {
Chat []Model `json:"chat"`
Inline []Model `json:"inline"`
}
if err := json.Unmarshal(body, &payload); err != nil {
return nil, err
}
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 {
return nil, err
}
requestID := newHexID()
body, err := c.buildBody(requestID, request)
if err != nil {
return nil, err
}
headers, err := c.headers(cred, chatPath, body)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.cfg.BaseURL+chatPath+chatQuery, strings.NewReader(body))
if err != nil {
return nil, err
}
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
respBody, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("remote chat status %d: %s", resp.StatusCode, truncate(string(respBody), 1000))
}
var builder strings.Builder
toolCallBuffer := newRemoteToolCallBuffer()
if err := scanSSE(resp.Body, func(event sseEvent) error {
if event.Done {
return nil
}
if len(event.ToolCalls) > 0 {
toolCallBuffer.Add(event.ToolCalls)
}
if event.Content == "" {
return nil
}
builder.WriteString(event.Content)
if onDelta != nil {
onDelta(event.Content)
}
return nil
}); err != nil {
return nil, err
}
text := builder.String()
return &ChatResult{
Text: text,
InputTokens: estimateTokens(request.Prompt),
OutputTokens: estimateTokens(text),
RequestID: requestID,
CredentialSrc: cred.Source,
ToolCalls: toolCallBuffer.Calls(),
}, nil
}
func (c *Client) buildBody(requestID string, request ChatRequest) (string, error) {
temperature := 0.1
if request.Temperature != nil {
temperature = *request.Temperature
}
model := strings.TrimSpace(request.Model)
if strings.EqualFold(model, "auto") {
model = ""
}
imageURLs := projectImages(request.Images)
payload := map[string]any{
"request_id": requestID,
"request_set_id": "",
"chat_record_id": requestID,
"stream": true,
"image_urls": nullableSlice(imageURLs),
"is_reply": false,
"is_retry": false,
"session_id": "",
"code_language": "",
"source": 0,
"version": "3",
"chat_prompt": "",
"parameters": map[string]float64{"temperature": temperature},
"aliyun_user_type": "personal_standard",
"agent_id": "agent_common",
"task_id": "question_refine",
"model_config": map[string]any{
"key": model,
"display_name": "",
"model": model,
"format": "",
"is_vl": len(imageURLs) > 0,
"is_reasoning": false,
"api_key": "",
"url": "",
"source": "",
"enable": false,
},
"messages": projectMessages(request),
"business": map[string]any{
"product": "jb_plugin",
"version": c.cfg.CosyVersion,
"type": "memory",
"id": newUUID(),
"begin_at": time.Now().UnixMilli(),
"stage": "start",
"name": "memory_intent_recognition_" + requestID,
},
}
if tools := projectTools(request.Tools); len(tools) > 0 {
payload["tools"] = tools
}
if choice := projectToolChoice(request.ToolChoice); choice != nil {
payload["tool_choice"] = choice
}
body, err := json.Marshal(payload)
return string(body), err
}
func nullableSlice[T any](items []T) any {
if len(items) == 0 {
return nil
}
return items
}
func projectImages(images []Image) []string {
if len(images) == 0 {
return nil
}
out := make([]string, 0, len(images))
for _, img := range images {
item := projectImage(img)
if item != "" {
out = append(out, item)
}
}
return out
}
func projectImage(img Image) string {
if strings.TrimSpace(img.Data) == "" && strings.TrimSpace(img.URL) == "" {
return ""
}
mediaType := strings.TrimSpace(img.MediaType)
if mediaType == "" {
mediaType = "image/jpeg"
}
if strings.TrimSpace(img.Data) != "" {
return "data:" + mediaType + ";base64," + strings.TrimSpace(img.Data)
}
return strings.TrimSpace(img.URL)
}
func projectMessages(request ChatRequest) []map[string]any {
source := request.Messages
if len(source) == 0 {
source = []Message{{Role: "user", Content: request.Prompt}}
}
out := make([]map[string]any, 0, len(source))
for _, message := range source {
role := strings.TrimSpace(message.Role)
if role == "" {
continue
}
item := map[string]any{
"role": role,
"content": projectMessageContent(message),
"response_meta": map[string]any{
"id": "",
"usage": map[string]int{
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0,
},
},
"reasoning_content_signature": "",
}
if message.Name != "" {
item["name"] = message.Name
}
if message.ToolCallID != "" {
item["tool_call_id"] = message.ToolCallID
}
if calls := projectMessageToolCalls(message.ToolCalls); len(calls) > 0 {
item["tool_calls"] = calls
}
out = append(out, item)
}
if len(out) == 0 {
return []map[string]any{{"role": "user", "content": request.Prompt}}
}
return out
}
func projectMessageContent(message Message) any {
if len(message.Images) == 0 {
return message.Content
}
content := make([]map[string]any, 0, len(message.Images)+1)
if strings.TrimSpace(message.Content) != "" {
content = append(content, map[string]any{
"type": "text",
"text": message.Content,
})
}
for _, img := range message.Images {
imageURL := projectImage(img)
if imageURL == "" {
continue
}
content = append(content, map[string]any{
"type": "image_url",
"image_url": map[string]any{
"url": imageURL,
},
})
}
if len(content) == 0 {
return message.Content
}
return content
}
func projectMessageToolCalls(calls []toolemulation.ToolCall) []map[string]any {
if len(calls) == 0 {
return nil
}
out := make([]map[string]any, 0, len(calls))
for i, call := range calls {
name := strings.TrimSpace(call.Name)
if name == "" {
continue
}
args, _ := json.Marshal(call.Arguments)
out = append(out, map[string]any{
"index": i,
"id": strings.TrimSpace(call.ID),
"type": "function",
"function": map[string]any{
"name": name,
"arguments": string(args),
},
})
}
return out
}
func projectTools(tools []toolemulation.ToolDef) []map[string]any {
if len(tools) == 0 {
return nil
}
out := make([]map[string]any, 0, len(tools))
for _, tool := range tools {
name := strings.TrimSpace(tool.Name)
if name == "" {
continue
}
params := any(tool.InputSchema)
if len(tool.InputSchema) == 0 {
params = map[string]any{"type": "object", "properties": map[string]any{}}
}
out = append(out, map[string]any{
"type": "function",
"function": map[string]any{
"name": name,
"description": strings.TrimSpace(tool.Description),
"parameters": params,
},
})
}
return out
}
func projectToolChoice(choice toolemulation.ToolChoice) any {
switch choice.Mode {
case "none":
return "none"
case "any":
return "required"
case "tool":
name := strings.TrimSpace(choice.Name)
if name == "" {
return nil
}
return map[string]any{
"type": "function",
"function": map[string]any{
"name": name,
},
}
default:
return nil
}
}
func (c *Client) headers(cred Credential, path string, body string) (map[string]string, error) {
if err := validateCredential(cred); err != nil {
return nil, err
}
date := strconv.FormatInt(time.Now().Unix(), 10)
authPayload := map[string]string{
"cosyVersion": c.cfg.CosyVersion,
"ideVersion": "",
"info": cred.EncryptUserInfo,
"requestId": newUUID(),
"version": "v1",
}
authPayloadBytes, err := json.Marshal(authPayload)
if err != nil {
return nil, err
}
payloadBase64 := base64.StdEncoding.EncodeToString(authPayloadBytes)
preimage := strings.Join([]string{
payloadBase64,
cred.CosyKey,
date,
body,
normalizePath(path),
}, "\n")
signature := md5.Sum([]byte(preimage))
return map[string]string{
"Authorization": fmt.Sprintf("Bearer COSY.%s.%x", payloadBase64, signature),
"Content-Type": "application/json",
"Appcode": "cosy",
"Cosy-Date": date,
"Cosy-Key": cred.CosyKey,
"Cosy-Machineid": cred.MachineID,
"Cosy-User": cred.UserID,
"Cosy-Clientip": "198.18.0.1",
"Cosy-Clienttype": "2",
"Cosy-Machineos": MachineOSHeader(),
"Cosy-Machinetoken": "",
"Cosy-Machinetype": "",
"Cosy-Version": c.cfg.CosyVersion,
"Login-Version": "v2",
"User-Agent": "lingma-proxy/remote",
"Accept": "text/event-stream",
"Cache-Control": "no-cache",
}, nil
}
func normalizePath(path string) string {
return strings.TrimPrefix(path, "/algo")
}
type outerSSE struct {
Body string `json:"body"`
StatusCode int `json:"statusCodeValue"`
}
type innerSSE struct {
Choices []struct {
Delta struct {
Content string `json:"content"`
ToolCalls []remoteToolCallDelta `json:"tool_calls"`
} `json:"delta"`
} `json:"choices"`
}
type sseEvent struct {
Content string
ToolCalls []remoteToolCallFragment
Done bool
}
type remoteToolCallFragment struct {
Index int
ID string
Type string
Name string
ArgumentsFragment string
}
type remoteToolCallDelta struct {
Index int `json:"index"`
ID string `json:"id,omitempty"`
Type string `json:"type,omitempty"`
Function struct {
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
} `json:"function,omitempty"`
}
func scanSSE(reader io.Reader, onEvent func(sseEvent) error) error {
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || !strings.HasPrefix(line, "data:") {
continue
}
payload := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
if payload == "[DONE]" {
return onEvent(sseEvent{Done: true})
}
event, ok, err := parseSSEPayload(payload)
if err != nil {
return err
}
if !ok {
continue
}
if err := onEvent(event); err != nil {
return err
}
}
return scanner.Err()
}
func parseSSEPayload(payload string) (sseEvent, bool, error) {
var outer outerSSE
if err := json.Unmarshal([]byte(payload), &outer); err != nil {
return sseEvent{}, false, err
}
if outer.StatusCode >= 400 {
return sseEvent{}, false, fmt.Errorf("remote sse status %d", outer.StatusCode)
}
if outer.Body == "" {
return sseEvent{}, false, nil
}
if outer.Body == "[DONE]" {
return sseEvent{Done: true}, true, nil
}
var inner innerSSE
if err := json.Unmarshal([]byte(outer.Body), &inner); err != nil {
return sseEvent{}, false, err
}
var builder strings.Builder
var toolCalls []remoteToolCallFragment
for _, choice := range inner.Choices {
builder.WriteString(choice.Delta.Content)
for _, tc := range choice.Delta.ToolCalls {
toolCalls = append(toolCalls, remoteToolCallFragment{
Index: tc.Index,
ID: strings.TrimSpace(tc.ID),
Type: strings.TrimSpace(tc.Type),
Name: strings.TrimSpace(tc.Function.Name),
ArgumentsFragment: tc.Function.Arguments,
})
}
}
return sseEvent{Content: builder.String(), ToolCalls: toolCalls}, true, nil
}
type remoteToolCallBuffer struct {
order []int
states map[int]*remoteToolCallState
}
type remoteToolCallState struct {
id string
callType string
name string
arguments strings.Builder
}
func newRemoteToolCallBuffer() *remoteToolCallBuffer {
return &remoteToolCallBuffer{states: map[int]*remoteToolCallState{}}
}
func (b *remoteToolCallBuffer) Add(fragments []remoteToolCallFragment) {
if b == nil {
return
}
for _, fragment := range fragments {
state := b.states[fragment.Index]
if state == nil {
state = &remoteToolCallState{}
b.states[fragment.Index] = state
b.order = append(b.order, fragment.Index)
}
if fragment.ID != "" {
state.id = fragment.ID
}
if fragment.Type != "" {
state.callType = fragment.Type
}
if fragment.Name != "" {
state.name = fragment.Name
}
if fragment.ArgumentsFragment != "" {
state.arguments.WriteString(fragment.ArgumentsFragment)
}
}
}
func (b *remoteToolCallBuffer) Calls() []toolemulation.ToolCall {
if b == nil || len(b.order) == 0 {
return nil
}
out := make([]toolemulation.ToolCall, 0, len(b.order))
for _, index := range b.order {
state := b.states[index]
if state == nil || strings.TrimSpace(state.name) == "" {
continue
}
args := strings.TrimSpace(state.arguments.String())
call := toolemulation.ToolCall{
ID: strings.TrimSpace(state.id),
Name: strings.TrimSpace(state.name),
Arguments: map[string]any{},
}
if args != "" {
var parsed map[string]any
if err := json.Unmarshal([]byte(args), &parsed); err == nil {
call.Arguments = parsed
} else {
call.Arguments = map[string]any{"raw_arguments": args}
}
}
if call.ID == "" {
call.ID = fmt.Sprintf("toolu_%d_%d", time.Now().UnixNano(), index)
}
out = append(out, call)
}
return out
}
func candidateConfigFiles() []string {
home, err := os.UserHomeDir()
if err != nil {
return nil
}
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-proxy", "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 {
body, err := os.ReadFile(path)
if err != nil {
return ""
}
var value any
if err := json.Unmarshal(body, &value); err != nil {
return extractBaseURLFromText(string(body))
}
if value := findBaseURL(value); value != "" {
return value
}
return extractBaseURLFromText(string(body))
}
func findBaseURL(value any) string {
switch typed := value.(type) {
case map[string]any:
for key, item := range typed {
lower := strings.ToLower(key)
if strings.Contains(lower, "base") || strings.Contains(lower, "domain") || strings.Contains(lower, "url") {
if text, ok := item.(string); ok && strings.HasPrefix(strings.TrimSpace(text), "http") && strings.Contains(text, "lingma") {
return strings.TrimSpace(text)
}
}
if nested := findBaseURL(item); nested != "" {
return nested
}
}
case []any:
for _, item := range typed {
if nested := findBaseURL(item); nested != "" {
return nested
}
}
}
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 {
matches := remoteBaseURLPattern.FindAllString(text, -1)
for i := len(matches) - 1; i >= 0; i-- {
if value := normalizeRemoteBaseURLHint(matches[i]); value != "" {
return value
}
}
for _, marker := range []string{
"endpoint config:",
"Using service url:",
"Download asset from:",
} {
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 ""
}
if strings.HasPrefix(raw, "ttps://") {
raw = "h" + raw
}
parsed, err := url.Parse(raw)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return ""
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return ""
}
host := strings.ToLower(parsed.Host)
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 == "" {
return 0
}
return len([]rune(text)) / 4
}
func truncate(value string, max int) string {
value = strings.TrimSpace(value)
if len(value) <= max {
return value
}
return value[:max] + "... [truncated]"
}
func expandHome(path string) string {
if strings.HasPrefix(path, "~/") {
if home, err := os.UserHomeDir(); err == nil {
return filepath.Join(home, strings.TrimPrefix(path, "~/"))
}
}
return path
}
func valueOr(value string, fallback string) string {
if strings.TrimSpace(value) != "" {
return value
}
return fallback
}
var hexCounter uint64