mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-13 13:17:28 +08:00
feat(provider): switch to clawgo provider and improve office state handling
This commit is contained in:
@@ -55,10 +55,10 @@ curl -fsSL https://raw.githubusercontent.com/YspCoder/clawgo/main/install.sh | b
|
||||
clawgo onboard
|
||||
```
|
||||
|
||||
### 3) 配置模型
|
||||
### 3) 配置 Provider
|
||||
|
||||
```bash
|
||||
clawgo login
|
||||
clawgo provider
|
||||
```
|
||||
|
||||
### 4) 看状态
|
||||
@@ -118,7 +118,7 @@ http://<host>:<port>/webui?token=<gateway.token>
|
||||
|
||||
```text
|
||||
clawgo onboard
|
||||
clawgo login
|
||||
clawgo provider
|
||||
clawgo status
|
||||
clawgo agent [-m "..."]
|
||||
clawgo gateway [run|start|stop|restart|status]
|
||||
|
||||
@@ -55,10 +55,10 @@ curl -fsSL https://raw.githubusercontent.com/YspCoder/clawgo/main/install.sh | b
|
||||
clawgo onboard
|
||||
```
|
||||
|
||||
### 3) Configure model/proxy
|
||||
### 3) Configure provider
|
||||
|
||||
```bash
|
||||
clawgo login
|
||||
clawgo provider
|
||||
```
|
||||
|
||||
### 4) Check status
|
||||
@@ -118,7 +118,7 @@ Main pages:
|
||||
|
||||
```text
|
||||
clawgo onboard
|
||||
clawgo login
|
||||
clawgo provider
|
||||
clawgo status
|
||||
clawgo agent [-m "..."]
|
||||
clawgo gateway [run|start|stop|restart|status]
|
||||
|
||||
@@ -92,6 +92,7 @@ func printHelp() {
|
||||
fmt.Println(" agent Interact with the agent directly")
|
||||
fmt.Println(" gateway Register/manage gateway service")
|
||||
fmt.Println(" status Show clawgo status")
|
||||
fmt.Println(" provider Configure provider credentials")
|
||||
fmt.Println(" config Get/set config values")
|
||||
fmt.Println(" cron Manage scheduled tasks")
|
||||
fmt.Println(" channel Test and manage messaging channels")
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"clawgo/pkg/config"
|
||||
@@ -26,8 +28,6 @@ func configCmd() {
|
||||
configCheckCmd()
|
||||
case "reload":
|
||||
configReloadCmd()
|
||||
case "login":
|
||||
configLoginCmd()
|
||||
default:
|
||||
fmt.Printf("Unknown config command: %s\n", os.Args[2])
|
||||
configHelp()
|
||||
@@ -171,35 +171,216 @@ func configCheckCmd() {
|
||||
}
|
||||
}
|
||||
|
||||
func configLoginCmd() {
|
||||
func providerCmd() {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
fmt.Printf("Error loading config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Configuring CLIProxyAPI...")
|
||||
fmt.Printf("Current Base: %s\n", cfg.Providers.Proxy.APIBase)
|
||||
|
||||
fmt.Print("Enter CLIProxyAPI Base URL (e.g. http://localhost:8080/v1): ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
line, _ := reader.ReadString('\n')
|
||||
apiBase := strings.TrimSpace(line)
|
||||
if apiBase != "" {
|
||||
cfg.Providers.Proxy.APIBase = apiBase
|
||||
defaultProxy := strings.TrimSpace(cfg.Agents.Defaults.Proxy)
|
||||
if defaultProxy == "" {
|
||||
defaultProxy = "proxy"
|
||||
}
|
||||
available := providerNames(cfg)
|
||||
fmt.Printf("Current default provider: %s\n", defaultProxy)
|
||||
fmt.Printf("Available providers: %s\n", strings.Join(available, ", "))
|
||||
|
||||
argName := ""
|
||||
if len(os.Args) >= 3 {
|
||||
argName = strings.TrimSpace(os.Args[2])
|
||||
}
|
||||
if argName == "" || strings.HasPrefix(argName, "-") {
|
||||
argName = defaultProxy
|
||||
}
|
||||
providerName := promptLine(reader, "Provider name to configure", argName)
|
||||
if providerName == "" {
|
||||
providerName = defaultProxy
|
||||
}
|
||||
|
||||
fmt.Print("Enter API Key (optional): ")
|
||||
fmt.Scanln(&cfg.Providers.Proxy.APIKey)
|
||||
pc := providerConfigByName(cfg, providerName)
|
||||
if pc.TimeoutSec <= 0 {
|
||||
pc.TimeoutSec = 90
|
||||
}
|
||||
if strings.TrimSpace(pc.Auth) == "" {
|
||||
pc.Auth = "bearer"
|
||||
}
|
||||
if len(pc.Models) == 0 {
|
||||
pc.Models = append([]string{}, cfg.Providers.Proxy.Models...)
|
||||
}
|
||||
if len(pc.Models) == 0 {
|
||||
pc.Models = []string{"glm-4.7"}
|
||||
}
|
||||
|
||||
pc.APIBase = promptLine(reader, "api_base", pc.APIBase)
|
||||
apiKey := promptLine(reader, "api_key (leave empty to keep current)", "")
|
||||
if apiKey != "" {
|
||||
pc.APIKey = apiKey
|
||||
}
|
||||
modelsRaw := promptLine(reader, "models (comma-separated)", strings.Join(pc.Models, ","))
|
||||
if models := parseCSV(modelsRaw); len(models) > 0 {
|
||||
pc.Models = models
|
||||
}
|
||||
pc.Auth = promptLine(reader, "auth (bearer/oauth/none)", pc.Auth)
|
||||
timeoutRaw := promptLine(reader, "timeout_sec", fmt.Sprintf("%d", pc.TimeoutSec))
|
||||
pc.TimeoutSec = parseIntOrDefault(timeoutRaw, pc.TimeoutSec)
|
||||
pc.SupportsResponsesCompact = promptBool(reader, "supports_responses_compact", pc.SupportsResponsesCompact)
|
||||
|
||||
setProviderConfigByName(cfg, providerName, pc)
|
||||
|
||||
makeDefault := promptBool(reader, fmt.Sprintf("Set %s as agents.defaults.proxy", providerName), providerName == defaultProxy)
|
||||
if makeDefault {
|
||||
cfg.Agents.Defaults.Proxy = providerName
|
||||
}
|
||||
|
||||
currentFallbacks := strings.Join(cfg.Agents.Defaults.ProxyFallbacks, ",")
|
||||
fallbackRaw := promptLine(reader, "agents.defaults.proxy_fallbacks (comma-separated names)", currentFallbacks)
|
||||
fallbacks := parseCSV(fallbackRaw)
|
||||
valid := map[string]struct{}{}
|
||||
for _, name := range providerNames(cfg) {
|
||||
valid[name] = struct{}{}
|
||||
}
|
||||
filteredFallbacks := make([]string, 0, len(fallbacks))
|
||||
seen := map[string]struct{}{}
|
||||
defaultName := strings.TrimSpace(cfg.Agents.Defaults.Proxy)
|
||||
for _, fb := range fallbacks {
|
||||
if fb == "" || fb == defaultName {
|
||||
continue
|
||||
}
|
||||
if _, ok := valid[fb]; !ok {
|
||||
fmt.Printf("Skip unknown fallback provider: %s\n", fb)
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[fb]; ok {
|
||||
continue
|
||||
}
|
||||
seen[fb] = struct{}{}
|
||||
filteredFallbacks = append(filteredFallbacks, fb)
|
||||
}
|
||||
cfg.Agents.Defaults.ProxyFallbacks = filteredFallbacks
|
||||
|
||||
if err := config.SaveConfig(getConfigPath(), cfg); err != nil {
|
||||
fmt.Printf("Error saving config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("✓ CLIProxyAPI configuration saved.")
|
||||
fmt.Println("✓ Provider configuration saved.")
|
||||
running, err := triggerGatewayReload()
|
||||
if err != nil {
|
||||
if running {
|
||||
fmt.Printf("Hot reload not applied: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("Gateway not running, reload skipped: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("✓ Gateway hot reload signal sent")
|
||||
}
|
||||
|
||||
func providerNames(cfg *config.Config) []string {
|
||||
names := []string{"proxy"}
|
||||
for k := range cfg.Providers.Proxies {
|
||||
k = strings.TrimSpace(k)
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
names = append(names, k)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func providerConfigByName(cfg *config.Config, name string) config.ProviderConfig {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || name == "proxy" {
|
||||
return cfg.Providers.Proxy
|
||||
}
|
||||
if cfg.Providers.Proxies != nil {
|
||||
if pc, ok := cfg.Providers.Proxies[name]; ok {
|
||||
return pc
|
||||
}
|
||||
}
|
||||
return config.ProviderConfig{
|
||||
APIBase: cfg.Providers.Proxy.APIBase,
|
||||
TimeoutSec: cfg.Providers.Proxy.TimeoutSec,
|
||||
Auth: cfg.Providers.Proxy.Auth,
|
||||
Models: append([]string{}, cfg.Providers.Proxy.Models...),
|
||||
}
|
||||
}
|
||||
|
||||
func setProviderConfigByName(cfg *config.Config, name string, pc config.ProviderConfig) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" || name == "proxy" {
|
||||
cfg.Providers.Proxy = pc
|
||||
return
|
||||
}
|
||||
if cfg.Providers.Proxies == nil {
|
||||
cfg.Providers.Proxies = map[string]config.ProviderConfig{}
|
||||
}
|
||||
cfg.Providers.Proxies[name] = pc
|
||||
}
|
||||
|
||||
func promptLine(reader *bufio.Reader, label, defaultValue string) string {
|
||||
label = strings.TrimSpace(label)
|
||||
if defaultValue != "" {
|
||||
fmt.Printf("%s [%s]: ", label, defaultValue)
|
||||
} else {
|
||||
fmt.Printf("%s: ", label)
|
||||
}
|
||||
line, _ := reader.ReadString('\n')
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
func promptBool(reader *bufio.Reader, label string, defaultValue bool) bool {
|
||||
def := "N"
|
||||
if defaultValue {
|
||||
def = "Y"
|
||||
}
|
||||
raw := promptLine(reader, label+" (y/n)", def)
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "y", "yes", "true", "1":
|
||||
return true
|
||||
case "n", "no", "false", "0":
|
||||
return false
|
||||
default:
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
func parseCSV(raw string) []string {
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
seen := map[string]struct{}{}
|
||||
for _, p := range parts {
|
||||
v := strings.TrimSpace(p)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[v]; ok {
|
||||
continue
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseIntOrDefault(raw string, def int) int {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return def
|
||||
}
|
||||
v, err := strconv.Atoi(raw)
|
||||
if err != nil || v <= 0 {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func loadConfigAsMap(path string) (map[string]interface{}, error) {
|
||||
return configops.LoadConfigAsMap(path)
|
||||
|
||||
@@ -59,6 +59,8 @@ func main() {
|
||||
gatewayCmd()
|
||||
case "status":
|
||||
statusCmd()
|
||||
case "provider":
|
||||
providerCmd()
|
||||
case "config":
|
||||
configCmd()
|
||||
case "cron":
|
||||
|
||||
@@ -2516,18 +2516,51 @@ func (s *Server) handleWebUIOfficeState(w http.ResponseWriter, r *http.Request)
|
||||
return st
|
||||
}
|
||||
}
|
||||
officeStateForStatus := func(status string, ts time.Time) string {
|
||||
st := normalizeTaskStatus(status)
|
||||
switch st {
|
||||
case "running":
|
||||
return "working"
|
||||
case "error", "blocked":
|
||||
return "error"
|
||||
case "suppressed":
|
||||
return "syncing"
|
||||
case "success":
|
||||
// Briefly keep success in working pose, then fall back to idle.
|
||||
if !ts.IsZero() && now.Sub(ts) <= 90*time.Second {
|
||||
return "working"
|
||||
}
|
||||
return "idle"
|
||||
default:
|
||||
return "idle"
|
||||
}
|
||||
}
|
||||
officeZoneForState := func(state string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(state)) {
|
||||
case "working":
|
||||
return "work"
|
||||
case "syncing":
|
||||
return "server"
|
||||
case "error":
|
||||
return "bug"
|
||||
default:
|
||||
return "breakroom"
|
||||
}
|
||||
}
|
||||
isFreshTaskState := func(status string, ts time.Time) bool {
|
||||
if ts.IsZero() {
|
||||
return false
|
||||
}
|
||||
window := 30 * time.Minute
|
||||
window := 20 * time.Minute
|
||||
switch status {
|
||||
case "running", "waiting":
|
||||
window = 2 * time.Hour
|
||||
case "blocked", "error":
|
||||
window = 6 * time.Hour
|
||||
case "success", "suppressed":
|
||||
case "running":
|
||||
window = 3 * time.Hour
|
||||
case "waiting":
|
||||
window = 30 * time.Minute
|
||||
case "blocked", "error":
|
||||
window = 2 * time.Hour
|
||||
case "success", "suppressed":
|
||||
window = 12 * time.Minute
|
||||
}
|
||||
return !ts.Before(now.Add(-window))
|
||||
}
|
||||
@@ -2656,59 +2689,9 @@ func (s *Server) handleWebUIOfficeState(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
mainState := "idle"
|
||||
mainZone := "breakroom"
|
||||
switch {
|
||||
case stats["running"] > 0:
|
||||
mainState = "executing"
|
||||
mainZone = "work"
|
||||
case stats["error"] > 0 || stats["blocked"] > 0:
|
||||
mainState = "error"
|
||||
mainZone = "bug"
|
||||
case stats["suppressed"] > 0:
|
||||
mainState = "syncing"
|
||||
mainZone = "server"
|
||||
case stats["waiting"] > 0:
|
||||
mainState = "idle"
|
||||
mainZone = "breakroom"
|
||||
case stats["success"] > 0:
|
||||
mainState = "writing"
|
||||
mainZone = "work"
|
||||
default:
|
||||
mainState = "idle"
|
||||
mainZone = "breakroom"
|
||||
}
|
||||
|
||||
mainTaskID := ""
|
||||
mainDetail := "No active task"
|
||||
isMainStatus := func(st string) bool {
|
||||
st = strings.ToLower(strings.TrimSpace(st))
|
||||
switch mainState {
|
||||
case "executing":
|
||||
return st == "running"
|
||||
case "error":
|
||||
return st == "error" || st == "blocked"
|
||||
case "syncing":
|
||||
return st == "suppressed"
|
||||
case "writing":
|
||||
return st == "success"
|
||||
default:
|
||||
return st == "waiting" || st == "idle"
|
||||
}
|
||||
}
|
||||
for _, row := range items {
|
||||
st := normalizeTaskStatus(fmt.Sprintf("%v", row["status"]))
|
||||
if isMainStatus(st) {
|
||||
mainTaskID = strings.TrimSpace(fmt.Sprintf("%v", row["task_id"]))
|
||||
mainDetail = strings.TrimSpace(fmt.Sprintf("%v", row["input_preview"]))
|
||||
if mainDetail == "" {
|
||||
mainDetail = strings.TrimSpace(fmt.Sprintf("%v", row["log"]))
|
||||
}
|
||||
if mainDetail == "" {
|
||||
mainDetail = "Task " + mainTaskID
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if mainTaskID == "" && len(items) > 0 {
|
||||
if len(items) > 0 {
|
||||
mainTaskID = strings.TrimSpace(fmt.Sprintf("%v", items[0]["task_id"]))
|
||||
mainDetail = strings.TrimSpace(fmt.Sprintf("%v", items[0]["input_preview"]))
|
||||
if mainDetail == "" {
|
||||
@@ -2717,6 +2700,10 @@ func (s *Server) handleWebUIOfficeState(w http.ResponseWriter, r *http.Request)
|
||||
if mainDetail == "" {
|
||||
mainDetail = "Task " + mainTaskID
|
||||
}
|
||||
st := normalizeTaskStatus(fmt.Sprintf("%v", items[0]["status"]))
|
||||
ts := parseTime(fmt.Sprintf("%v", items[0]["time"]))
|
||||
mainState = officeStateForStatus(st, ts)
|
||||
mainZone = officeZoneForState(mainState)
|
||||
}
|
||||
|
||||
nodeState := func(n nodes.NodeInfo) string {
|
||||
@@ -2727,16 +2714,23 @@ func (s *Server) handleWebUIOfficeState(w http.ResponseWriter, r *http.Request)
|
||||
if !n.LastSeenAt.IsZero() && now.Sub(n.LastSeenAt) > 20*time.Second {
|
||||
return "syncing"
|
||||
}
|
||||
return "online"
|
||||
if n.Capabilities.Model || n.Capabilities.Run {
|
||||
return "working"
|
||||
}
|
||||
return "idle"
|
||||
}
|
||||
nodeZone := func(n nodes.NodeInfo) string {
|
||||
if !n.Online {
|
||||
st := nodeState(n)
|
||||
if st == "offline" {
|
||||
return "bug"
|
||||
}
|
||||
if n.Capabilities.Model || n.Capabilities.Run {
|
||||
if st == "syncing" {
|
||||
return "server"
|
||||
}
|
||||
if st == "working" && (n.Capabilities.Model || n.Capabilities.Run) {
|
||||
return "work"
|
||||
}
|
||||
if n.Capabilities.Invoke || n.Capabilities.Camera || n.Capabilities.Screen || n.Capabilities.Canvas || n.Capabilities.Location {
|
||||
if st == "working" {
|
||||
return "server"
|
||||
}
|
||||
return "breakroom"
|
||||
|
||||
@@ -448,6 +448,7 @@ const OfficeScene: React.FC<OfficeSceneProps> = ({ main, nodes }) => {
|
||||
const [tick, setTick] = useState(0);
|
||||
const [showDebug, setShowDebug] = useState(false);
|
||||
const [manualState, setManualState] = useState<MainVisualState | null>(null);
|
||||
const [manualStateUntil, setManualStateUntil] = useState<number>(0);
|
||||
const [panEnabled, setPanEnabled] = useState<boolean>(() => true);
|
||||
const [camera, setCamera] = useState<CameraState>({ x: 0, y: 0, zoom: 1 });
|
||||
const [pointerWorld, setPointerWorld] = useState<Point | null>(null);
|
||||
@@ -461,10 +462,22 @@ const OfficeScene: React.FC<OfficeSceneProps> = ({ main, nodes }) => {
|
||||
useEffect(() => {
|
||||
if (manualState && prevLiveStateRef.current !== liveMainState) {
|
||||
setManualState(null);
|
||||
setManualStateUntil(0);
|
||||
}
|
||||
prevLiveStateRef.current = liveMainState;
|
||||
}, [liveMainState, manualState]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!manualState || manualStateUntil <= 0) return;
|
||||
const timer = window.setInterval(() => {
|
||||
if (Date.now() >= manualStateUntil) {
|
||||
setManualState(null);
|
||||
setManualStateUntil(0);
|
||||
}
|
||||
}, 400);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [manualState, manualStateUntil]);
|
||||
|
||||
const placedNodes = useMemo(() => {
|
||||
const counters: Record<OfficeZone, number> = { breakroom: 0, work: 0, server: 0, bug: 0 };
|
||||
return nodes.slice(0, 24).map((n, i) => {
|
||||
@@ -650,6 +663,11 @@ const OfficeScene: React.FC<OfficeSceneProps> = ({ main, nodes }) => {
|
||||
|
||||
const setVisualState = useCallback((state: MainVisualState | null) => {
|
||||
setManualState(state);
|
||||
if (state === null) {
|
||||
setManualStateUntil(0);
|
||||
return;
|
||||
}
|
||||
setManualStateUntil(Date.now() + 15_000);
|
||||
}, []);
|
||||
|
||||
const cycleServerMode = useCallback(() => {
|
||||
@@ -660,6 +678,7 @@ const OfficeScene: React.FC<OfficeSceneProps> = ({ main, nodes }) => {
|
||||
const serverOn = serverMode === 'on' || (serverMode === 'auto' && effectiveMainState !== 'idle');
|
||||
const serverFrame = serverOn ? frameAtTick(DECOR_SPRITES.serverroom, tick, 700) : 0;
|
||||
const mainFrame = frameAtTick(MAIN_SPRITES[effectiveMainState], tick);
|
||||
const manualLeftSec = manualState ? Math.max(0, Math.ceil((manualStateUntil - Date.now()) / 1000)) : 0;
|
||||
|
||||
const furnitureScale = camera.zoom;
|
||||
|
||||
@@ -894,7 +913,7 @@ const OfficeScene: React.FC<OfficeSceneProps> = ({ main, nodes }) => {
|
||||
</div>
|
||||
|
||||
<div className="absolute right-2 top-2 z-[300] rounded-lg border border-zinc-700 bg-zinc-950/85 px-2 py-1 text-[11px] text-zinc-300">
|
||||
state={effectiveMainState} live={liveMainState} mode={manualState ? 'manual' : 'follow'} server={serverMode} coffee={coffeePaused ? 'paused' : 'on'}
|
||||
state={effectiveMainState} live={liveMainState} mode={manualState ? `manual(${manualLeftSec}s)` : 'follow'} server={serverMode} coffee={coffeePaused ? 'paused' : 'on'}
|
||||
</div>
|
||||
|
||||
{showDebug ? (
|
||||
|
||||
Reference in New Issue
Block a user