mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
## ① 提示词:只有显式调用 outputch 才会送达用户(用户明确要求)
模型最常见的错误不是不会调工具,而是**以为返回文本就等于回复了** ——
在异步通道(qq/wechat/群聊)上那等于自言自语,用户永远收不到。
改动前【输出规则】开篇是「消息不会自动发送到对话来源通道,你必须自己决定
如何回复」,语气偏描述。改为开篇总述句:
**只有显式调用 `output_send__{通道名}` 才会把内容送达用户。**
返回纯文本只是在这次推理里说话,用户看不到 —— 除非是下面说的同步通道。
同时补上本次内核新增能力:payload 传数组可一次发多条。
## ② 契约值改名:ProxyAuthHomeAgent="homeagent" → ProxyAuthAgent="agent"
用户指示契约值一起改。常量名与字符串值同时改,但**保留旧值读取路径**:
ProxyAuthAgent = "agent" ← 新值
ProxyAuthAgentLegacy = "homeagent" ← 只用于兼容读取
ProxyAuthHomeAgent = ProxyAuthAgent (Deprecated 别名,存量源码可编译)
为什么必须保留:`Auth` 是**写进 plugin.json 的持久化声明值**。存量配置里
写着旧值 —— 本机实测 `/home/newqqagent/plugins/huawei_smarthome/plugin.json:19`
就是 `"auth": "homeagent"`。只认新值 ⇒ 该插件加载时被判为非法声明 ⇒
反代不生效,症状是「插件好好的但页面打不开」,且无任何报错指向改名。
### 归一化是安全关键
`EffectiveProxyAuth` 把**空串与旧值都归一化为 ProxyAuthAgent**。
判断点全都写成 `auth == ProxyAuthAgent`。若不归一化,就得每点写两遍比较
—— **漏一处就是鉴权绕过**:旧值被判为「非门户鉴权」⇒ 跳过 authorizeProxy
⇒ 请求直接透传给上游。所以兼容收在一个函数里,不散落在调用点。
### 顺带修掉两处隐患
1. `webui/proxy.go` 的裸字面量 `"homeagent"`(非法行占位)改为
`sdk.ProxyAuthAgent` —— 裸字面量在改名时会被漏掉,届时它悄悄变成
未知值,而未知值在旧代码里等于「无鉴权」。
2. 两处鉴权判断改用 `EffectiveProxyAuth(...)`,堵住旧值绕过。
## 判据
| 判据 | 覆盖 | 变异自证 |
|---|---|---|
| `TestSystemPromptRequiresExplicitOutputCall` | 总述句必须存在、必须说明「返回文本用户看不到」、同步通道是唯一例外、payload 可传数组 | 删掉总述句 ⇒ 红 |
| `TestProxyAuthLegacyValueStillAccepted` | 旧值仍被接受 + 必须归一化为新值 | 去掉归一化分支 ⇒ 「旧值应归一化为 "agent",实际 "homeagent"」红 |
## 验证
- `go build ./...` / `go vet ./...` 干净
- webui / internal-sdk / agent-core 全绿
- SDK 仓(独立提交 acd1166):`go build` + `go test ./...` 0 失败
122 lines
4.3 KiB
Go
122 lines
4.3 KiB
Go
// Package sdk —— 反代声明的内核侧登记表。
|
||
//
|
||
// 契约定义在公开 SDK(pubsdk.ProxyDef)。本文件只做两件事:
|
||
//
|
||
// 1. 把公开类型重新导出,让内核侧调用方只需 import 本包。
|
||
// 2. 维护**内置插件**的声明表(它们没有 plugin.json,扫目录发现不了)。
|
||
//
|
||
// 外部插件的声明由反代层直接读 plugin.json 得到,不经过这里 —— 那份声明
|
||
// 是静态的、插件没启动也可见,而内置插件的声明只能运行期拿到。两种来源在
|
||
// 反代层合并(见 webui.buildProxyTable)。
|
||
package sdk
|
||
|
||
import (
|
||
"strings"
|
||
"sync"
|
||
|
||
pubsdk "github.com/JianFeeeee/homeagentsdk/sdk"
|
||
)
|
||
|
||
// ProxyDef 是一条反代声明(见 pubsdk.ProxyDef 的完整文档,含「单一入口原则」)。
|
||
// 与 ConfigDef / ChannelDef / ToolDef 同族:SDK 定契约,内核实现行为。
|
||
type ProxyDef = pubsdk.ProxyDef
|
||
|
||
// 鉴权与 Host 规范化的常量/函数转发(调用方不必两处 import)。
|
||
const (
|
||
// ProxyAuthAgent 表示由内核统一保护(门户会话或 X-API-Key)。
|
||
ProxyAuthAgent = pubsdk.ProxyAuthAgent
|
||
ProxyAuthNone = pubsdk.ProxyAuthNone
|
||
|
||
// ProxyAuthAgentLegacy 是旧值 "homeagent":存量 plugin.json 里写的仍是它。
|
||
// 只用于兼容读取;判断时一律先过 EffectiveProxyAuth 归一化。
|
||
ProxyAuthAgentLegacy = pubsdk.ProxyAuthAgentLegacy
|
||
|
||
// ProxyAuthHomeAgent 是 ProxyAuthAgent 的旧名,仅为存量源码保留。
|
||
//
|
||
// Deprecated: 用 ProxyAuthAgent。
|
||
ProxyAuthHomeAgent = pubsdk.ProxyAuthHomeAgent
|
||
)
|
||
|
||
// ValidateProxyDef 校验一条声明,返回人类可读的错误(合法时为空)。
|
||
func ValidateProxyDef(d ProxyDef) string { return pubsdk.ValidateProxyDef(d) }
|
||
|
||
// EffectiveProxyAuth 返回生效的鉴权模式(空串与旧值都归一化为 ProxyAuthAgent)。
|
||
func EffectiveProxyAuth(auth string) string { return pubsdk.EffectiveProxyAuth(auth) }
|
||
|
||
// ValidProxyAuth 判断鉴权取值是否合法。
|
||
func ValidProxyAuth(auth string) bool { return pubsdk.ValidProxyAuth(auth) }
|
||
|
||
// NormalizeProxyHost 由插件名推导默认的 Host 标签。
|
||
func NormalizeProxyHost(plugin string) string { return pubsdk.NormalizeProxyHost(plugin) }
|
||
|
||
// ValidProxyHostLabel 校验子域名标签是否合法(DNS label 规则)。
|
||
func ValidProxyHostLabel(label string) bool { return pubsdk.ValidProxyHostLabel(label) }
|
||
|
||
// 内置插件(编译进内核、无 plugin.json)的反代声明表。
|
||
//
|
||
// 版本号 builtinProxyVer 每次变更自增。反代层据此判断缓存的路由表是否过期 ——
|
||
// 比每次请求都重新聚合一遍便宜得多。
|
||
var (
|
||
builtinProxyMu sync.RWMutex
|
||
builtinProxyVer int64
|
||
builtinProxyDefs = map[string][]ProxyDef{}
|
||
)
|
||
|
||
// RegisterBuiltinProxy 登记一个内置插件的服务声明(由 RegisterProxy 转发)。
|
||
//
|
||
// ProxyDef.Name 由调用方保证非空(RegisterProxy 会在缺失时兜底为 "service")。
|
||
func RegisterBuiltinProxy(plugin, name string, d ProxyDef) {
|
||
if plugin == "" || strings.TrimSpace(d.Target) == "" {
|
||
return
|
||
}
|
||
if d.Name == "" {
|
||
d.Name = name
|
||
}
|
||
if d.Name == "" {
|
||
d.Name = "service"
|
||
}
|
||
builtinProxyMu.Lock()
|
||
defer builtinProxyMu.Unlock()
|
||
// 同名重复登记(如自动重启后再次 Start)视为刷新,不重复累积。
|
||
list := builtinProxyDefs[plugin]
|
||
for i := range list {
|
||
if list[i].Name == d.Name {
|
||
list[i] = d
|
||
builtinProxyVer++
|
||
return
|
||
}
|
||
}
|
||
builtinProxyDefs[plugin] = append(list, d)
|
||
builtinProxyVer++
|
||
}
|
||
|
||
// ClearBuiltinProxyDefs 清除某插件的声明(插件停止/卸载时调用)。
|
||
func ClearBuiltinProxyDefs(plugin string) {
|
||
builtinProxyMu.Lock()
|
||
defer builtinProxyMu.Unlock()
|
||
if _, ok := builtinProxyDefs[plugin]; ok {
|
||
delete(builtinProxyDefs, plugin)
|
||
builtinProxyVer++
|
||
}
|
||
}
|
||
|
||
// BuiltinProxyDefs 返回内置声明的快照(plugin → defs)。
|
||
func BuiltinProxyDefs() map[string][]ProxyDef {
|
||
builtinProxyMu.RLock()
|
||
defer builtinProxyMu.RUnlock()
|
||
out := make(map[string][]ProxyDef, len(builtinProxyDefs))
|
||
for k, v := range builtinProxyDefs {
|
||
cp := make([]ProxyDef, len(v))
|
||
copy(cp, v)
|
||
out[k] = cp
|
||
}
|
||
return out
|
||
}
|
||
|
||
// BuiltinProxyVersion 是声明表的版本号,供反代层判断缓存是否过期。
|
||
func BuiltinProxyVersion() int64 {
|
||
builtinProxyMu.RLock()
|
||
defer builtinProxyMu.RUnlock()
|
||
return builtinProxyVer
|
||
}
|