fix: 修复 AI 回复无法送达发送者 + 重复消息 + action角色泄露 + OS环境支持
广播逻辑重构: - AI 回复 (stream_start/response/stream_segments/multi_message/stream_end) 改用 broadcastToUser 发送给所有客户端 - 用户消息回显保持 broadcastToUserExcept 排除发送者 消息去重与角色修复: - CacheMessage(user) 移至回复生成后,避免本轮 LLM 调用出现重复用户消息 - action 角色消息在 DB 存储时映射为 assistant,DeepSeek 等模型不支持自定义角色 - stream_end defer 机制确保错误路径也会终止客户端思考指示器 OS 完整环境支持: - host 包重构为 HostBackend 接口 + Direct/WSL/Docker 三种后端 - 新增 os_exec/os_file/os_system 工具供 AI 在完整 Linux 环境中自由操作 其他: - 视觉模型注入 + 图片预处理后清空 Images 避免传给 Chat 模型 - 图片 URL 相对路径→绝对 URL 转换 - DevTools 链路追踪页面 + 重启修复 - 记忆搜索模糊匹配增强 - 后台思考定时调度支持 - 管理后台页面 (模型配置/用户管理等) - docs/api 更新广播机制说明 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+104
-7
@@ -133,6 +133,7 @@ func main() {
|
||||
adminUserID := "admin"
|
||||
adminSessionID := "admin-session-main"
|
||||
if cfg.DatabaseURL != "" {
|
||||
convStore.SetDatabaseURL(cfg.DatabaseURL)
|
||||
if err := convStore.LoadFromDB(cfg.DatabaseURL, adminSessionID, 50); err != nil {
|
||||
log.Printf("⚠ 从数据库恢复会话历史失败(不影响服务启动): %v", err)
|
||||
}
|
||||
@@ -151,13 +152,22 @@ func main() {
|
||||
log.Println("IoT 客户端未配置 (IOT_SERVICE_URL 和 IOT_DEBUG_SERVICE_URL 均为空)")
|
||||
}
|
||||
|
||||
// 初始化主机操控管理器 (Phase 6.2: 沙箱执行 + 文件系统隔离)
|
||||
// 初始化主机操控管理器 (沙箱执行 + 文件系统隔离)
|
||||
hostSandbox := host.NewSandbox(host.DefaultSandboxConfig())
|
||||
hostManager := host.NewManager(hostSandbox)
|
||||
directBackend := host.NewDirectBackend(hostSandbox)
|
||||
hostManager := host.NewManager(directBackend)
|
||||
dataDir := getEnv("DATA_DIR", "/tmp/cyrene_data")
|
||||
hostManager.SetAllowedDirs([]string{dataDir, os.TempDir(), "."})
|
||||
log.Printf("主机操控管理器已就绪: 沙箱执行 + 文件隔离 (数据目录=%s)", dataDir)
|
||||
|
||||
// 初始化完整OS环境管理器 (WSL/Docker,无沙箱限制,供 os_* 工具使用)
|
||||
osManager := createOSManager()
|
||||
if osManager != nil {
|
||||
log.Printf("完整OS环境管理器已就绪: backend=%s", osManager.BackendName())
|
||||
} else {
|
||||
log.Println("完整OS环境管理器未配置 (设置 HOST_EXEC_BACKEND=wsl 或 docker 以启用)")
|
||||
}
|
||||
|
||||
// 初始化 RAG 知识库 (Phase 6.6: 知识库 RAG 增强)
|
||||
knowledgeDir := getEnv("KNOWLEDGE_DIR", "./data/knowledge")
|
||||
ragEmbedder := rag.NewEmbedder(cfg.LLMBaseURL, cfg.LLMAPIKey, "text-embedding-3-small")
|
||||
@@ -167,6 +177,7 @@ func main() {
|
||||
|
||||
// 初始化工具注册中心 (使用共享插件模块)
|
||||
toolRegistry := plgManager.NewToolRegistry()
|
||||
var visionProvider llm.LLMProvider
|
||||
if getEnvBool("ENABLE_TOOLS", true) {
|
||||
// 11 个共享通用插件 — 注册其工具到统一注册中心
|
||||
registerPluginTools(toolRegistry, &pluginCalc.CalculatorPlugin{})
|
||||
@@ -198,7 +209,13 @@ func main() {
|
||||
toolRegistry.Register(wrapTool(tools.NewHostSystemTool(hostManager), "host_system", "Host System Info", "system"))
|
||||
}
|
||||
|
||||
var visionProvider llm.LLMProvider
|
||||
if osManager != nil {
|
||||
toolRegistry.Register(wrapTool(tools.NewOSExecTool(osManager), "os_exec", "OS Command Execution", "system"))
|
||||
toolRegistry.Register(wrapTool(tools.NewOSFileTool(osManager), "os_file", "OS File Operations", "system"))
|
||||
toolRegistry.Register(wrapTool(tools.NewOSSystemTool(osManager), "os_system", "OS System Info", "system"))
|
||||
}
|
||||
|
||||
visionProvider = nil
|
||||
if configLoader != nil && configLoader.HasConfig() {
|
||||
cfg := configLoader.GetConfig()
|
||||
if route, ok := cfg.Routing["vision"]; ok && len(route.FallbackChain) > 0 {
|
||||
@@ -300,7 +317,9 @@ func main() {
|
||||
// 注册子会话提供者
|
||||
subManager.Register(subsession.NewGeneralProvider(personaLoader))
|
||||
if memRetriever != nil {
|
||||
subManager.Register(subsession.NewMemoryProvider(memRetriever))
|
||||
memProvider := subsession.NewMemoryProvider(memRetriever)
|
||||
memProvider.SetFuzzySearch(memoryAdapter, memClient)
|
||||
subManager.Register(memProvider)
|
||||
}
|
||||
if iotClient != nil {
|
||||
subManager.Register(subsession.NewIoTProvider(iotClient, personaDir))
|
||||
@@ -322,6 +341,10 @@ func main() {
|
||||
memExtractor,
|
||||
)
|
||||
orch.SetToolRegistry(toolRegistry)
|
||||
if visionProvider != nil {
|
||||
orch.SetVisionProvider(visionProvider)
|
||||
log.Printf("对话编排器: 视觉模型已注入 (%s)", visionProvider.ModelName())
|
||||
}
|
||||
log.Println("对话编排器 v2.0 已就绪")
|
||||
_ = orch
|
||||
|
||||
@@ -428,6 +451,32 @@ func main() {
|
||||
json.NewEncoder(w).Encode(toolRegistry.GetCallStats())
|
||||
})
|
||||
|
||||
// OS 环境监控端点
|
||||
mux.HandleFunc("/api/v1/system/info", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
result := map[string]interface{}{
|
||||
"os_enabled": osManager != nil,
|
||||
}
|
||||
if osManager != nil {
|
||||
result["backend"] = osManager.BackendName()
|
||||
result["system"] = osManager.SystemInfo()
|
||||
if disk, err := osManager.DiskUsage("/"); err == nil {
|
||||
result["disk"] = disk
|
||||
}
|
||||
}
|
||||
if hostManager != nil {
|
||||
result["host"] = map[string]interface{}{
|
||||
"backend": hostManager.BackendName(),
|
||||
"system": hostManager.SystemInfo(),
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
// 启动HTTP服务
|
||||
srv := &http.Server{
|
||||
Addr: ":" + cfg.Port,
|
||||
@@ -525,6 +574,42 @@ func getEnvBool(key string, fallback bool) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// createOSManager 根据 HOST_EXEC_BACKEND 环境变量创建完整OS环境管理器。
|
||||
// 支持 "wsl" 和 "docker" 两种后端。返回 nil 表示未配置或配置无效。
|
||||
func createOSManager() *host.Manager {
|
||||
backend := strings.ToLower(os.Getenv("HOST_EXEC_BACKEND"))
|
||||
switch backend {
|
||||
case "wsl":
|
||||
distro := getEnv("WSL_DISTRO", "Ubuntu-22.04")
|
||||
username := getEnv("WSL_USER", "cyrene")
|
||||
password := os.Getenv("WSL_USER_PASSWORD")
|
||||
maxTimeout := time.Duration(getEnvInt("HOST_EXEC_MAX_TIMEOUT", 300)) * time.Second
|
||||
wslBackend := host.NewWSLBackend(distro, username, password, maxTimeout)
|
||||
return host.NewManager(wslBackend)
|
||||
case "docker":
|
||||
container := getEnv("SANDBOX_CONTAINER", "cyrene-sandbox")
|
||||
image := getEnv("SANDBOX_IMAGE", "ubuntu:22.04")
|
||||
maxTimeout := time.Duration(getEnvInt("HOST_EXEC_MAX_TIMEOUT", 300)) * time.Second
|
||||
dockerBackend := host.NewDockerBackend(container, image, maxTimeout)
|
||||
return host.NewManager(dockerBackend)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// getEnvInt 获取整数类型的环境变量
|
||||
func getEnvInt(key string, fallback int) int {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return fallback
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil || n <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// registerPluginTools 从插件实例注册其所有工具到注册中心
|
||||
func registerPluginTools(registry *plgManager.ToolRegistry, plugin plgSDK.Plugin) {
|
||||
for _, t := range plugin.Tools() {
|
||||
@@ -638,9 +723,6 @@ func handleChat(
|
||||
userNickname = cfg.AdminNickname
|
||||
}
|
||||
|
||||
// 0.1 缓存用户消息到会话历史
|
||||
ctxBuilder.CacheMessage(req.SessionID, model.RoleUser, req.Message)
|
||||
|
||||
// 1. 设置 SSE 响应头
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
@@ -678,6 +760,19 @@ func handleChat(
|
||||
var fullContent string
|
||||
for event := range eventCh {
|
||||
switch event.Type {
|
||||
case model.StreamToolProgress:
|
||||
tp := event.ToolProgress
|
||||
progressData, _ := json.Marshal(map[string]interface{}{
|
||||
"type": "tool_progress",
|
||||
"tool_name": tp.ToolName,
|
||||
"status": tp.Status,
|
||||
"progress": tp.Progress,
|
||||
"message": tp.Message,
|
||||
"message_id": messageID,
|
||||
})
|
||||
fmt.Fprintf(w, "data: %s\n\n", progressData)
|
||||
flusher.Flush()
|
||||
|
||||
case model.StreamError:
|
||||
log.Printf("[chat] 流式错误: %v", event.Error)
|
||||
errData, _ := json.Marshal(map[string]string{"delta": "", "error": event.Error.Error()})
|
||||
@@ -729,6 +824,8 @@ func handleChat(
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存用户消息到会话历史(在回复生成后,避免本轮 LLM 调用出现重复用户消息)
|
||||
ctxBuilder.CacheMessage(req.SessionID, model.RoleUser, req.Message)
|
||||
// 4. 对话完成后触发昔涟的自主思考(事件驱动,非定时)
|
||||
if thinker != nil {
|
||||
thinker.TriggerPostChatThink()
|
||||
|
||||
Reference in New Issue
Block a user