This commit is contained in:
王性驊 2026-03-31 03:02:11 +00:00
parent eaa9af41a4
commit 974f2f2bb5
6 changed files with 141 additions and 45 deletions

21
.env Normal file
View File

@ -0,0 +1,21 @@
# 由 make env 自動產生,請勿手動編輯
CURSOR_BRIDGE_HOST=127.0.0.1
CURSOR_BRIDGE_PORT=8766
CURSOR_BRIDGE_API_KEY=
CURSOR_BRIDGE_TIMEOUT_MS=3600000
CURSOR_BRIDGE_MULTI_PORT=false
CURSOR_BRIDGE_VERBOSE=false
CURSOR_AGENT_BIN=agent
CURSOR_AGENT_NODE=
CURSOR_AGENT_SCRIPT=
CURSOR_BRIDGE_DEFAULT_MODEL=auto
CURSOR_BRIDGE_STRICT_MODEL=true
CURSOR_BRIDGE_MAX_MODE=true
CURSOR_BRIDGE_FORCE=false
CURSOR_BRIDGE_APPROVE_MCPS=false
CURSOR_BRIDGE_WORKSPACE=
CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=true
CURSOR_CONFIG_DIRS=
CURSOR_BRIDGE_TLS_CERT=
CURSOR_BRIDGE_TLS_KEY=
CURSOR_BRIDGE_SESSIONS_LOG=

View File

@ -5,9 +5,9 @@
# ── 伺服器設定 ───────────────────────────────── # ── 伺服器設定 ─────────────────────────────────
HOST ?= 127.0.0.1 HOST ?= 127.0.0.1
PORT ?= 8765 PORT ?= 8766
API_KEY ?= API_KEY ?=
TIMEOUT_MS ?= 300000 TIMEOUT_MS ?= 3600000
MULTI_PORT ?= false MULTI_PORT ?= false
VERBOSE ?= false VERBOSE ?= false
@ -17,7 +17,7 @@ AGENT_NODE ?=
AGENT_SCRIPT ?= AGENT_SCRIPT ?=
DEFAULT_MODEL ?= auto DEFAULT_MODEL ?= auto
STRICT_MODEL ?= true STRICT_MODEL ?= true
MAX_MODE ?= false MAX_MODE ?= true
FORCE ?= false FORCE ?= false
APPROVE_MCPS ?= false APPROVE_MCPS ?= false

37
internal/env/env.go vendored
View File

@ -141,7 +141,26 @@ func discoverAccountDirs(homeDir string) []string {
return dirs return dirs
} }
func OsEnvToMap() EnvSource { func parseDotEnv(path string) EnvSource {
data, err := os.ReadFile(path)
if err != nil {
return nil
}
m := make(EnvSource)
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) == 2 {
m[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
}
}
return m
}
func OsEnvToMap(cwdHint ...string) EnvSource {
m := make(EnvSource) m := make(EnvSource)
for _, kv := range os.Environ() { for _, kv := range os.Environ() {
parts := strings.SplitN(kv, "=", 2) parts := strings.SplitN(kv, "=", 2)
@ -149,6 +168,22 @@ func OsEnvToMap() EnvSource {
m[parts[0]] = parts[1] m[parts[0]] = parts[1]
} }
} }
cwd := ""
if len(cwdHint) > 0 && cwdHint[0] != "" {
cwd = cwdHint[0]
} else {
cwd, _ = os.Getwd()
}
if dotenv := parseDotEnv(filepath.Join(cwd, ".env")); dotenv != nil {
for k, v := range dotenv {
if _, exists := m[k]; !exists {
m[k] = v
}
}
}
return m return m
} }

View File

@ -96,8 +96,7 @@ func HandleChatCompletions(w http.ResponseWriter, r *http.Request, cfg config.Br
return return
} }
if fit.Truncated { if fit.Truncated {
fmt.Printf("[%s] Windows: prompt truncated for CreateProcess limit (%d -> %d chars, tail preserved).\n", logger.LogTruncation(fit.OriginalLength, fit.FinalPromptLength)
time.Now().UTC().Format(time.RFC3339), fit.OriginalLength, fit.FinalPromptLength)
} }
cmdArgs := fit.Args cmdArgs := fit.Args

