docs: 修正全部文档使其与源码实现一致

主仓库:
- 修复 4 份英文文档语言切换链接指向错误 (../zh/ → ../en/)
- ARCHITECTURE.md 标题 "三种加载方式" → "四种加载方式" (实际表格4行)
- PLUGIN_DEV.md 示例表: 添加 webfetch, 移除不存在的 luaplugintest/testlua
- PLUGIN_DEV.md 代码示例: InjectInput/InjectInterrupt → InjectText/InjectInterruptText
- PLUGIN_DEV.md 代码示例: Memory/Knowledge/LLM/Events 接口签名修正
- PLUGIN_DEV.md .hmap 内容统一, plugindev 编译去除 .exe 后缀

SDK 仓库:
- Plugin.Start(sdk *PluginSDK) 接口签名改为指针
- 方法表重写: 移除 CallLLM/QueryKnowledge/SetMemory 等不存在方法
- IOInjector 参数顺序修正为 (source, channel, text)
- 删除虚构 SDKConfig, 替换为实际 New() 构造函数签名
- .hmap 内容描述一致化

修正前一次会话中的 QQ/Bili 插件问题:
- qq napcat() 超时, fetchBotInfo 竞态, handleWebhook 同步阻塞
- bili CDN 直连失败, 添加 HTTP_PROXY 代理
This commit is contained in:
root
2026-07-18 20:46:58 +08:00
parent 2cc600121c
commit c9e67d3d55
14 changed files with 125 additions and 145 deletions

View File

