Files
HomeAgent/internal/plugins/webui/history.go
JianFeeeee 6d38a164eb chore(module): Go module 路径随托管地迁移 gitcode → github
## 背景

仓库早已迁移到 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 保留。
2026-10-08 10:40:17 +08:00

166 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ""
}