View File

@ -1,6 +1,7 @@
package logger package logger
import ( import (
"cursor-api-proxy/internal/config"
"cursor-api-proxy/internal/pool" "cursor-api-proxy/internal/pool"
"fmt" "fmt"
"os" "os"
@ -38,7 +39,11 @@ var roleEmoji = map[string]string{
} }
func ts() string { func ts() string {
return cGray + time.Now().UTC().Format(time.RFC3339Nano) + cReset return cGray + time.Now().UTC().Format("15:04:05") + cReset
}
func tsDate() string {
return cGray + time.Now().UTC().Format("2006-01-02 15:04:05") + cReset
} }
func truncate(s string, max int) string { func truncate(s string, max int) string {
@ -60,8 +65,66 @@ type TrafficMessage struct {
Content string Content string
} }
func LogServerStart(version, scheme, host string, port int, cfg config.BridgeConfig) {
fmt.Printf("\n%s%s╔══════════════════════════════════════════╗%s\n", cBold, cBCyan, cReset)
fmt.Printf("%s%s cursor-api-proxy %sv%s%s%s%s ready%s\n",
cBold, cBCyan, cReset, cBold, cWhite, version, cBCyan, cReset)
fmt.Printf("%s%s╚══════════════════════════════════════════╝%s\n\n", cBold, cBCyan, cReset)
url := fmt.Sprintf("%s://%s:%d", scheme, host, port)
fmt.Printf(" %s●%s listening %s%s%s\n", cBGreen, cReset, cBold, url, cReset)
fmt.Printf(" %s▸%s agent %s%s%s\n", cCyan, cReset, cDim, cfg.AgentBin, cReset)
fmt.Printf(" %s▸%s workspace %s%s%s\n", cCyan, cReset, cDim, cfg.Workspace, cReset)
fmt.Printf(" %s▸%s model %s%s%s\n", cCyan, cReset, cDim, cfg.DefaultModel, cReset)
fmt.Printf(" %s▸%s mode %s%s%s\n", cCyan, cReset, cDim, cfg.Mode, cReset)
flags := []string{}
if cfg.Force {
flags = append(flags, "force")
}
if cfg.ApproveMcps {
flags = append(flags, "approve-mcps")
}
if cfg.MaxMode {
flags = append(flags, "max-mode")
}
if cfg.Verbose {
flags = append(flags, "verbose")
}
if cfg.ChatOnlyWorkspace {
flags = append(flags, "chat-only")
}
if cfg.RequiredKey != "" {
flags = append(flags, "api-key-required")
}
if len(flags) > 0 {
fmt.Printf(" %s▸%s flags %s%s%s\n", cCyan, cReset, cYellow, strings.Join(flags, " · "), cReset)
}
if len(cfg.ConfigDirs) > 0 {
fmt.Printf(" %s▸%s pool %s%d accounts%s\n", cCyan, cReset, cBGreen, len(cfg.ConfigDirs), cReset)
}
fmt.Println()
}
func LogShutdown(sig string) {
fmt.Printf("\n%s %s⊘ %s received — shutting down gracefully…%s\n", tsDate(), cYellow, sig, cReset)
}
func LogIncoming(method, pathname, remoteAddress string) { func LogIncoming(method, pathname, remoteAddress string) {
fmt.Printf("[%s] Incoming: %s %s (from %s)\n", time.Now().UTC().Format(time.RFC3339), method, pathname, remoteAddress) methodColor := cBCyan
switch method {
case "POST":
methodColor = cBMagenta
case "GET":
methodColor = cBCyan
case "DELETE":
methodColor = cRed
}
fmt.Printf("%s %s%s%s%s %s%s%s %s(%s)%s\n",
ts(),
methodColor, cBold, method, cReset,
cWhite, pathname, cReset,
cDim, remoteAddress, cReset,
)
} }
func LogAccountAssigned(configDir string) { func LogAccountAssigned(configDir string) {
@ -69,7 +132,7 @@ func LogAccountAssigned(configDir string) {
return return
} }
name := filepath.Base(configDir) name := filepath.Base(configDir)
fmt.Printf("[%s] %s→ account%s %s%s%s\n", time.Now().UTC().Format(time.RFC3339), cBCyan, cReset, cBold, name, cReset) fmt.Printf("%s %s→%s account %s%s%s\n", ts(), cBCyan, cReset, cBold, name, cReset)
} }
func LogAccountStats(verbose bool, stats []pool.AccountStat) { func LogAccountStats(verbose bool, stats []pool.AccountStat) {
@ -80,17 +143,17 @@ func LogAccountStats(verbose bool, stats []pool.AccountStat) {
fmt.Printf("%s┌─ Account Stats %s┐%s\n", cGray, strings.Repeat("─", 44), cReset) fmt.Printf("%s┌─ Account Stats %s┐%s\n", cGray, strings.Repeat("─", 44), cReset)
for _, s := range stats { for _, s := range stats {
name := fmt.Sprintf("%-20s", filepath.Base(s.ConfigDir)) name := fmt.Sprintf("%-20s", filepath.Base(s.ConfigDir))
active := fmt.Sprintf("%sdim%sactive:0%s", cDim, "", cReset) active := fmt.Sprintf("%sactive:0%s", cDim, cReset)
if s.ActiveRequests > 0 { if s.ActiveRequests > 0 {
active = fmt.Sprintf("%sactive:%d%s", cBCyan, s.ActiveRequests, cReset) active = fmt.Sprintf("%sactive:%d%s", cBCyan, s.ActiveRequests, cReset)
} }
total := fmt.Sprintf("total:%s%d%s", cBold, s.TotalRequests, cReset) total := fmt.Sprintf("total:%s%d%s", cBold, s.TotalRequests, cReset)
ok := fmt.Sprintf("%sok:%d%s", cGreen, s.TotalSuccess, cReset) ok := fmt.Sprintf("%sok:%d%s", cGreen, s.TotalSuccess, cReset)
errStr := fmt.Sprintf("%sdim%serr:0%s", cDim, "", cReset) errStr := fmt.Sprintf("%serr:0%s", cDim, cReset)
if s.TotalErrors > 0 { if s.TotalErrors > 0 {
errStr = fmt.Sprintf("%serr:%d%s", cRed, s.TotalErrors, cReset) errStr = fmt.Sprintf("%serr:%d%s", cRed, s.TotalErrors, cReset)
} }
rl := fmt.Sprintf("%sdim%srl:0%s", cDim, "", cReset) rl := fmt.Sprintf("%srl:0%s", cDim, cReset)
if s.TotalRateLimits > 0 { if s.TotalRateLimits > 0 {
rl = fmt.Sprintf("%srl:%d%s", cYellow, s.TotalRateLimits, cReset) rl = fmt.Sprintf("%srl:%d%s", cYellow, s.TotalRateLimits, cReset)
} }
@ -114,7 +177,7 @@ func LogTrafficRequest(verbose bool, model string, messages []TrafficMessage, is
if !verbose { if !verbose {
return return
} }
modeTag := fmt.Sprintf("%sdim%ssync%s", cDim, "", cReset) modeTag := fmt.Sprintf("%ssync%s", cDim, cReset)
if isStream { if isStream {
modeTag = fmt.Sprintf("%s⚡ stream%s", cBCyan, cReset) modeTag = fmt.Sprintf("%s⚡ stream%s", cBCyan, cReset)
} }
@ -142,7 +205,7 @@ func LogTrafficResponse(verbose bool, model, text string, isStream bool) {
if !verbose { if !verbose {
return return
} }
modeTag := fmt.Sprintf("%sdim%ssync%s", cDim, "", cReset) modeTag := fmt.Sprintf("%ssync%s", cDim, cReset)
if isStream { if isStream {
modeTag = fmt.Sprintf("%s⚡ stream%s", cBGreen, cReset) modeTag = fmt.Sprintf("%s⚡ stream%s", cBGreen, cReset)
} }
@ -166,9 +229,14 @@ func AppendSessionLine(logPath, method, pathname, remoteAddress string, statusCo
} }
} }
func LogTruncation(originalLen, finalLen int) {
fmt.Printf("%s %s⚠ prompt truncated%s %s(%d → %d chars, tail preserved)%s\n",
ts(), cYellow, cReset, cDim, originalLen, finalLen, cReset)
}
func LogAgentError(logPath, method, pathname, remoteAddress string, exitCode int, stderr string) string { func LogAgentError(logPath, method, pathname, remoteAddress string, exitCode int, stderr string) string {
errMsg := fmt.Sprintf("Cursor CLI failed (exit %d): %s", exitCode, strings.TrimSpace(stderr)) errMsg := fmt.Sprintf("Cursor CLI failed (exit %d): %s", exitCode, strings.TrimSpace(stderr))
fmt.Fprintf(os.Stderr, "[%s] Agent error: %s\n", time.Now().UTC().Format(time.RFC3339), errMsg) fmt.Fprintf(os.Stderr, "%s %s✗ agent error%s %s%s%s\n", ts(), cRed, cReset, cDim, errMsg, cReset)
truncated := strings.TrimSpace(stderr) truncated := strings.TrimSpace(stderr)
if len(truncated) > 200 { if len(truncated) > 200 {
truncated = truncated[:200] truncated = truncated[:200]

View File

@ -7,6 +7,7 @@ import (
"cursor-api-proxy/internal/handlers" "cursor-api-proxy/internal/handlers"
"cursor-api-proxy/internal/pool" "cursor-api-proxy/internal/pool"
"cursor-api-proxy/internal/process" "cursor-api-proxy/internal/process"
"cursor-api-proxy/internal/logger"
"cursor-api-proxy/internal/router" "cursor-api-proxy/internal/router"
"fmt" "fmt"
"net/http" "net/http"
@ -98,34 +99,7 @@ func startSingleServer(opts ServerOptions) *http.Server {
} }
}() }()
fmt.Printf("cursor-api-proxy listening on %s://%s:%d\n", scheme, cfg.Host, cfg.Port) logger.LogServerStart(opts.Version, scheme, cfg.Host, cfg.Port, cfg)
fmt.Printf("- agent bin: %s\n", cfg.AgentBin)
fmt.Printf("- workspace: %s\n", cfg.Workspace)
fmt.Printf("- mode: %s\n", cfg.Mode)
fmt.Printf("- default model: %s\n", cfg.DefaultModel)
fmt.Printf("- force: %v\n", cfg.Force)
fmt.Printf("- approve mcps: %v\n", cfg.ApproveMcps)
fmt.Printf("- required api key: %v\n", cfg.RequiredKey != "")
fmt.Printf("- sessions log: %s\n", cfg.SessionsLogPath)
if cfg.ChatOnlyWorkspace {
fmt.Println("- chat-only workspace: yes (isolated temp dir)")
} else {
fmt.Println("- chat-only workspace: no")
}
if cfg.Verbose {
fmt.Println("- verbose traffic: yes (CURSOR_BRIDGE_VERBOSE=true)")
} else {
fmt.Println("- verbose traffic: no")
}
if cfg.MaxMode {
fmt.Println("- max mode: yes (CURSOR_BRIDGE_MAX_MODE=true)")
} else {
fmt.Println("- max mode: no")
}
fmt.Printf("- Windows cmdline budget: %d (prompt tail truncation when over limit; Windows only)\n", cfg.WinCmdlineMax)
if len(cfg.ConfigDirs) > 0 {
fmt.Printf("- account pool: enabled with %d configuration directories\n", len(cfg.ConfigDirs))
}
return srv return srv
} }
@ -136,8 +110,7 @@ func SetupGracefulShutdown(servers []*http.Server, timeoutMs int) {
go func() { go func() {
sig := <-sigCh sig := <-sigCh
fmt.Printf("\n[%s] %s received — shutting down gracefully…\n", logger.LogShutdown(sig.String())
time.Now().UTC().Format(time.RFC3339), sig)
process.KillAllChildProcesses() process.KillAllChildProcesses()