refactor(webui): handler.go 2993 行按资源拆成 11 个同包文件

拆法:按「资源面」搬家,每个顶层声明(func/type/var/const)整体搬到目标文件,
声明体一字未改,各文件按实际用到的包重新生成 import。文件头加一行说明本文件负责哪一面。

  handler.go              骨架:嵌入前端资源、Handler/构造、路由表、鉴权会话日志中间件、静态页
  handler_chat.go         对话面:消息模型与内存历史、SSE 事件订阅、对话/历史接口
  handler_upload.go       上传面:handleChatFile / handleUploads / 中断对话
  handler_memory.go       记忆面:图/文档/文本记忆、知识库、LLM 源、变更追踪
  handler_agents.go       内核与代理面:状态、kernel、人格、代理/快照/回滚
  handler_settings.go     设置与插件面:配置读写、插件列表详情(含 pluginmgr 反代)
  handler_terminal.go     终端面:终端会话、终端接口、命令历史
  handler_sse.go          SSE 环形缓冲(断线重连补发)
  handler_openai.go       OpenAI 兼容面:/v1/chat/completions
  handler_device.go       设备网关反代(HTTP + WS 升级透传)
  handler_files.go        /files/ 与 /uploads/ 下载

零漂移校验:拿重构前的 handler.go 与新 11 个文件逐行比对(忽略空行、package/import 头),
**丢失行 0**;新增行恰好是 11 个文件头注释(14 行)。

顺带修掉 import 里两处假使用:handler_openai 的 sdk 只作为 Handler 字段名出现(h.sdk.),
handler_settings 的 fmt 只出现在注释里 —— 都从 import 里去掉。

验证:go build ./... / go vet / webui+config+sdk 测试全绿;
起真实实例(沿用已有 data 目录)后 /status /settings /chat/history /plugins /terminals
/kernel /memory /config /login 全部 200,设置在注入 5MB 历史的情况下仍是 33,921 字节。

最大文件从 2993 → 706 行(handler_chat.go)。
This commit is contained in:
HomeAgent Agent
2026-09-14 07:03:47 +08:00
parent c4998bb102
commit 642e1c39b1
12 changed files with 2623 additions and 2485 deletions

View File

