mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
fix(cabi): output_send 等待真实发送结果,消除假成功(plan 11.1 / Part 0.1)
根因:CORE_REGISTER_OUTPUT_CH handler 无条件返回 {status:queued}+err=nil,
模型永远收到「已发送」,实际失败(如 meta 缺 user_id)只写日志,模型无法感知不会重试。
现网近 7 天成功 44 次、失败 2 次全部谎报成功。
改动:
- loader.go: 新增 awaitOutputResult(+可注入版 awaitOutputResultWith)+ outputSendTimeout=10s
goroutine 执行 cgo 发送 + 带超时 channel 等结果 → sent / error / unconfirmed 三态
handler 由 executeOutputSendTool 从 Go 侧调起,非 cgo 栈,不构成 cgo 嵌套
- output.go: executeOutputSendTool 识别 unconfirmed|queued,回报「发送结果未确认」而非「已发送」
- output_test.go: Success/Failure/Timeout 三用例
验证: go build exit 0; go test ./internal/plugin/... ./internal/agent/... 全绿
接口冻结: git diff third_party/homeagent-sdk/sdk/ 为空
This commit is contained in:
@ -67,6 +67,17 @@ func (a *Agent) executeOutputSendTool(tc agentAPI.ToolCall) string {
|
||||
if err != nil {
|
||||
return fmt.Sprintf("通过 [%s] 通道发送失败: %v", channel, err)
|
||||
}
|
||||
// 通道可能回报「未确认」(已提交但超时未拿到发送确认)——此时不能对模型
|
||||
// 谎报「已发送」,否则模型不会重试/核实(plan.md 11.1)。
|
||||
if m, ok := result.(map[string]interface{}); ok {
|
||||
if status, _ := m["status"].(string); status == "unconfirmed" || status == "queued" {
|
||||
note, _ := m["note"].(string)
|
||||
if note == "" {
|
||||
note = "发送已提交但未收到通道确认,结果未知"
|
||||
}
|
||||
return fmt.Sprintf("[%s] 通道发送结果未确认:%s", channel, note)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("已通过 [%s] 通道发送: %v", channel, result)
|
||||
}
|
||||
|
||||
|
||||
@ -52,11 +52,17 @@ import (
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
// outputSendTimeout 是 output_send 等待通道真实发送确认的超时。
|
||||
// 超过该时间仍未收到插件确认,返回 unconfirmed(结果未知)而非谎报成功。
|
||||
// (plan.md 11.1)
|
||||
const outputSendTimeout = 10 * time.Second
|
||||
|
||||
var (
|
||||
pluginMap sync.Map // int32 pluginID → *pluginState
|
||||
nextID int32
|
||||
@ -255,6 +261,49 @@ func pluginInvokeTool(pluginID int32, name, argsJSON string) (string, error) {
|
||||
return C.GoString(result), nil
|
||||
}
|
||||
|
||||
// awaitOutputResult 在 goroutine 内执行真正的 cgo 发送调用,并等待其结果:
|
||||
// - 发送成功 → {status: sent}
|
||||
// - 发送失败 → 返回 error(模型可感知并重试),不再像旧实现那样谎报成功
|
||||
// - 超时未确认 → {status: unconfirmed}(结果未知,不谎报成功/失败)
|
||||
//
|
||||
// 为什么用 goroutine + channel 而不是直接同步调用:pluginInvokeOutput 是 cgo 调用,
|
||||
// 不能嵌套在 cgo 栈上执行(cgo within cgo 会崩溃)。本 handler 由 executeOutputSendTool
|
||||
// 从 Go 侧调起(不在 cgo 栈内),所以这里启动子 goroutine 执行 cgo 调用并等待其结果,
|
||||
// 不构成嵌套。
|
||||
//
|
||||
// 修复 plan.md 11.1:旧实现无条件返回 {status: queued} + err=nil,模型永远收到「已发送」
|
||||
// 而实际失败(如 meta 缺 user_id)只写日志,模型无法感知、不会重试。
|
||||
func awaitOutputResult(pid int32, channel, argsJSON string) (interface{}, error) {
|
||||
return awaitOutputResultWith(pid, channel, argsJSON, pluginInvokeOutput, outputSendTimeout)
|
||||
}
|
||||
|
||||
// awaitOutputResultWith 是 awaitOutputResult 的可注入版本(供单测替换 cgo 发送与超时)。
|
||||
func awaitOutputResultWith(
|
||||
pid int32,
|
||||
channel, argsJSON string,
|
||||
invoke func(pluginID int32, channel, payload string) error,
|
||||
timeout time.Duration,
|
||||
) (interface{}, error) {
|
||||
resCh := make(chan error, 1)
|
||||
go func() { resCh <- invoke(pid, channel, argsJSON) }()
|
||||
select {
|
||||
case err := <-resCh:
|
||||
if err != nil {
|
||||
log.Printf("[dispatch] output %s failed: %v", channel, err)
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("[dispatch] output %s OK", channel)
|
||||
return map[string]interface{}{"status": "sent"}, nil
|
||||
case <-time.After(timeout):
|
||||
// 超时未确认:插件仍在后台发送,结果未知。不谎报成功,也不谎报失败。
|
||||
log.Printf("[dispatch] output %s 等待确认超时(%s),插件仍在后台发送", channel, timeout)
|
||||
return map[string]interface{}{
|
||||
"status": "unconfirmed",
|
||||
"note": fmt.Sprintf("发送已提交但 %s 内未收到通道确认,结果未知;如需确认请查询该通道状态", timeout),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func pluginInvokeOutput(pluginID int32, channel, payload string) error {
|
||||
v, ok := pluginMap.Load(pluginID)
|
||||
if !ok {
|
||||
@ -456,18 +505,13 @@ func go_core_dispatch(methodID C.int, ctx unsafe.Pointer, s1, s2, s3 *C.char, i1
|
||||
}
|
||||
}
|
||||
s.RegisterOutputChannel(chName, n1, a2, chDef, func(args map[string]interface{}) (interface{}, error) {
|
||||
// Output is async: return immediately, send in background
|
||||
// to avoid nested cgo calls (cgo within cgo can crash)
|
||||
go func() {
|
||||
argsJSON, _ := json.Marshal(args)
|
||||
log.Printf("[dispatch] async output %s/%s args=%s", ps.name, chName, string(argsJSON))
|
||||
if err := pluginInvokeOutput(pid, chName, string(argsJSON)); err != nil {
|
||||
log.Printf("[dispatch] async output %s/%s failed: %v", ps.name, chName, err)
|
||||
} else {
|
||||
log.Printf("[dispatch] async output %s/%s OK", ps.name, chName)
|
||||
}
|
||||
}()
|
||||
return map[string]interface{}{"status": "queued"}, nil
|
||||
// 发送在 goroutine 内进行(cgo 调用不能嵌套在 cgo 栈上,否则可能崩溃),
|
||||
// 但调用方必须拿到真实结果:本 handler 由 executeOutputSendTool 从 Go 侧
|
||||
// 调起,不在 cgo 栈内,因此这里等待 goroutine 的结果不构成 cgo 嵌套。
|
||||
// (plan.md 11.1)
|
||||
argsJSON, _ := json.Marshal(args)
|
||||
log.Printf("[dispatch] output %s/%s args=%s", ps.name, chName, string(argsJSON))
|
||||
return awaitOutputResult(pid, chName, string(argsJSON))
|
||||
})
|
||||
return 0
|
||||
|
||||
|
||||
49
internal/plugin/cabi/output_test.go
Normal file
49
internal/plugin/cabi/output_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package cabi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// awaitOutputResult 的 decision 核心:
|
||||
|
||||
func TestAwaitOutputResult_Success(t *testing.T) {
|
||||
res, err := awaitOutputResultWith(0, "qq", `{"x":1}`, func(pid int32, ch, args string) error {
|
||||
return nil
|
||||
}, outputSendTimeout)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
m, _ := res.(map[string]interface{})
|
||||
if m["status"] != "sent" {
|
||||
t.Fatalf("expected status=sent, got %v", m["status"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwaitOutputResult_Failure(t *testing.T) {
|
||||
_, err := awaitOutputResultWith(0, "qq", `{}`, func(pid int32, ch, args string) error {
|
||||
return errors.New("meta 中需要 group_id 或 user_id 字段")
|
||||
}, outputSendTimeout)
|
||||
if err == nil {
|
||||
t.Fatal("expected error on failed send, got nil (旧实现会谎报成功)")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "需要 group_id") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAwaitOutputResult_Timeout(t *testing.T) {
|
||||
res, err := awaitOutputResultWith(0, "qq", `{}`, func(pid int32, ch, args string) error {
|
||||
time.Sleep(2 * time.Second) // 模拟插件发送迟迟不确认
|
||||
return nil
|
||||
}, 50*time.Millisecond)
|
||||
if err != nil {
|
||||
t.Fatalf("unconfirmed 不应返回 error,got %v", err)
|
||||
}
|
||||
m, _ := res.(map[string]interface{})
|
||||
if m["status"] != "unconfirmed" {
|
||||
t.Fatalf("expected status=unconfirmed, got %v", m["status"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user