mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
## 背景 仓库早已迁移到 GitHub(`github.com/JianFeeeee/HomeAgent`,CI/CD 都在那边跑), gitcode 保留为**国内只读镜像**(`ci.yml` 的 sync-gitcode-main 快进推送)。 但 go.mod 的 module 路径一直是 `gitcode.com/JianFeeeee/...` —— 既与权威源 不符,`meta.SourceURL`(AGPL §13 的源码提供入口)也指向镜像而非实际运行的代码。 ## 关键区分(这是本次改动安全的前提) 两个 module 是**不同**的,影响面差一个数量级: | module | 谁依赖 | 改它的后果 | |---|---|---| | `.../HomeAgent`(主仓) | **只有主仓自己** | 内部重构,零外部影响 | | `.../homeagent-sdk`(SDK 仓) | 主仓 49 文件 + 所有外部插件 | 需重编插件 | ★ 实测确认:**插件不 import 主仓 module**(`strings qq/plugin.bin | grep HomeAgent` = 0), 只依赖 SDK 仓。而插件与内核的边界是 **RPC 方法名字符串** (`plugin.init` / `tool.invoke`…,见 proc/protocol.go),**与 Go module 名无关**。 ⇒ module 改名**不改变任何 wire 协议**,现网已装的插件照常运行。 SDK 的 GitHub 仓名是 `homeagentsdk`(无连字符),module 路径必须与之精确匹配, 否则 `go get` 会因路径不符失败,故取 `github.com/JianFeeeee/homeagentsdk`。 ## 改动 - `go.mod` / `go.sum`:两个 module 路径 + require/replace - 277 个 Go 文件 + Makefile + 打包脚本的 import 与 ldflags 路径 - SDK 仓 `meta.CoreModule`、`tools/hmapdev` 的 module 声明与工程模板 - 对外文档/链接的仓库地址(README、PLUGIN_DEV、OVERVIEW 等) ## 明确**保留** gitcode 的地方(镜像机制,不能动) - `GITCODE_TOKEN`、`GC_URL="…@gitcode.com"`(CI 同步 job) - `gitcode.com/api/v5/...`(release 附件上传) - git remote `gitcode`(镜像拉取/推送) - `docs/zh/ci-cd-runbook.md`(整篇讲镜像运维) ## 验证 - `go build ./...` / `go vet ./...` 绿 - `go test ./... -count=1` → 48 包 ok / 0 FAIL - hmapdev 工具链编译通过(`third_party/homeagent-sdk/tools/hmapdev`) - 全仓残留检查:module 形态的 `gitcode.com/JianFeeeee` = 0 - 镜像机制抽验:GC_URL / @gitcode / api/v5 / remote 均完好 ## 顺带修正 `hmapdev/templates.go` 的注释此前写「gitcode 的模块不在 proxy.golang.org 上」—— module 已迁 GitHub,该论断不再成立。改为如实说明**为何仍默认 replace 到本机 SDK**: 离线可构建、避免版本漂移、内网可用。理由与托管地无关,故 replace 保留。
166 lines
5.4 KiB
Go
166 lines
5.4 KiB
Go
package webui
|
||
|
||
import (
|
||
"encoding/json"
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
|
||
sdk "github.com/JianFeeeee/HomeAgent/internal/sdk"
|
||
)
|
||
|
||
// defaultHistoryFile 是聊天记录的默认文件名(落在 <data> 下)。
|
||
const defaultHistoryFile = "webui_chat_history.json"
|
||
|
||
// historyStore 把聊天记录存在**独立文件**里。
|
||
//
|
||
// 背景:原先聊天记录是插件配置项 plugin.webui.chathistory,存在 config.db 的
|
||
// config_webui 表里,带来三个后果:
|
||
//
|
||
// 1. 整段记录(生产实例实测 5,176,016 字节)会被 GET /api/v1/settings
|
||
// 当普通配置项整块返回(那次响应实测 8,244,108 字节);
|
||
// 2. 每来一条消息就把整段记录重新 marshal 后 UPDATE 回 config 表,而那次写要拿
|
||
// config registry 的**全局写锁** —— 消息频繁时所有配置读写都被拖着排队;
|
||
// 3. 存放位置不可配(想放挂载盘只能改整个 data_dir)。
|
||
//
|
||
// 现在改为独立文件:默认 <data>/webui_chat_history.json,插件设置 history_file
|
||
// 可指定(相对路径按 data 目录解析)。写盘用 tmp+rename 原子替换,
|
||
// 避免进程被杀时留下半截 JSON。
|
||
type historyStore struct {
|
||
mu sync.Mutex
|
||
path string
|
||
}
|
||
|
||
func newHistoryStore(path string) *historyStore { return &historyStore{path: path} }
|
||
|
||
// Path 返回记录文件路径(用于日志与状态展示)。
|
||
func (hs *historyStore) Path() string { return hs.path }
|
||
|
||
// Load 读回全部记录。文件不存在返回 nil;内容损坏时按空历史处理并告警 ——
|
||
// 聊天记录不是关键数据,不该因为它让 WebUI 起不来。
|
||
func (hs *historyStore) Load() []ChatMsg {
|
||
hs.mu.Lock()
|
||
defer hs.mu.Unlock()
|
||
b, err := os.ReadFile(hs.path)
|
||
if err != nil {
|
||
if !os.IsNotExist(err) {
|
||
log.Printf("[webui] 读取聊天记录 %s 失败: %v", hs.path, err)
|
||
}
|
||
return nil
|
||
}
|
||
var msgs []ChatMsg
|
||
if err := json.Unmarshal(b, &msgs); err != nil {
|
||
log.Printf("[webui] 聊天记录 %s 解析失败(按空历史处理): %v", hs.path, err)
|
||
return nil
|
||
}
|
||
return msgs
|
||
}
|
||
|
||
// Save 原子写回全部记录(同目录 tmp + rename)。
|
||
func (hs *historyStore) Save(msgs []ChatMsg) error {
|
||
b, err := json.Marshal(msgs)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
hs.mu.Lock()
|
||
defer hs.mu.Unlock()
|
||
if dir := filepath.Dir(hs.path); dir != "" && dir != "." {
|
||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
tmp := hs.path + ".tmp"
|
||
if err := os.WriteFile(tmp, b, 0644); err != nil {
|
||
return err
|
||
}
|
||
return os.Rename(tmp, hs.path)
|
||
}
|
||
|
||
// LoadWithMigration 返回聊天记录:以文件为准,并顺手收拾老版本的配置项。
|
||
//
|
||
// - 文件里已有记录:老配置项若还在(上次迁移没删掉),直接清掉;
|
||
// - 文件为空/不存在但老配置项有内容:把记录搬到文件,搬成功后删除配置项
|
||
// —— 这条 5MB 的记录正是 config.db 膨胀与设置接口大响应的来源。
|
||
func (hs *historyStore) LoadWithMigration(settings sdk.SettingsAPI) []ChatMsg {
|
||
if msgs := hs.Load(); len(msgs) > 0 {
|
||
if settings != nil {
|
||
if v, err := settings.Get("chathistory"); err == nil && v != nil {
|
||
if err := settings.Remove("chathistory"); err == nil {
|
||
log.Printf("[webui] 已清理历史遗留的配置项 chathistory(记录现存放于 %s)", hs.path)
|
||
}
|
||
}
|
||
}
|
||
return msgs
|
||
}
|
||
return migrateLegacyHistory(settings, hs)
|
||
}
|
||
|
||
// migrateLegacyHistory 把老配置项里的整段聊天记录搬到文件,成功后删掉那条配置。
|
||
func migrateLegacyHistory(settings sdk.SettingsAPI, hs *historyStore) []ChatMsg {
|
||
if settings == nil {
|
||
return nil
|
||
}
|
||
v, err := settings.Get("chathistory")
|
||
if err != nil || v == nil {
|
||
return nil
|
||
}
|
||
raw, ok := v.(string)
|
||
if !ok || strings.TrimSpace(raw) == "" {
|
||
// 空值也算遗留(占着设置页一行),直接删掉。
|
||
_ = settings.Remove("chathistory")
|
||
return nil
|
||
}
|
||
var msgs []ChatMsg
|
||
if err := json.Unmarshal([]byte(raw), &msgs); err != nil {
|
||
log.Printf("[webui] 历史配置项无法解析,保留原值不迁移: %v", err)
|
||
return nil
|
||
}
|
||
if err := hs.Save(msgs); err != nil {
|
||
log.Printf("[webui] 聊天记录迁移到 %s 失败(保留原配置项): %v", hs.path, err)
|
||
return nil
|
||
}
|
||
if err := settings.Remove("chathistory"); err != nil {
|
||
log.Printf("[webui] 聊天记录已迁移到 %s,但旧配置项删除失败: %v", hs.path, err)
|
||
} else {
|
||
log.Printf("[webui] 聊天记录已迁移到独立文件 %s(%d 条),并从插件配置表移除", hs.path, len(msgs))
|
||
}
|
||
return msgs
|
||
}
|
||
|
||
// resolveHistoryFile 解析聊天记录文件路径:
|
||
// 插件设置 history_file(非空)> 默认 <data>/webui_chat_history.json。
|
||
// 相对路径按 data 目录解析,便于把记录放到独立挂载盘。
|
||
func resolveHistoryFile(setting, dataDir string) string {
|
||
if s := strings.TrimSpace(setting); s != "" {
|
||
if filepath.IsAbs(s) {
|
||
return s
|
||
}
|
||
if dataDir == "" {
|
||
dataDir = os.TempDir()
|
||
}
|
||
return filepath.Join(dataDir, s)
|
||
}
|
||
if dataDir == "" {
|
||
// data 目录未知(嵌入/测试场景):落到系统临时目录,别污染进程当前目录。
|
||
dataDir = os.TempDir()
|
||
}
|
||
return filepath.Join(dataDir, defaultHistoryFile)
|
||
}
|
||
|
||
// settingString 读一个字符串型插件设置(读不到/类型不符都返回空串)。
|
||
func settingString(s sdk.SettingsAPI, key string) string {
|
||
if s == nil {
|
||
return ""
|
||
}
|
||
v, err := s.Get(key)
|
||
if err != nil || v == nil {
|
||
return ""
|
||
}
|
||
if str, ok := v.(string); ok {
|
||
return str
|
||
}
|
||
return ""
|
||
}
|