Files
HomeAgent/third_party/homeagent-sdk/sdk/events.go
JianFeeeee 4af07d08fe fix(memory,llm): L2 预填充并入召回主路 + 回退换 provider 时换 model(视觉失效真因)
## 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
2026-10-27 11:49:26 +08:00

67 lines
3.0 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 sdk
// 受限事件总线(EventsAPI)。
//
// ## 为什么现在才敢暴露
//
// 事件总线是内核内部机制:它连接内核模块、状态面、WebUI 与插件。
// 早先不给插件的理由是**没有边界**——一给就是"任何插件能发任何事件、能订阅任何事件",
// 等于把内核内部的同步面变成公共 API,且没有任何审查。
//
// 2026-10 之后内核侧有了 security 模块(凭据闸门 + 内容审查 + 审计),
// 于是可以只开**受限的一条缝**:
//
// · 发布:只能用白名单事件类型,且只能在自己的命名空间下扩展
// · 载荷:出向经 security 审查(明文凭据会被换成 @{别名}),并限制大小
// · 订阅:只能订阅白名单里的只读事件
//
// ## 插件能做什么
//
// Publish("skill_detected", {...}) // 交接类事件(兼容层 → skillmgr)
// Publish("plugin.<自己名字>.<任意>", {...}) // 自己的命名空间,供插件间协作
// Subscribe("tools_ready", handler) // 等内核就绪再做事的入口
//
// 越界会**报错**而不是静默丢弃:静默丢弃会让插件以为发成功了。
// EventsAPI 是插件可见的事件总线。
//
// 它**不是** *events.Bus 的投影:这是一个有边界、有审查的接口。
type EventsAPI interface {
// Publish 发布一个事件。
//
// eventType 必须在白名单内(见 AllowedPublish),或形如
// "plugin.<本插件名>.<后缀>"。载荷大小与键数有上限。
Publish(eventType string, payload map[string]interface{}) error
// Subscribe 订阅一个白名单内的事件(只读面)。
//
// 返回的 cancel 在插件 Stop 时调用可解除订阅;宿主卸载插件时也会自动解除。
// handler 的 panic 会被总线隔离,不影响其他订阅者。
Subscribe(eventType string, handler func(payload map[string]interface{})) (cancel func(), err error)
// AllowedPublish 返回当前可发布的事件类型(白名单 + 自己的命名空间前缀)。
AllowedPublish() []string
// AllowedSubscribe 返回当前可订阅的事件类型。
AllowedSubscribe() []string
}
// 常用事件名(**只是名字,不是授权表**)。
//
// 授权表由内核 security 模块持有(`security.EventPolicy`)并可随配置调整;
// SDK 侧只声明"这些名字存在",避免插件作者拼错字符串。
//
// 是否允许你用某个名字,以运行时 `Events().Publish/Subscribe` 的返回为准:
// 越界会**报错**,不会静默丢弃。
const (
// EventNameSkillDetected:兼容层把发现的纯 SKILL 条目交给 skillmgr 接管。
// (职责链交接,不是执行指令)
EventNameSkillDetected = "skill_detected"
// EventNameToolsReady:全部插件加载完、工具注册齐(只读订阅)。
EventNameToolsReady = "tools_ready"
// EventNameSecurityReady:安全闸门装配完成(只读订阅)。
EventNameSecurityReady = "security_ready"
// EventNameDeviceReady:工具/设备数据面就绪(只读订阅)。
EventNameDeviceReady = "device_ready"
)