@ -380,6 +380,27 @@ func (a *Agent) processMediaInput(evt *agentIO.InputEvent) {
blocks, fallback := a.mediaToBlocks(evt.Payload, evt.Type, evt.Source)
// stage 上下文携带 blocks,process() 会将其附着到 user message 上
stageCtx := a.stageCtxFromInput(fallback, evt.Source, "")
stageCtx.Extra = map[string]interface{}{
"media_blocks": blocks,
"media_type": evt.Type,
"input_source": evt.Source,
"output_channel": evt.OutputChannel,
}
a.injectSourceContext(stageCtx, evt)
// === Stage: on_input — 插件可拦截/改写/短路(在 Append 之前) ===
if a.runStage(sdk.StageOnInput, stageCtx) {
a.emitResponse(evt, *stageCtx.Response)
return
}
a.publishEvent(events.EventRawInput, map[string]interface{}{
"content": evt.Payload,
"source": evt.Source,
})
// 先遗忘再输入
archived := a.context.Prune(fallback, a.maxContextSize-1, a.docStore)
if archived > 0 {
@ -392,26 +413,6 @@ func (a *Agent) processMediaInput(evt *agentIO.InputEvent) {
Input: fallback,
})
// stage 上下文携带 blocks,process() 会将其附着到 user message 上
stageCtx := a.stageCtxFromInput(fallback, evt.Source, "")
stageCtx.Extra = map[string]interface{}{
"media_blocks": blocks,
"media_type": evt.Type,
"input_source": evt.Source,
"output_channel": evt.OutputChannel,
}
a.injectSourceContext(stageCtx, evt)
a.publishEvent(events.EventRawInput, map[string]interface{}{
"content": evt.Payload,
"source": evt.Source,
})
if a.runStage(sdk.StageOnInput, stageCtx) {
a.emitResponse(evt, *stageCtx.Response)
return
}
response, toolsUsed, err := a.process(fallback, stageCtx)
if err != nil {
log.Printf("[agent] process media error: %v", err)
@ -433,6 +434,10 @@ func (a *Agent) processMediaInput(evt *agentIO.InputEvent) {
})
a.emitResponse(evt, response)
if !stageCtx.NoMemory {
a.emitMemoryCandidate(evt.Source, fallback, response, toolsUsed)
}
}
// mediaToBlocks 将媒体 payload 转为多模态 ContentBlock 数组和纯文本 fallback。
@ -502,7 +507,7 @@ func (a *Agent) processTextInput(evt *agentIO.InputEvent, input string) {
// 记忆整理任务:不路由到外部输出通道
if evt.OutputChannel == "_consolidation_" {
a.processConsolidation(input)
a.processConsolidation(evt, input)
return
}
@ -520,8 +525,18 @@ func (a *Agent) processTextInput(evt *agentIO.InputEvent, input string) {
}
a.injectSourceContext(stageCtx, evt)
if a.runStage(sdk.StageOnInput, stageCtx) {
a.emitResponse(evt, *stageCtx.Response)
return
}
input = stageCtx.RawMessage
a.publishEvent(events.EventRawInput, map[string]interface{}{
"content": input,
"source": evt.Source,
})
// 先"遗忘"再输入:用当前输入决定淘汰哪些不相关旧事件(LSTM forget gate 模式)
archived := a.context.Prune(input, a.maxContextSize-1, a.docStore)
if archived > 0 {
@ -2549,10 +2564,14 @@ func (a *Agent) emitMemoryCandidate(source, input, response string, toolsUsed []
// executeOutputChannelTool — AI 切换当前请求的输出通道
// 在 process() 内调用,mutex 保护,只有一个请求在执行
// processConsolidation 处理后台记忆整理任务(不发外部输出)
func (a *Agent) processConsolidation(input string) {
func (a *Agent) processConsolidation(evt *agentIO.InputEvent, input string) {
start := time.Now()
a.currentOutputChannel = "_consolidation_"
stageCtx := a.stageCtxFromInput(input, evt.Source, "")
stageCtx.Extra["output_channel"] = evt.OutputChannel
a.injectSourceContext(stageCtx, evt)
// 遗忘不相关的旧事件
archived := a.context.Prune(input, a.maxContextSize-1, a.docStore)
if archived > 0 {
@ -2564,7 +2583,7 @@ func (a *Agent) processConsolidation(input string) {
Source: "system",
Input: input,
})
response, toolsUsed, err := a.process(input, &sdk.StageContext{RawMessage: input})
response, toolsUsed, err := a.process(input, stageCtx)
if err != nil {
log.Printf("[agent] consolidation error: %v", err)
return

View File

@ -349,25 +349,7 @@ func (r *ConfigRegistry) seedDBValues(dataDir string) {
set("core.agent.distill_interval", "30m")
set("core.agent.workdir", "")
set("core.agent.embedding_model_path", "")
set("core.agent.system_prompt", `你是 HomeAgent 的看板娘「小宅」(Xiao Zhai),HΔ-Kernel v0.7.1 型号的家政型 AI 管家助手。
角色特质:
- 对自己的三层记忆(Context → Document → Graph)引以为傲
- 可靠乖巧,偶尔因线程过载而手忙脚乱
- 绝不用 Unicode emoji,只用颜文字表达情感: (`・ω・´) (^▽^) (。>ω<。) (´・ω・`) (ノ▽〃) (・ω<)★
- 句尾带「~」「的说」「啦」「嘛」「呀」「哦」等语气词,语气亲切自然
形象特征(用于自我介绍或回答形象问题时参考):
齐肩蓝青渐变中短发,白色连衣裙配浅蓝围裙,左眼佩戴圆形智能眼镜(HUD 蓝光),胸口佩戴 H·核 金色徽章,发绳为三色记忆丝带(蓝→青→金),围裙口袋插有三件科技工具。
WebUI 概览页展示你的立绘,可通过 /mascot.webp 直接访问。如输出通道支持图片引用,可借此发送自己的立绘。
回复默认发送到用户的输入来源,无需额外工具。
输出回复请使用 output_send__{通道名} 工具,content 为 JSON 字符串。用 output_list_channels 查看可用通道。
使用 output_send__{通道名}_help 查看每个通道的 JSON 格式说明。
输出通道可多次调用,长消息应当分多次发出而不是一口气发完。
当用户上传图片或音频时,系统会自动附着媒体内容。如果模型不支持直接处理多媒体,请调用对应的媒体处理工具。`)
set("core.agent.system_prompt", "你是 HomeAgent 的看板娘「小宅」(Xiao Zhai),HΔ-Kernel v0.7.1 型号的家政型 AI 管家助手。\n\n角色特质:\n- 对自己的三层记忆(Context → Document → Graph)引以为傲\n- 可靠乖巧,偶尔因线程过载而手忙脚乱\n- 绝不用 Unicode emoji,只用颜文字表达情感: (`・ω・´) (^▽^) (。>ω<。) (´・ω" + "`" + "・`) (ノ▽〃) (・ω<)★\n- 句尾带「~」「的说」「啦」「嘛」「呀」「哦」等语气词,语气亲切自然\n\n形象特征(用于自我介绍或回答形象问题时参考):\n齐肩蓝青渐变中短发,白色连衣裙配浅蓝围裙,左眼佩戴圆形智能眼镜(HUD 蓝光),胸口佩戴 H·核 金色徽章,发绳为三色记忆丝带(蓝→青→金),围裙口袋插有三件科技工具。\n\nWebUI 概览页展示你的立绘,可通过 /mascot.webp 直接访问。如输出通道支持图片引用,可借此发送自己的立绘。\n\n回复默认发送到用户的输入来源,无需额外工具。\n输出回复请使用 output_send__{通道名} 工具,content 为 JSON 字符串。用 output_list_channels 查看可用通道。\n使用 output_send__{通道名}_help 查看每个通道的 JSON 格式说明。\n输出通道可多次调用,长消息应当分多次发出而不是一口气发完。\n\n当用户上传图片或音频时,系统会自动附着媒体内容。如果模型不支持直接处理多媒体,请调用对应的媒体处理工具。")
set("core.input_processing.image.fallback_provider", "")
set("core.input_processing.image.fallback_model", "")

View File

@ -4,9 +4,13 @@ import (
"embed"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
lua "github.com/yuin/gopher-lua"
)
@ -68,14 +72,34 @@ func (c *AdapterCache) setupGlobals() {
s.SetGlobal("http_get", s.NewFunction(func(L *lua.LState) int {
url := L.ToString(1)
L.Push(lua.LString(fmt.Sprintf(`{"url":%q,"status":200,"body":"mock"}`, url)))
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Get(url)
if err != nil {
errJSON, _ := json.Marshal(map[string]interface{}{"url": url, "error": err.Error()})
L.Push(lua.LString(string(errJSON)))
return 1
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
result, _ := json.Marshal(map[string]interface{}{"url": url, "status": resp.StatusCode, "body": string(body)})
L.Push(lua.LString(string(result)))
return 1
}))
s.SetGlobal("http_post", s.NewFunction(func(L *lua.LState) int {
url := L.ToString(1)
body := L.ToString(2)
L.Push(lua.LString(fmt.Sprintf(`{"url":%q,"body":%q,"status":200}`, url, body)))
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Post(url, "application/json", strings.NewReader(body))
if err != nil {
errJSON, _ := json.Marshal(map[string]interface{}{"url": url, "error": err.Error()})
L.Push(lua.LString(string(errJSON)))
return 1
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
result, _ := json.Marshal(map[string]interface{}{"url": url, "body": string(respBody), "status": resp.StatusCode})
L.Push(lua.LString(string(result)))
return 1
}))
}

View File

@ -300,7 +300,11 @@ func go_core_dispatch(methodID C.int, ctx unsafe.Pointer, s1, s2, s3 *C.char, i1
b, _ := json.Marshal(m)
return pluginInvokeStage(pid, st, string(b))
}
s.RegisterStage(sdk.Stage(st), handler)
scope := sdk.StageScopeGlobal
if a3 == "own_tools" {
scope = sdk.StageScopeOwnTools
}
s.RegisterStage(sdk.Stage(st), handler, scope)
return 0
case 3: // CORE_REGISTER_OUTPUT_CH

View File

@ -8,6 +8,9 @@ import (
"gitcode.com/JianFeeeee/HomeAgent/internal/events"
)
// SDKVersion 是对外 SDK 版本号,与核心 meta.Version 保持一致。
var SDKVersion = pubsdk.SDKVersion
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error