Files
HomeAgent/internal/plugin/proc/shmcodec.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

515 lines
14 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 proc
import (
"encoding/json"
"fmt"
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
)
// StageContext 的跨进程编解码(§3.3 数据面 / §3.4 SDK 封装全部复杂度)。
//
// 设计要点:
//
// 1. **插件作者永远不接触 Slice{off,len}**。插件进程内保留原生
// *pubsdk.StageContext,handler 照常读写字段;stage 入口从共享段
// 反序列化成本地对象,handler 返回时把**脏字段**写回共享段。
//
// 2. **字段级描述符消除 lost update**。只改 FinalText 的插件不触碰
// ToolResults 的描述符,故不存在"只读插件把旧快照写回覆盖他人改写"
// (今日副本模型实测 35.8~36.8% 丢失,§8.4)。
//
// 3. **全部 16 个字段可见**。今日经 C ABI 只下发 10 个字段,外部插件永远
// 看不到 ContextMsgs/ReasoningContent/TokenUsage/Memory/Extra/Errors(§8.3);
// 共享内存下全部可见可改——接口形式不变,能力变强。
//
// 4. **Extra 的 4 个键提升为具名字段**(§3.3 已核实全部使用点仅这 4 个):
// media_blocks / media_type / input_source / output_channel。
// 它们都是内核写、插件读,无并发改写需求;真正需要多插件并发改的
// (LLMText/FinalText/ToolCalls/Errors)全是强类型字段。
// extra 中被提升为具名字段的键。
const (
ExtraKeyMediaBlocks = "media_blocks"
ExtraKeyMediaType = "media_type"
ExtraKeyInputSource = "input_source"
ExtraKeyOutputChannel = "output_channel"
)
// WriteAll 把整个 StageContext 写入共享段(内核侧在 stage 开始前调用一次)。
// 调用方须持有写锁。
func (s *Segment) WriteAll(sc *pubsdk.StageContext) error {
sc.RLock()
snap := captureLocal(sc)
sc.RUnlock()
return s.writeLocal(snap)
}
// localCtx 是 StageContext 的值快照,用于在不持有 sc 锁的情况下做编解码。
type localCtx struct {
RawMessage string
UserID string
GroupID string
LLMText string
ReasoningContent string
FinalText string
Response *string
Phase string
NoMemory bool
ContextMsgs []map[string]interface{}
ToolCalls []pubsdk.ToolCall
ToolResults []pubsdk.ToolResult
Memory []pubsdk.MemItem
TokenUsage map[string]int
Errors []string
ExtraMediaBlocks interface{}
ExtraMediaType interface{}
ExtraInputSource interface{}
ExtraOutputChan interface{}
}
func captureLocal(sc *pubsdk.StageContext) *localCtx {
l := &localCtx{
RawMessage: sc.RawMessage,
UserID: sc.UserID,
GroupID: sc.GroupID,
LLMText: sc.LLMText,
ReasoningContent: sc.ReasoningContent,
FinalText: sc.FinalText,
Response: sc.Response,
Phase: string(sc.Phase),
NoMemory: sc.NoMemory,
ContextMsgs: sc.ContextMsgs,
ToolCalls: sc.ToolCalls,
ToolResults: sc.ToolResults,
Memory: sc.Memory,
TokenUsage: sc.TokenUsage,
Errors: sc.Errors,
}
if sc.Extra != nil {
l.ExtraMediaBlocks = sc.Extra[ExtraKeyMediaBlocks]
l.ExtraMediaType = sc.Extra[ExtraKeyMediaType]
l.ExtraInputSource = sc.Extra[ExtraKeyInputSource]
l.ExtraOutputChan = sc.Extra[ExtraKeyOutputChannel]
}
return l
}
func (s *Segment) writeLocal(l *localCtx) error {
putStr := func(f stageField, v string) error {
sl, err := s.write([]byte(v))
if err != nil {
return fmt.Errorf("写入字段 %s: %w", f, err)
}
s.setDesc(f, sl)
return nil
}
putJSON := func(f stageField, v interface{}) error {
if v == nil {
s.setDesc(f, Slice{})
return nil
}
b, err := json.Marshal(v)
if err != nil {
return fmt.Errorf("序列化字段 %s: %w", f, err)
}
sl, err := s.write(b)
if err != nil {
return fmt.Errorf("写入字段 %s: %w", f, err)
}
s.setDesc(f, sl)
return nil
}
for _, step := range []struct {
f stageField
v string
}{
{fRawMessage, l.RawMessage},
{fUserID, l.UserID},
{fGroupID, l.GroupID},
{fLLMText, l.LLMText},
{fReasoningContent, l.ReasoningContent},
{fFinalText, l.FinalText},
{fPhase, l.Phase},
} {
if err := putStr(step.f, step.v); err != nil {
return err
}
}
// Response 是 *string:用标志位表达 nil,避免 "" 与 nil 混淆
if l.Response != nil {
if err := putStr(fResponse, *l.Response); err != nil {
return err
}
s.setFlag(flagResponseSet, true)
} else {
s.setDesc(fResponse, Slice{})
s.setFlag(flagResponseSet, false)
}
s.setFlag(flagNoMemory, l.NoMemory)
// 切片/映射字段:nil 与空切片都写成"未设置",避免插件收到 [] 后误以为
// 内核显式清空过(与今日 writable 的 len>0 才下发语义一致)。
jsonFields := []struct {
f stageField
v interface{}
}{
{fContextMsgs, sliceOrNil(len(l.ContextMsgs), l.ContextMsgs)},
{fToolCalls, sliceOrNil(len(l.ToolCalls), l.ToolCalls)},
{fToolResults, sliceOrNil(len(l.ToolResults), l.ToolResults)},
{fMemory, sliceOrNil(len(l.Memory), l.Memory)},
{fTokenUsage, sliceOrNil(len(l.TokenUsage), l.TokenUsage)},
{fErrors, sliceOrNil(len(l.Errors), l.Errors)},
{fExtraMediaBlocks, l.ExtraMediaBlocks},
{fExtraMediaType, l.ExtraMediaType},
{fExtraInputSource, l.ExtraInputSource},
{fExtraOutputChannel, l.ExtraOutputChan},
}
for _, step := range jsonFields {
if err := putJSON(step.f, step.v); err != nil {
return err
}
}
s.bumpSeq()
return nil
}
// sliceOrNil 让长度为 0 的容器写成 nil(未设置),非零则原样返回。
func sliceOrNil(n int, v interface{}) interface{} {
if n == 0 {
return nil
}
return v
}
// ReadInto 从共享段读出全部字段填充到 sc(插件进程侧 stage 入口调用)。
// 调用方须持有读锁或写锁。
func (s *Segment) ReadInto(sc *pubsdk.StageContext) error {
getStr := func(f stageField) (string, error) {
b, err := s.read(s.getDesc(f))
if err != nil {
return "", fmt.Errorf("读取字段 %s: %w", f, err)
}
return string(b), nil
}
getJSON := func(f stageField, out interface{}) error {
b, err := s.read(s.getDesc(f))
if err != nil {
return fmt.Errorf("读取字段 %s: %w", f, err)
}
if len(b) == 0 {
return nil
}
if err := json.Unmarshal(b, out); err != nil {
return fmt.Errorf("反序列化字段 %s: %w", f, err)
}
return nil
}
raw, err := getStr(fRawMessage)
if err != nil {
return err
}
uid, err := getStr(fUserID)
if err != nil {
return err
}
gid, err := getStr(fGroupID)
if err != nil {
return err
}
llm, err := getStr(fLLMText)
if err != nil {
return err
}
reason, err := getStr(fReasoningContent)
if err != nil {
return err
}
final, err := getStr(fFinalText)
if err != nil {
return err
}
phase, err := getStr(fPhase)
if err != nil {
return err
}
var ctxMsgs []map[string]interface{}
var toolCalls []pubsdk.ToolCall
var toolResults []pubsdk.ToolResult
var mem []pubsdk.MemItem
var usage map[string]int
var errs []string
if err := getJSON(fContextMsgs, &ctxMsgs); err != nil {
return err
}
if err := getJSON(fToolCalls, &toolCalls); err != nil {
return err
}
if err := getJSON(fToolResults, &toolResults); err != nil {
return err
}
if err := getJSON(fMemory, &mem); err != nil {
return err
}
if err := getJSON(fTokenUsage, &usage); err != nil {
return err
}
if err := getJSON(fErrors, &errs); err != nil {
return err
}
extra := map[string]interface{}{}
for _, pair := range []struct {
f stageField
key string
}{
{fExtraMediaBlocks, ExtraKeyMediaBlocks},
{fExtraMediaType, ExtraKeyMediaType},
{fExtraInputSource, ExtraKeyInputSource},
{fExtraOutputChannel, ExtraKeyOutputChannel},
} {
var v interface{}
if err := getJSON(pair.f, &v); err != nil {
return err
}
if v != nil {
extra[pair.key] = v
}
}
var respPtr *string
if s.getFlag(flagResponseSet) {
r, err := getStr(fResponse)
if err != nil {
return err
}
respPtr = &r
}
sc.Lock()
defer sc.Unlock()
sc.RawMessage = raw
sc.UserID = uid
sc.GroupID = gid
sc.LLMText = llm
sc.ReasoningContent = reason
sc.FinalText = final
sc.Phase = pubsdk.Stage(phase)
sc.NoMemory = s.getFlag(flagNoMemory)
sc.Response = respPtr
sc.ContextMsgs = ctxMsgs
sc.ToolCalls = toolCalls
sc.ToolResults = toolResults
sc.Memory = mem
sc.TokenUsage = usage
sc.Errors = errs
if len(extra) > 0 {
sc.Extra = extra
} else {
sc.Extra = nil
}
return nil
}
// WriteDirty 只把与 base 快照不同的字段写回共享段(插件进程侧 handler 返回后调用)。
//
// **这是消除 lost update 的关键**:只读插件的 dirty 集为空 → 零写入 →
// 不可能覆盖其他插件的改写。对比今日副本模型无条件回传 10 个字段的行为
// (§8.4 实测 35.8~36.8% 丢失,现网量级百分之几的脏数据进 LLM)。
//
// 返回实际写回的字段数,便于诊断与测试断言。
func (s *Segment) WriteDirty(sc *pubsdk.StageContext, base *Snapshot) (int, error) {
sc.RLock()
cur := captureLocal(sc)
sc.RUnlock()
now := newSnapshotFromLocal(cur)
changed := 0
putStr := func(f stageField, v string) error {
sl, err := s.write([]byte(v))
if err != nil {
return fmt.Errorf("写回字段 %s: %w", f, err)
}
s.setDesc(f, sl)
changed++
return nil
}
putRaw := func(f stageField, raw string) error {
if raw == "" {
s.setDesc(f, Slice{})
changed++
return nil
}
sl, err := s.write([]byte(raw))
if err != nil {
return fmt.Errorf("写回字段 %s: %w", f, err)
}
s.setDesc(f, sl)
changed++
return nil
}
for _, step := range []struct {
f stageField
v string
}{
{fRawMessage, cur.RawMessage},
{fUserID, cur.UserID},
{fGroupID, cur.GroupID},
{fLLMText, cur.LLMText},
{fReasoningContent, cur.ReasoningContent},
{fFinalText, cur.FinalText},
{fPhase, cur.Phase},
} {
if base.strs[step.f] != now.strs[step.f] {
if err := putStr(step.f, step.v); err != nil {
return changed, err
}
}
}
// JSON 字段:比较序列化结果
for f := range now.jsons {
if base.jsons[f] != now.jsons[f] {
if err := putRaw(f, now.jsons[f]); err != nil {
return changed, err
}
}
}
// Response 的 nil 语义变化也算脏
if base.responseSet != now.responseSet || base.response != now.response {
if now.responseSet {
if err := putStr(fResponse, now.response); err != nil {
return changed, err
}
s.setFlag(flagResponseSet, true)
} else {
// 插件把 Response 置回 nil:短路语义不应被撑销,故不清空内核已设的值。
// 与 C ABI 路径 applyStageResult 的行为保持一致。
s.setFlag(flagResponseSet, s.getFlag(flagResponseSet))
}
}
if base.noMemory != now.noMemory {
s.setFlag(flagNoMemory, now.noMemory)
changed++
}
if changed > 0 {
s.bumpSeq()
}
return changed, nil
}
// Snapshot 是 handler 运行前的字段快照,用于计算脏字段。
//
// ❗ 必须存**序列化后的字符串**而非 Go 值:StageContext 的切片字段与
// 调用方共享底层数组,handler 原地改元素(sc.ToolResults[0].Result = x)
// 时直接持有的 Go 值快照会跟着变,脏字段计算失效——这个坑在 C ABI 侧
// 修 11.3 时已经踩过一次(见 SDK 仓 stagediff_test.go 的注释)。
type Snapshot struct {
strs map[stageField]string
jsons map[stageField]string
response string
responseSet bool
noMemory bool
}
// Snapshot 抓取当前 StageContext 的快照(插件进程侧 handler 前调用)。
func TakeSnapshot(sc *pubsdk.StageContext) *Snapshot {
sc.RLock()
l := captureLocal(sc)
sc.RUnlock()
return newSnapshotFromLocal(l)
}
func newSnapshotFromLocal(l *localCtx) *Snapshot {
sn := &Snapshot{
strs: map[stageField]string{},
jsons: map[stageField]string{},
}
sn.strs[fRawMessage] = l.RawMessage
sn.strs[fUserID] = l.UserID
sn.strs[fGroupID] = l.GroupID
sn.strs[fLLMText] = l.LLMText
sn.strs[fReasoningContent] = l.ReasoningContent
sn.strs[fFinalText] = l.FinalText
sn.strs[fPhase] = l.Phase
marshal := func(v interface{}) string {
if v == nil {
return ""
}
b, err := json.Marshal(v)
if err != nil {
return ""
}
return string(b)
}
sn.jsons[fContextMsgs] = marshal(sliceOrNil(len(l.ContextMsgs), l.ContextMsgs))
sn.jsons[fToolCalls] = marshal(sliceOrNil(len(l.ToolCalls), l.ToolCalls))
sn.jsons[fToolResults] = marshal(sliceOrNil(len(l.ToolResults), l.ToolResults))
sn.jsons[fMemory] = marshal(sliceOrNil(len(l.Memory), l.Memory))
sn.jsons[fTokenUsage] = marshal(sliceOrNil(len(l.TokenUsage), l.TokenUsage))
sn.jsons[fErrors] = marshal(sliceOrNil(len(l.Errors), l.Errors))
sn.jsons[fExtraMediaBlocks] = marshal(l.ExtraMediaBlocks)
sn.jsons[fExtraMediaType] = marshal(l.ExtraMediaType)
sn.jsons[fExtraInputSource] = marshal(l.ExtraInputSource)
sn.jsons[fExtraOutputChannel] = marshal(l.ExtraOutputChan)
if l.Response != nil {
sn.response = *l.Response
sn.responseSet = true
}
sn.noMemory = l.NoMemory
return sn
}
// String 让字段枚举在错误信息里可读。
func (f stageField) String() string {
switch f {
case fRawMessage:
return "raw_message"
case fUserID:
return "user_id"
case fGroupID:
return "group_id"
case fLLMText:
return "llm_text"
case fReasoningContent:
return "reasoning_content"
case fFinalText:
return "final_text"
case fResponse:
return "response"
case fPhase:
return "phase"
case fContextMsgs:
return "context_msgs"
case fToolCalls:
return "tool_calls"
case fToolResults:
return "tool_results"
case fMemory:
return "memory"
case fTokenUsage:
return "token_usage"
case fErrors:
return "errors"
case fExtraMediaBlocks:
return "extra." + ExtraKeyMediaBlocks
case fExtraMediaType:
return "extra." + ExtraKeyMediaType
case fExtraInputSource:
return "extra." + ExtraKeyInputSource
case fExtraOutputChannel:
return "extra." + ExtraKeyOutputChannel
}
return fmt.Sprintf("field(%d)", int(f))
}