mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 18:08:04 +00:00
拆法:按「资源面」搬家,每个顶层声明(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)。
195 lines
5.4 KiB
Go
195 lines
5.4 KiB
Go
package webui
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"time"
|
||
|
||
"encoding/json"
|
||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
"net/http"
|
||
)
|
||
|
||
// OpenAI 兼容面:/v1/chat/completions(含流式)。
|
||
|
||
func (h *Handler) handleOpenAICompletions(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||
return
|
||
}
|
||
|
||
if h.sdk == nil {
|
||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "IO manager not available"})
|
||
return
|
||
}
|
||
|
||
var req struct {
|
||
Model string `json:"model"`
|
||
Messages []openAIMessage `json:"messages"`
|
||
Stream bool `json:"stream"`
|
||
Temperature float64 `json:"temperature"`
|
||
MaxTokens int `json:"max_tokens"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request: " + err.Error()})
|
||
return
|
||
}
|
||
if len(req.Messages) == 0 {
|
||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "messages is required"})
|
||
return
|
||
}
|
||
|
||
lastMsg := req.Messages[len(req.Messages)-1]
|
||
if lastMsg.Role != "user" {
|
||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "last message must be from user"})
|
||
return
|
||
}
|
||
|
||
// 带超时的上下文(同 handleChat:客户端断开立即取消;300s 约束长生成)
|
||
ctx, cancel := context.WithTimeout(r.Context(), 300*time.Second)
|
||
defer cancel()
|
||
|
||
respCh := make(chan *agentIO.OutputEvent, 1)
|
||
go func() {
|
||
// NoMemory:本端点(OpenAI 兼容 /v1/chat/completions)的调用方是
|
||
// IDE、工具与脚本,送来的是**固定的提示词模板**("请分析这段代码"之类),
|
||
// 不是人类在对话。记进记忆会把真实对话挤掉,而且同一模板会反复刷屏。
|
||
// 原文仍进上下文,模型照旧看得到;只是不参与向量化/关键词提取/蒸馏。
|
||
respCh <- h.sdk.InjectTextSyncNoMemory("http", "http", lastMsg.Content)
|
||
}()
|
||
|
||
var response *agentIO.OutputEvent
|
||
select {
|
||
case response = <-respCh:
|
||
case <-ctx.Done():
|
||
writeJSON(w, http.StatusGatewayTimeout, map[string]string{"error": "agent timeout (300s)"})
|
||
return
|
||
}
|
||
|
||
if response == nil {
|
||
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "no response from agent"})
|
||
return
|
||
}
|
||
|
||
content, _ := response.Payload["content"].(string)
|
||
reasoningContent, _ := response.Payload["reasoning_content"].(string)
|
||
usage, _ := response.Payload["usage"].(map[string]interface{})
|
||
|
||
if req.Stream {
|
||
h.writeOpenAIStream(w, req.Model, content, reasoningContent, usage)
|
||
return
|
||
}
|
||
|
||
resp := map[string]interface{}{
|
||
"id": fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()),
|
||
"object": "chat.completion",
|
||
"created": time.Now().Unix(),
|
||
"model": req.Model,
|
||
"choices": []map[string]interface{}{
|
||
{
|
||
"index": 0,
|
||
"message": map[string]interface{}{
|
||
"role": "assistant",
|
||
"content": content,
|
||
},
|
||
"finish_reason": "stop",
|
||
},
|
||
},
|
||
}
|
||
if reasoningContent != "" {
|
||
resp["choices"].([]map[string]interface{})[0]["message"].(map[string]interface{})["reasoning_content"] = reasoningContent
|
||
}
|
||
if usage != nil {
|
||
resp["usage"] = usage
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
json.NewEncoder(w).Encode(resp)
|
||
}
|
||
|
||
func (h *Handler) writeOpenAIStream(w http.ResponseWriter, model, content, reasoningContent string, usage map[string]interface{}) {
|
||
flusher, ok := w.(http.Flusher)
|
||
if !ok {
|
||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "streaming not supported"})
|
||
return
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "text/event-stream")
|
||
w.Header().Set("Cache-Control", "no-cache")
|
||
w.Header().Set("Connection", "keep-alive")
|
||
w.WriteHeader(http.StatusOK)
|
||
flusher.Flush()
|
||
|
||
// 如果有 reasoning_content,先发送一个 reasoning chunk
|
||
if reasoningContent != "" {
|
||
reasoningChunk := map[string]interface{}{
|
||
"id": fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()),
|
||
"object": "chat.completion.chunk",
|
||
"created": time.Now().Unix(),
|
||
"model": model,
|
||
"choices": []map[string]interface{}{
|
||
{
|
||
"index": 0,
|
||
"delta": map[string]interface{}{
|
||
"content": "",
|
||
"reasoning_content": reasoningContent,
|
||
},
|
||
"finish_reason": nil,
|
||
},
|
||
},
|
||
}
|
||
data, _ := json.Marshal(reasoningChunk)
|
||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||
flusher.Flush()
|
||
}
|
||
|
||
// content chunk
|
||
contentChunk := map[string]interface{}{
|
||
"id": fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()),
|
||
"object": "chat.completion.chunk",
|
||
"created": time.Now().Unix(),
|
||
"model": model,
|
||
"choices": []map[string]interface{}{
|
||
{
|
||
"index": 0,
|
||
"delta": map[string]interface{}{
|
||
"content": content,
|
||
},
|
||
"finish_reason": nil,
|
||
},
|
||
},
|
||
}
|
||
data, _ := json.Marshal(contentChunk)
|
||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||
flusher.Flush()
|
||
|
||
// finish chunk
|
||
finishChunk := map[string]interface{}{
|
||
"id": fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()),
|
||
"object": "chat.completion.chunk",
|
||
"created": time.Now().Unix(),
|
||
"model": model,
|
||
"choices": []map[string]interface{}{
|
||
{
|
||
"index": 0,
|
||
"delta": map[string]interface{}{},
|
||
"finish_reason": "stop",
|
||
},
|
||
},
|
||
}
|
||
if usage != nil {
|
||
finishChunk["usage"] = usage
|
||
}
|
||
data, _ = json.Marshal(finishChunk)
|
||
fmt.Fprintf(w, "data: %s\n\n", data)
|
||
flusher.Flush()
|
||
|
||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||
flusher.Flush()
|
||
}
|
||
|
||
type openAIMessage struct {
|
||
Role string `json:"role"`
|
||
Content string `json:"content"`
|
||
}
|