feat(runtime): add process watch patterns, unified backup/import, pluggable context engine, token usage, and codex device login

This commit is contained in:
lpf
2026-04-14 14:53:18 +08:00
parent fac235db80
commit 79e0a48b74
18 changed files with 1257 additions and 64 deletions

View File

@@ -3,6 +3,8 @@ package tools
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
)
@@ -11,15 +13,19 @@ type ProcessTool struct{ m *ProcessManager }
func NewProcessTool(m *ProcessManager) *ProcessTool { return &ProcessTool{m: m} }
func (t *ProcessTool) Name() string { return "process" }
func (t *ProcessTool) Description() string {
return "Manage background exec sessions: list, poll, log, kill"
return "Manage background exec sessions: list, poll, log, kill, watch_patterns"
}
func (t *ProcessTool) Parameters() map[string]interface{} {
return map[string]interface{}{"type": "object", "properties": map[string]interface{}{
"action": map[string]interface{}{"type": "string", "description": "list|poll|log|kill"},
"session_id": map[string]interface{}{"type": "string"},
"offset": map[string]interface{}{"type": "integer"},
"limit": map[string]interface{}{"type": "integer"},
"timeout_ms": map[string]interface{}{"type": "integer"},
"action": map[string]interface{}{"type": "string", "description": "list|poll|log|kill|watch_patterns"},
"session_id": map[string]interface{}{"type": "string"},
"offset": map[string]interface{}{"type": "integer"},
"limit": map[string]interface{}{"type": "integer"},
"timeout_ms": map[string]interface{}{"type": "integer"},
"interval_ms": map[string]interface{}{"type": "integer"},
"patterns": map[string]interface{}{"type": "array", "items": map[string]interface{}{"type": "string"}},
"case_sensitive": map[string]interface{}{"type": "boolean"},
"alert_on_exit": map[string]interface{}{"type": "boolean"},
}, "required": []string{"action"}}
}
@@ -76,7 +82,156 @@ func (t *ProcessTool) Execute(ctx context.Context, args map[string]interface{})
}
b, _ := json.Marshal(resp)
return string(b), nil
case "watch_patterns":
patterns := MapStringListArg(args, "patterns")
if len(patterns) == 0 {
return "", fmt.Errorf("patterns is required")
}
timeout := MapIntArg(args, "timeout_ms", 30000)
if timeout < 1 {
timeout = 30000
}
interval := MapIntArg(args, "interval_ms", 250)
if interval < 50 {
interval = 50
}
if interval > timeout {
interval = timeout
}
off := MapIntArg(args, "offset", 0)
if off < 0 {
off = 0
}
caseSensitive := false
if v, ok := MapBoolArg(args, "case_sensitive"); ok {
caseSensitive = v
}
alertOnExit := true
if v, ok := MapBoolArg(args, "alert_on_exit"); ok {
alertOnExit = v
}
return t.watchPatterns(ctx, sid, patterns, off, timeout, interval, caseSensitive, alertOnExit)
default:
return "", nil
}
}
func (t *ProcessTool) watchPatterns(ctx context.Context, sid string, patterns []string, offset, timeoutMs, intervalMs int, caseSensitive, alertOnExit bool) (string, error) {
s, ok := t.m.Get(sid)
if !ok {
return "", fmt.Errorf("session not found: %s", sid)
}
type watchPattern struct {
original string
lookup string
}
normalized := make([]watchPattern, 0, len(patterns))
for _, p := range patterns {
p = strings.TrimSpace(p)
if p == "" {
continue
}
lookup := p
if !caseSensitive {
lookup = strings.ToLower(p)
}
normalized = append(normalized, watchPattern{original: p, lookup: lookup})
}
if len(normalized) == 0 {
return "", fmt.Errorf("patterns is required")
}
started := time.Now()
deadline := started.Add(time.Duration(timeoutMs) * time.Millisecond)
scanBuf := ""
nextOffset := offset
for {
chunk, err := t.m.Log(sid, nextOffset, 16*1024)
if err != nil {
return "", err
}
if chunk != "" {
nextOffset += len(chunk)
scanBuf += chunk
if len(scanBuf) > 24*1024 {
scanBuf = scanBuf[len(scanBuf)-24*1024:]
}
haystack := scanBuf
if !caseSensitive {
haystack = strings.ToLower(haystack)
}
for _, pattern := range normalized {
if strings.Contains(haystack, pattern.lookup) {
resp := map[string]interface{}{
"id": s.ID,
"matched": true,
"pattern": pattern.original,
"running": processSessionRunning(s),
"next_offset": nextOffset,
"elapsed_ms": time.Since(started).Milliseconds(),
}
b, _ := json.Marshal(resp)
return string(b), nil
}
}
}
running, exitCode := processSessionState(s)
if !running {
resp := map[string]interface{}{
"id": s.ID,
"matched": false,
"running": false,
"exit_code": exitCode,
"next_offset": nextOffset,
"elapsed_ms": time.Since(started).Milliseconds(),
}
if alertOnExit {
resp["event"] = "process_exited"
}
b, _ := json.Marshal(resp)
return string(b), nil
}
now := time.Now()
if now.After(deadline) {
resp := map[string]interface{}{
"id": s.ID,
"matched": false,
"running": true,
"timed_out": true,
"next_offset": nextOffset,
"elapsed_ms": now.Sub(started).Milliseconds(),
}
b, _ := json.Marshal(resp)
return string(b), nil
}
wait := time.Duration(intervalMs) * time.Millisecond
if remaining := time.Until(deadline); wait > remaining {
wait = remaining
}
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(wait):
}
}
}
func processSessionRunning(s *processSession) bool {
if s == nil {
return false
}
s.mu.RLock()
defer s.mu.RUnlock()
return s.ExitCode == nil
}
func processSessionState(s *processSession) (running bool, exitCode interface{}) {
if s == nil {
return false, nil
}
s.mu.RLock()
defer s.mu.RUnlock()
if s.ExitCode == nil {
return true, nil
}
return false, *s.ExitCode
}