@ -0,0 +1,240 @@
package webui
import (
"fmt"
"os"
"strings"
"time"
"encoding/json"
internalConfig "gitcode.com/JianFeeeee/HomeAgent/internal/config"
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
"gitcode.com/JianFeeeee/HomeAgent/pkg/types"
"net/http"
"path/filepath"
)
// 内核与代理面:状态、kernel 快照、人格、代理列表/快照/回滚/动作。
func (h *Handler) handleStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
agentCount := 0
if h.supervisor != nil {
agentCount = len(h.supervisor.ListAgents())
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "running",
"uptime": time.Since(h.startTime).Round(time.Second).String(),
"agents": agentCount,
// 内核版本取 internal/meta(-ldflags 注入点)。
//
// 此前这里给的是 `sdk.SDKVersion`,那条链最终指向 **SDK 仓
// meta.Version 的硬编码值**,与构建时注入的内核版本无关——
// 两仲版本号碰巧相等时看不出问题,一旦不等就报错。
// SDK 版本另用 sdk_version 字段并行暴露。
"version": meta.Version,
"commit": meta.Commit,
"build_time": meta.BuildTime,
"sdk_version": sdk.SDKVersion,
"startedAt": h.startTime,
})
}
func (h *Handler) handleKernel(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if h.status == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "kernel status provider not available"})
return
}
writeJSON(w, http.StatusOK, h.status.GetKernelStatus())
}
// 人格设定:配置项键,以及「首启向导已经问过」的一次性标记。
//
// 为什么需要向导:人格曾经只有 <dataDir>/personal/personal.md 一个来源且无人维护,
// 里面写死的旧版本号反过来让实例自述旧版本(v1.2.0 压测发现)。
// 现在人格是配置项(默认模板不含任何版本号),首启问一次,之后不再打扰。
// handlePersona 是首启人格向导的后端(与内核 persona_set 工具共用 internal/config 的实现)。
//
// GET → {initialized, current_prompt, file_override}
// POST → {"mode":"default"|"custom"|"later","content":"..."}
// 写入 core.agent.personal_prompt 并打一次性标记,返回 restart_required
//
// 生效时机:人格在 homed 启动时载入(以【人格设定】块拼进系统提示词),
// 所以**自定义内容需重启生效**;选「默认」或「稍后」(保持当前默认)无需重启。
// 不回答就是「稍后」:保留默认并打标记,不阻塞任何流程。
//
// 跨通道:这里只是 WebUI 侧的入口;任何通道的消息到来时,内核都会检查同一枚标记,
// 未确认则在提示词里要求模型主动询问(见 internal/agent/core 的首启门禁)。
func (h *Handler) handlePersona(w http.ResponseWriter, r *http.Request) {
if h.settings == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "settings not available"})
return
}
switch r.Method {
case http.MethodGet:
writeJSON(w, http.StatusOK, map[string]interface{}{
"initialized": internalConfig.PersonaInitializedKV(h.settings),
"current_prompt": internalConfig.CurrentPersonaKV(h.settings),
"file_override": h.personaFileExists(),
})
case http.MethodPost:
var req struct {
Mode string `json:"mode"`
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
return
}
restart, err := internalConfig.SetPersonaKV(h.settings, req.Mode, req.Content)
if err != nil {
// 非法 mode / 空内容 → 400;落库失败 → 500。两者都不打标记。
code := http.StatusInternalServerError
if strings.Contains(err.Error(), "unknown mode") || strings.Contains(err.Error(), "content required") {
code = http.StatusBadRequest
}
writeJSON(w, code, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"status": "ok", "mode": req.Mode, "restart_required": restart,
})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
// personaFileExists 报告是否存在会覆盖配置项的人格文件(存在时它优先)。
// 数据目录取自 core.daemon.data_dir(由播种写入)。
func (h *Handler) personaFileExists() bool {
v, err := h.settings.GetCore("core.daemon.data_dir")
if err != nil {
return false
}
dir, _ := v.(string)
if dir == "" {
return false
}
_, err = os.Stat(filepath.Join(dir, "personal", "personal.md"))
return err == nil
}
func (h *Handler) handleAgents(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
if h.supervisor == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
return
}
agents := h.supervisor.ListAgents()
writeJSON(w, http.StatusOK, map[string]interface{}{"agents": agents})
case http.MethodPost:
var cfg types.AgentConfig
if err := json.NewDecoder(r.Body).Decode(&cfg); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
if cfg.ID == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "agent id is required"})
return
}
if h.config != nil {
kcfg := h.config.Get()
kcfg.Agents = append(kcfg.Agents, cfg)
h.config.Put(kcfg)
}
writeJSON(w, http.StatusCreated, map[string]string{"id": string(cfg.ID)})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *Handler) handleAgentByID(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/api/v1/agents/")
parts := strings.Split(path, "/")
agentID := types.AgentID(parts[0])
if len(parts) == 1 {
switch r.Method {
case http.MethodGet:
if h.supervisor == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
return
}
status, err := h.supervisor.GetAgentStatus(string(agentID))
if err != nil {
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, status)
case http.MethodDelete:
writeJSON(w, http.StatusOK, map[string]string{"status": "deleted", "id": string(agentID)})
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
return
}
action := parts[1]
switch action {
case "snapshots":
h.handleSnapshots(w, r, agentID, parts)
case "rollback":
h.handleRollback(w, r, agentID, parts)
case "start", "stop", "restart":
h.handleAgentAction(w, r, agentID, action)
default:
http.Error(w, "not found", http.StatusNotFound)
}
}
func (h *Handler) handleSnapshots(w http.ResponseWriter, r *http.Request, agentID types.AgentID, parts []string) {
switch r.Method {
case http.MethodGet:
writeJSON(w, http.StatusOK, map[string]interface{}{"agent_id": agentID, "snapshots": []map[string]interface{}{}})
case http.MethodPost:
if h.supervisor == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
return
}
snap, err := h.supervisor.PreActionSnapshot(string(agentID))
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusCreated, snap)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *Handler) handleRollback(w http.ResponseWriter, r *http.Request, agentID types.AgentID, parts []string) {
if r.Method != http.MethodPost || len(parts) < 3 {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if h.supervisor == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "supervisor not available"})
return
}
snapID := types.SnapshotID(parts[2])
if err := h.supervisor.RollbackAgent(string(agentID), string(snapID)); err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "rollback_initiated", "agent": string(agentID), "snap": string(snapID)})
}
func (h *Handler) handleAgentAction(w http.ResponseWriter, r *http.Request, agentID types.AgentID, action string) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
writeJSON(w, http.StatusNotImplemented, map[string]string{"error": fmt.Sprintf("agent %s action not implemented by supervisor", action)})
}