2026-05-08 10:12:31 +08:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-05-16 17:21:29 +08:00
|
|
|
|
"crypto/sha256"
|
2026-05-08 10:12:31 +08:00
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"os"
|
|
|
|
|
|
"path/filepath"
|
2026-05-16 17:21:29 +08:00
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
2026-05-08 10:12:31 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
func sessionPath() string {
|
2026-05-16 17:21:29 +08:00
|
|
|
|
return filepath.Join(ConfigDir(), "session", "session.json")
|
2026-05-08 10:12:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func ClearSession() {
|
|
|
|
|
|
os.Remove(sessionPath())
|
2026-05-16 17:21:29 +08:00
|
|
|
|
infoLog("会话已清空")
|
2026-05-08 10:12:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 17:21:29 +08:00
|
|
|
|
const maxSessionMessages = 40
|
|
|
|
|
|
|
2026-05-08 10:12:31 +08:00
|
|
|
|
func LoadSession() []Message {
|
|
|
|
|
|
data, err := os.ReadFile(sessionPath())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var messages []Message
|
|
|
|
|
|
if err := json.Unmarshal(data, &messages); err != nil {
|
2026-05-16 17:21:29 +08:00
|
|
|
|
warnLog("解析 session.json 失败", "err", err)
|
2026-05-08 10:12:31 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2026-05-16 17:21:29 +08:00
|
|
|
|
if len(messages) > maxSessionMessages {
|
|
|
|
|
|
messages = messages[len(messages)-maxSessionMessages:]
|
|
|
|
|
|
}
|
2026-05-08 10:12:31 +08:00
|
|
|
|
return messages
|
2026-05-16 17:21:29 +08:00
|
|
|
|
|
2026-05-08 10:12:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func AppendToSession(msg Message) {
|
|
|
|
|
|
|
|
|
|
|
|
messages := LoadSession()
|
|
|
|
|
|
messages = append(messages, msg)
|
|
|
|
|
|
|
|
|
|
|
|
data, err := json.MarshalIndent(messages, "", " ")
|
|
|
|
|
|
if err != nil {
|
2026-05-16 17:21:29 +08:00
|
|
|
|
warnLog("序列化 session 失败", "err", err)
|
2026-05-08 10:12:31 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
os.WriteFile(sessionPath(), data, 0644)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-16 17:21:29 +08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Cache 辅助
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
func cacheDir() string {
|
|
|
|
|
|
return filepath.Join(ConfigDir(), "cache")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func cacheFilePath(agentName string) string {
|
|
|
|
|
|
return filepath.Join(cacheDir(), agentName+".json")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type cacheEntry struct {
|
|
|
|
|
|
CreatedAt time.Time `json:"created_at"`
|
|
|
|
|
|
TTL int `json:"ttl"`
|
|
|
|
|
|
Data interface{} `json:"data"`
|
|
|
|
|
|
Raw map[string]interface{} `json:"raw"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildCacheKey(keys []string, args map[string]interface{}) string {
|
|
|
|
|
|
parts := make([]string, 0)
|
|
|
|
|
|
for _, k := range keys {
|
|
|
|
|
|
if v, ok := args[k]; ok {
|
|
|
|
|
|
parts = append(parts, fmt.Sprintf("%s=%v", k, v))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(parts) == 0 {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
h := sha256.Sum256([]byte(strings.Join(parts, "&")))
|
|
|
|
|
|
return fmt.Sprintf("%x", h[:6])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func readCache(agentName, key string) *cacheEntry {
|
|
|
|
|
|
if key == "" {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
data, err := os.ReadFile(cacheFilePath(agentName))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
var store map[string]cacheEntry
|
|
|
|
|
|
if err := json.Unmarshal(data, &store); err != nil {
|
|
|
|
|
|
warnLog("解析缓存失败", "agent", agentName, "err", err)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
entry, ok := store[key]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
if time.Since(entry.CreatedAt) > time.Duration(entry.TTL)*time.Second {
|
|
|
|
|
|
delete(store, key)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return &entry
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func writeCache(agentName, key string, data interface{}, raw map[string]interface{}, ttl int) {
|
|
|
|
|
|
if key == "" {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
store := make(map[string]cacheEntry)
|
|
|
|
|
|
existing, err := os.ReadFile(cacheFilePath(agentName))
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
if err := json.Unmarshal(existing, &store); err != nil {
|
|
|
|
|
|
warnLog("读取旧缓存解析失败", "agent", agentName, "err", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
store[key] = cacheEntry{
|
|
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
|
|
TTL: ttl,
|
|
|
|
|
|
Data: data,
|
|
|
|
|
|
Raw: raw,
|
|
|
|
|
|
}
|
|
|
|
|
|
dir := cacheDir()
|
|
|
|
|
|
os.MkdirAll(dir, 0755)
|
|
|
|
|
|
out, err := json.MarshalIndent(store, "", " ")
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
warnLog("序列化缓存失败", "agent", agentName, "err", err)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
os.WriteFile(cacheFilePath(agentName), out, 0644)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 子 Agent 返回解析
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
func parseSubResult(raw string) (text string, resultData interface{}) {
|
|
|
|
|
|
const resultMarker = "---RESULT---\n"
|
|
|
|
|
|
const textMarker = "\n---TEXT---"
|
|
|
|
|
|
|
|
|
|
|
|
if !strings.Contains(raw, resultMarker) {
|
|
|
|
|
|
return raw, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
parts := strings.SplitN(raw, resultMarker, 2)
|
|
|
|
|
|
remaining := parts[1]
|
|
|
|
|
|
|
|
|
|
|
|
resultEnd := strings.Index(remaining, textMarker)
|
|
|
|
|
|
if resultEnd == -1 {
|
|
|
|
|
|
return raw, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
jsonStr := strings.TrimSpace(remaining[:resultEnd])
|
|
|
|
|
|
json.Unmarshal([]byte(jsonStr), &resultData)
|
|
|
|
|
|
text = strings.TrimSpace(remaining[resultEnd+len(textMarker):])
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// RunSubAgent — 隔离的子 Agent 执行(不读写 session)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
func RunSubAgent(def *AgentDef, userInput string) (string, error) {
|
|
|
|
|
|
infoLog("子 Agent 开始", "agent", def.Name)
|
|
|
|
|
|
messages := []Message{
|
|
|
|
|
|
{Role: RoleSystem, Content: def.SystemPrompt},
|
|
|
|
|
|
{Role: RoleUser, Content: userInput},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
toolDefs := GetToolDefs(def.Tools)
|
|
|
|
|
|
maxToolCalls := 2
|
|
|
|
|
|
toolCallCount := 0
|
|
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
resp, err := CallLLM(messages, toolDefs)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return "", err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
choice := resp.Choices[0]
|
|
|
|
|
|
|
|
|
|
|
|
if len(choice.Message.ToolCalls) > 0 {
|
|
|
|
|
|
toolCallCount++
|
|
|
|
|
|
if toolCallCount > maxToolCalls {
|
|
|
|
|
|
warnLog("子 Agent 执行轮次超限", "agent", def.Name, "rounds", toolCallCount)
|
|
|
|
|
|
return "---TEXT---\n(子 Agent 执行轮次超限,已终止)", nil
|
|
|
|
|
|
}
|
|
|
|
|
|
assistantMsg := Message{
|
|
|
|
|
|
Role: RoleAssistant,
|
|
|
|
|
|
ToolCalls: choice.Message.ToolCalls,
|
|
|
|
|
|
}
|
|
|
|
|
|
messages = append(messages, assistantMsg)
|
|
|
|
|
|
|
|
|
|
|
|
for _, tc := range choice.Message.ToolCalls {
|
|
|
|
|
|
result, err := ExecuteTool(tc)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
result = fmt.Sprintf("工具执行错误: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
toolMsg := Message{
|
|
|
|
|
|
Role: RoleTool,
|
|
|
|
|
|
Content: result,
|
|
|
|
|
|
ToolCallID: tc.ID,
|
|
|
|
|
|
}
|
|
|
|
|
|
messages = append(messages, toolMsg)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
content := ""
|
|
|
|
|
|
if choice.Message.Content != nil {
|
|
|
|
|
|
content = *choice.Message.Content
|
|
|
|
|
|
}
|
|
|
|
|
|
return content, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-08 10:12:31 +08:00
|
|
|
|
func RunAgent(def *AgentDef, userInput string) error {
|
|
|
|
|
|
messages := LoadSession()
|
|
|
|
|
|
|
|
|
|
|
|
fullMessages := []Message{
|
|
|
|
|
|
{Role: RoleSystem, Content: def.SystemPrompt},
|
|
|
|
|
|
}
|
|
|
|
|
|
fullMessages = append(fullMessages, messages...)
|
|
|
|
|
|
fullMessages = append(fullMessages, Message{Role: RoleUser, Content: userInput})
|
|
|
|
|
|
|
|
|
|
|
|
AppendToSession(Message{Role: RoleUser, Content: userInput})
|
|
|
|
|
|
|
|
|
|
|
|
toolDefs := GetToolDefs(def.Tools)
|
|
|
|
|
|
|
|
|
|
|
|
for {
|
2026-05-16 17:21:29 +08:00
|
|
|
|
fmt.Println()
|
|
|
|
|
|
resp, err := CallLLMStream(fullMessages, toolDefs)
|
2026-05-08 10:12:31 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
choice := resp.Choices[0]
|
|
|
|
|
|
|
|
|
|
|
|
if len(choice.Message.ToolCalls) > 0 {
|
|
|
|
|
|
assistantMsg := Message{
|
|
|
|
|
|
Role: RoleAssistant,
|
|
|
|
|
|
ToolCalls: choice.Message.ToolCalls,
|
|
|
|
|
|
}
|
|
|
|
|
|
fullMessages = append(fullMessages, assistantMsg)
|
|
|
|
|
|
AppendToSession(assistantMsg)
|
|
|
|
|
|
|
|
|
|
|
|
for _, tc := range choice.Message.ToolCalls {
|
|
|
|
|
|
result, err := ExecuteTool(tc)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
result = fmt.Sprintf("工具执行错误: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
toolMsg := Message{
|
|
|
|
|
|
Role: RoleTool,
|
|
|
|
|
|
Content: result,
|
|
|
|
|
|
ToolCallID: tc.ID,
|
|
|
|
|
|
}
|
|
|
|
|
|
fullMessages = append(fullMessages, toolMsg)
|
|
|
|
|
|
AppendToSession(toolMsg)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
content := ""
|
|
|
|
|
|
if choice.Message.Content != nil {
|
|
|
|
|
|
content = *choice.Message.Content
|
|
|
|
|
|
}
|
2026-05-16 17:21:29 +08:00
|
|
|
|
|
2026-05-08 10:12:31 +08:00
|
|
|
|
assistantMsg := Message{
|
|
|
|
|
|
Role: RoleAssistant,
|
|
|
|
|
|
Content: content,
|
|
|
|
|
|
}
|
|
|
|
|
|
fullMessages = append(fullMessages, assistantMsg)
|
|
|
|
|
|
AppendToSession(assistantMsg)
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|