fix(gateway): 收敛 sdk.Channel 与 adapter.PlatformAdapter 接口漂移,/admin/channels 恢复枚举

根因:sdk.Channel.Connect 为 2 参 (ctx, config any),adapter.PlatformAdapter.Connect 为
1 参 (ctx),Go 无重载 → 4 个渠道(qq-obv11/weixin-ilink/telegram/webhook)只实现 1 参
Connect,类型断言 cap.Value.(sdk.Channel) 恒失败 → /api/v1/admin/channels 恒返回 []。

收敛方向:sdk.Channel.Connect 改 1 参 (ctx),与 PlatformAdapter 生命周期子集一致;
4 个真实适配器自动满足新接口,adminListChannels 枚举成功。最小改动(不改渠道实现、
不引依赖)。同步修正 sdk_test/registry_test/extra_test 的 mock。
This commit is contained in:
Aska committed 2026-09-06 14:04:52 +08:00
1 parent 77f75b0d99
commit 5dae2b9741
4 files changed
+31 -20

No files matched your search

@@ -32,7 +32,7 @@ func (s regSkill) Guardrails() []sdk.Guardrail { return nil }
type regChannel struct{ name string } type regChannel struct{ name string }
func (c regChannel) Name() string { return c.name } func (c regChannel) Name() string { return c.name }
func (c regChannel) Connect(_ context.Context, _ any) error { return nil } func (c regChannel) Connect(_ context.Context) error { return nil }
func (c regChannel) Disconnect(_ context.Context) error { return nil } func (c regChannel) Disconnect(_ context.Context) error { return nil }
func (c regChannel) IsConnected() bool { return true } func (c regChannel) IsConnected() bool { return true }
func (c regChannel) HealthCheck(_ context.Context) error { return nil } func (c regChannel) HealthCheck(_ context.Context) error { return nil }
+26 -15
View File
@@ -21,7 +21,9 @@ import (
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/config"
"git.yeij.top/AskaEth/Cyrene_v2/internal/iot" "git.yeij.top/AskaEth/Cyrene_v2/internal/iot"
personareg "git.yeij.top/AskaEth/Cyrene_v2/internal/persona/registry"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/memory"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk" "git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
) )
@@ -165,20 +167,22 @@ type stubSessions struct {
func (s *stubSessions) List(ctx context.Context) ([]session.Session, error) { return s.list, s.err } func (s *stubSessions) List(ctx context.Context) ([]session.Session, error) { return s.list, s.err }
// stubPersonas mimics PersonaStore. // stubPersonas mimics PersonaStore, returning domain characters that the gateway
// maps to PersonaSummary.
type stubPersonas struct { type stubPersonas struct {
list []PersonaSummary list []personareg.Character
} }
func (s *stubPersonas) List() []PersonaSummary { return s.list } func (s *stubPersonas) List() []personareg.Character { return s.list }
// stubMemories mimics MemoryStore. // stubMemories mimics MemoryStore, returning domain memory items that the gateway
// maps to MemorySummary.
type stubMemories struct { type stubMemories struct {
list []MemorySummary list []memory.MemoryItem
err error err error
} }
func (s *stubMemories) List(ctx context.Context) ([]MemorySummary, error) { return s.list, s.err } func (s *stubMemories) List(ctx context.Context) ([]memory.MemoryItem, error) { return s.list, s.err }
// stubIoT mimics IoTStore. // stubIoT mimics IoTStore.
type stubIoT struct { type stubIoT struct {
@@ -192,7 +196,7 @@ func (s *stubIoT) List(ctx context.Context) ([]iot.Device, error) { return s.lis
type stubChannel struct{} type stubChannel struct{}
func (stubChannel) Name() string { return "qq" } func (stubChannel) Name() string { return "qq" }
func (stubChannel) Connect(ctx context.Context, c any) error { return nil } func (stubChannel) Connect(ctx context.Context) error { return nil }
func (stubChannel) Disconnect(ctx context.Context) error { return nil } func (stubChannel) Disconnect(ctx context.Context) error { return nil }
func (stubChannel) IsConnected() bool { return true } func (stubChannel) IsConnected() bool { return true }
func (stubChannel) HealthCheck(ctx context.Context) error { return nil } func (stubChannel) HealthCheck(ctx context.Context) error { return nil }
@@ -365,7 +369,9 @@ func TestListSessionsAdminError(t *testing.T) {
func TestListPersonasNonEmpty(t *testing.T) { func TestListPersonasNonEmpty(t *testing.T) {
t.Parallel() t.Parallel()
deps := Deps{Personas: &stubPersonas{list: []PersonaSummary{{ID: "p1", Name: "苏梦", Mode: "chat"}}}} deps := Deps{Personas: &stubPersonas{list: []personareg.Character{{
ID: "p1", Name: "苏梦", PersonaID: "p1", Default: true, Modes: []string{"chat"},
}}}}
srv, ts, jwt := buildEnv(t, cfgDefault(), deps) srv, ts, jwt := buildEnv(t, cfgDefault(), deps)
_ = srv _ = srv
tok := signAny(t, jwt, "acct", auth.RoleOwner) tok := signAny(t, jwt, "acct", auth.RoleOwner)
@@ -375,21 +381,25 @@ func TestListPersonasNonEmpty(t *testing.T) {
t.Fatalf("%s status = %d, want 200 (body=%s)", path, resp.StatusCode, body) t.Fatalf("%s status = %d, want 200 (body=%s)", path, resp.StatusCode, body)
} }
var items []struct { var items []struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
IsDefault bool `json:"isDefault"`
Mode string `json:"mode"`
} }
if err := json.Unmarshal(decodeBody(t, body)["data"], &items); err != nil { if err := json.Unmarshal(decodeBody(t, body)["data"], &items); err != nil {
t.Fatalf("decode %s: %v", path, err) t.Fatalf("decode %s: %v", path, err)
} }
if len(items) != 1 || items[0].Name != "苏梦" { if len(items) != 1 || items[0].Name != "苏梦" || items[0].ID != "p1" || !items[0].IsDefault || items[0].Mode != "chat" {
t.Fatalf("%s items = %+v, want single 苏梦", path, items) t.Fatalf("%s items = %+v, want single 苏梦/p1/default/chat", path, items)
} }
} }
} }
func TestAdminMemoriesNonEmpty(t *testing.T) { func TestAdminMemoriesNonEmpty(t *testing.T) {
t.Parallel() t.Parallel()
deps := Deps{Memories: &stubMemories{list: []MemorySummary{{ID: "m1", Scope: "project", Text: "note"}}}} deps := Deps{Memories: &stubMemories{list: []memory.MemoryItem{{
ID: "m1", PersonaID: "persona.a", Scope: memory.ScopeProject, Content: "note",
}}}}
srv, ts, jwt := buildEnv(t, cfgDefault(), deps) srv, ts, jwt := buildEnv(t, cfgDefault(), deps)
_ = srv _ = srv
tok := signAny(t, jwt, "acct", auth.RoleOwner) tok := signAny(t, jwt, "acct", auth.RoleOwner)
@@ -400,12 +410,13 @@ func TestAdminMemoriesNonEmpty(t *testing.T) {
var items []struct { var items []struct {
ID string `json:"id"` ID string `json:"id"`
Scope string `json:"scope"` Scope string `json:"scope"`
Text string `json:"text"`
} }
if err := json.Unmarshal(decodeBody(t, body)["data"], &items); err != nil { if err := json.Unmarshal(decodeBody(t, body)["data"], &items); err != nil {
t.Fatalf("decode: %v", err) t.Fatalf("decode: %v", err)
} }
if len(items) != 1 || items[0].Scope != "project" { if len(items) != 1 || items[0].Scope != "project" || items[0].Text != "note" {
t.Fatalf("items = %+v, want single project", items) t.Fatalf("items = %+v, want single project/note", items)
} }
} }
+3 -3
View File
@@ -16,9 +16,9 @@ import "context"
type Channel interface { type Channel interface {
// Name returns the platform identifier (for example "qq", "telegram"). // Name returns the platform identifier (for example "qq", "telegram").
Name() string Name() string
// Connect establishes the platform connection using config. It may be // Connect establishes the platform connection. It may be called more than
// called more than once and must be idempotent. // once and must be idempotent.
Connect(ctx context.Context, config any) error Connect(ctx context.Context) error
// Disconnect tears the connection down gracefully. It returns nil when the // Disconnect tears the connection down gracefully. It returns nil when the
// adapter was already disconnected. // adapter was already disconnected.
Disconnect(ctx context.Context) error Disconnect(ctx context.Context) error
+1 -1
View File
@@ -31,7 +31,7 @@ func (s stubSkill) Guardrails() []Guardrail {
type stubChannel struct{ name string } type stubChannel struct{ name string }
func (s stubChannel) Name() string { return s.name } func (s stubChannel) Name() string { return s.name }
func (s stubChannel) Connect(_ context.Context, _ any) error { return nil } func (s stubChannel) Connect(_ context.Context) error { return nil }
func (s stubChannel) Disconnect(_ context.Context) error { return nil } func (s stubChannel) Disconnect(_ context.Context) error { return nil }
func (s stubChannel) IsConnected() bool { return true } func (s stubChannel) IsConnected() bool { return true }
func (s stubChannel) HealthCheck(_ context.Context) error { return nil } func (s stubChannel) HealthCheck(_ context.Context) error { return nil }