mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-31 22:37:28 +00:00
## 1. L2(冷上下文)预填充此前只走「注入文本」,等于没有
设计:prune 与 recall 是一对 —— 工具声明 ContextPolicy=prune + RecallPolicy=auto
时,工具输出既触发裁剪、也触发预填充(memoryPass → recallTextFor)。
但 L2 那一路只写在 buildSystemPrompt 里,query 用的是**注入文本**;
QQ 注入的是「来自「X」的私聊消息(message_id=…),先用 qq_get_message 取正文」,
与用户真正说的话毫无相似度 ⇒ 冷上下文预填充恒为空。
(现场:头天晚上讲的冷笑话,第二天早上完全唤不回;14 小时里
memory recall (input…) 只有 5 次、且 query 是元信息。)
改法:把 L2 检索并入 buildMemoryContext —— 输入路用输入文本、工具路用工具正文,
两条路共用同一次召回与预算;buildSystemPrompt 里那份重复注入删掉。
中断消息仍然 NoMemory + RecallPolicy=none(不裁剪、不召回、不进记忆),
这是设计要求,未改动。
## 2. 回退换 provider 时必须换 model(视觉失效的真因)
现场日志:
deepseek 402 Insufficient Balance
→ 回退 llmsproxy,但 req 仍带 model=「deepseek-flash」
→ llmsproxy 403 model_not_allowed
→ llmsproxy 被整源标记 unavailable(冷却)
→ 视觉回退 resolveModalFallback 跳过它 ⇒「配置了视觉却说没有视觉」
两处修正:
· 每个 provider 用自己的 model 发请求(model 是 provider 的属性)
· 403 区分「模型不被该 key 允许」与「凭证失效」:前者只否一个 model,
provider 仍可用于其他 model 与视觉回退
判据:internal/agent/core/provider_model_fallback_test.go
211 lines
7.0 KiB
Go
211 lines
7.0 KiB
Go
package sdk
|
||
|
||
import (
|
||
"fmt"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/events"
|
||
"github.com/JianFeeeee/HologramAgent/internal/security"
|
||
pubsdk "github.com/JianFeeeee/hologramagentsdk/sdk"
|
||
)
|
||
|
||
// 事件类型/事件类型别名:插件经 SDK 订阅/发布内核事件,
|
||
// 无需直接 import internal/events(内核事件总线仅通过 SDK 暴露)。
|
||
|
||
type EventType = events.EventType
|
||
type Event = events.Event
|
||
|
||
const (
|
||
EventRawInput = events.EventRawInput
|
||
EventAgentOutput = events.EventAgentOutput
|
||
EventAgentLLMChain = events.EventAgentLLMChain
|
||
EventToolCall = events.EventToolCall
|
||
EventReasoning = events.EventReasoning
|
||
EventStage = events.EventStage
|
||
EventSystem = events.EventSystem
|
||
EventTerminalOutput = events.EventTerminalOutput
|
||
EventAll = events.EventAll
|
||
|
||
// 流式增量事件(token 级):核心 process() 流式化后每收到一个增量块发布。
|
||
// 客户端可选订做真逐 token 渲染;聚合事件仍照常发布,旧订阅者不受影响。
|
||
EventReasoningDelta = events.EventReasoningDelta
|
||
EventContentDelta = events.EventContentDelta
|
||
|
||
// EventMemoryAccess 是图记忆的真实读写事件(哪个块被读/写/删/合)。
|
||
//
|
||
// 星图靠它做「反馈」:此前只能拿 tool_call 的工具名猜节点,而工具名是英文、
|
||
// 实体名是中文概念(实测命中 0),只能退化成点亮一批固定节点。
|
||
EventMemoryAccess = events.EventMemoryAccess
|
||
)
|
||
|
||
// ───────────────────── 受限事件总线(插件可见) ─────────────────────
|
||
//
|
||
// **本层只做适配,不做策略**(用户 2026-10-27 的指正):
|
||
//
|
||
// internal/security 谁可以发/订阅什么事件(EventPolicy)+ 载荷过闸
|
||
// internal/sdk(本文件) 查策略 → 发/订 → 过闸 → 落到 *events.Bus
|
||
// sdk(公开) 只给接口与事件名
|
||
//
|
||
// 为什么策略必须住在 security:事件的边界就是**授权**。能发 skill_detected
|
||
// 等于能驱动 skillmgr 接管;能发 update_request 等于能触发内核升级流程。
|
||
// 这类判断与工具黑白名单是同一类,必须与它们同处一处,否则策略会有两份真相,
|
||
// 换个实现就换口径。
|
||
|
||
type eventsAPI struct {
|
||
pluginName string
|
||
bus *events.Bus
|
||
sec *security.Runtime // 可 nil:未装配时按**默认策略**(最小集合)判定
|
||
unsubs []func()
|
||
// unavailable 非空表示宿主没接总线:Publish/Subscribe 一律返回该原因。
|
||
unavailable string
|
||
}
|
||
|
||
// newEventsAPI 构造插件的受限事件面。
|
||
//
|
||
// ★ bus 为 nil 时返回**不可用实现**而不是 nil:
|
||
// 返回 nil 会让 `s.Events().Publish(...)` 在未接总线的宿主里直接 panic
|
||
// (实测:internal/plugins 的集成测试在 loadOne 阶段崩在 clawhubadapter 上)。
|
||
// 插件不该为"宿主没接总线"写 nil 判断——那是一个该报错、而不是该崩溃的状态。
|
||
func newEventsAPI(pluginName string, bus *events.Bus) pubsdk.EventsAPI {
|
||
api := &eventsAPI{pluginName: pluginName, bus: bus, sec: security.RuntimeGlobal()}
|
||
if bus == nil {
|
||
api.unavailable = "event bus is not wired in this host"
|
||
}
|
||
return api
|
||
}
|
||
|
||
func (e *eventsAPI) AllowedPublish() []string {
|
||
pol := e.EventPolicy()
|
||
out := make([]string, len(pol.Publishable))
|
||
copy(out, pol.Publishable)
|
||
return append(out, "plugin."+e.pluginName+".*")
|
||
}
|
||
|
||
func (e *eventsAPI) AllowedSubscribe() []string {
|
||
pol := e.EventPolicy()
|
||
out := make([]string, len(pol.Subscribable))
|
||
copy(out, pol.Subscribable)
|
||
return out
|
||
}
|
||
|
||
// EventPolicy 取当前策略:优先用 security Runtime 的(可热改),
|
||
// Runtime 为 nil 时退回 security 包的默认策略(同样是最小集合,失败方向收紧)。
|
||
func (e *eventsAPI) EventPolicy() security.EventPolicy {
|
||
if e.sec != nil {
|
||
return e.sec.EventPolicy()
|
||
}
|
||
return security.DefaultEventPolicy()
|
||
}
|
||
|
||
func (e *eventsAPI) Publish(eventType string, payload map[string]interface{}) error {
|
||
if e.unavailable != "" {
|
||
return fmt.Errorf("events: %s", e.unavailable)
|
||
}
|
||
if d := e.checkPublish(eventType); !d.Allowed {
|
||
return fmt.Errorf("%s", d.Reason)
|
||
}
|
||
if payload == nil {
|
||
payload = map[string]interface{}{}
|
||
}
|
||
if d := e.checkSize(len(payload)); !d.Allowed {
|
||
return fmt.Errorf("%s", d.Reason)
|
||
}
|
||
|
||
// ★ 载荷出向审查:事件载荷会被其他订阅者(含 WebUI/状态面)读到,
|
||
// 插件顺手把凭据放进去就等于绕过凭据闸门。
|
||
e.bus.Publish(&events.Event{
|
||
Type: events.EventType(eventType),
|
||
Source: e.pluginName,
|
||
Payload: e.scrubOut(eventType, payload),
|
||
})
|
||
return nil
|
||
}
|
||
|
||
func (e *eventsAPI) checkPublish(t string) security.Decision {
|
||
if e.sec != nil {
|
||
return e.sec.CheckEventPublish(e.pluginName, t)
|
||
}
|
||
return security.DefaultEventPolicy().CheckEventPublish(e.pluginName, t)
|
||
}
|
||
|
||
func (e *eventsAPI) checkSubscribe(t string) security.Decision {
|
||
if e.sec != nil {
|
||
return e.sec.CheckEventSubscribe(e.pluginName, t)
|
||
}
|
||
return security.DefaultEventPolicy().CheckEventSubscribe(e.pluginName, t)
|
||
}
|
||
|
||
func (e *eventsAPI) checkSize(keys int) security.Decision {
|
||
if e.sec != nil {
|
||
return e.sec.CheckEventPayloadSize(keys)
|
||
}
|
||
return security.DefaultEventPolicy().CheckEventPayloadSize(keys)
|
||
}
|
||
|
||
// scrubOut 对载荷做一次出向脱敏(安全模块未装配时原样返回)。
|
||
func (e *eventsAPI) scrubOut(eventType string, payload map[string]interface{}) map[string]interface{} {
|
||
if e.sec == nil {
|
||
return payload
|
||
}
|
||
out := make(map[string]interface{}, len(payload))
|
||
for k, v := range payload {
|
||
if s, ok := v.(string); ok {
|
||
if clean, n := e.sec.PrepareOutbound(security.Inspection{
|
||
Direction: security.DirectionOutbound,
|
||
Stage: "event:" + eventType,
|
||
Source: e.pluginName,
|
||
}, s); n > 0 {
|
||
out[k] = clean
|
||
continue
|
||
}
|
||
}
|
||
out[k] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (e *eventsAPI) Subscribe(eventType string, handler func(map[string]interface{})) (func(), error) {
|
||
if e.unavailable != "" {
|
||
return nil, fmt.Errorf("events: %s", e.unavailable)
|
||
}
|
||
if d := e.checkSubscribe(eventType); !d.Allowed {
|
||
return nil, fmt.Errorf("%s", d.Reason)
|
||
}
|
||
if handler == nil {
|
||
return nil, fmt.Errorf("events: handler must not be nil")
|
||
}
|
||
unsub := e.bus.Subscribe(events.EventType(eventType), func(evt *events.Event) {
|
||
handler(e.scrubIn(eventType, evt.Payload))
|
||
})
|
||
e.unsubs = append(e.unsubs, unsub)
|
||
return unsub, nil
|
||
}
|
||
|
||
// scrubIn 对入向载荷做清洗(事件可能带其他模块写进来的内容)。
|
||
func (e *eventsAPI) scrubIn(eventType string, p map[string]interface{}) map[string]interface{} {
|
||
if e.sec == nil || p == nil {
|
||
return p
|
||
}
|
||
out := make(map[string]interface{}, len(p))
|
||
for k, v := range p {
|
||
if s, ok := v.(string); ok {
|
||
if clean, n := e.sec.ScrubInbound(security.Inspection{
|
||
Direction: security.DirectionInbound,
|
||
Stage: "event:" + eventType,
|
||
}, s); n > 0 {
|
||
out[k] = clean
|
||
continue
|
||
}
|
||
}
|
||
out[k] = v
|
||
}
|
||
return out
|
||
}
|
||
|
||
// stop 解除本插件建立的全部订阅(插件卸载时由宿主调用)。
|
||
func (e *eventsAPI) stop() {
|
||
for _, u := range e.unsubs {
|
||
u()
|
||
}
|
||
e.unsubs = nil
|
||
}
|