Files
Cyrene_v2/internal/services/gateway/admin.go
T
Aska 04bfa2c7a7
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway): admin 列表端点接真实数据——persona/session/memory 补 List() 并注入 gateway.Deps
根因:gateway.Server 定义 SessionStore/PersonaStore/MemoryStore 接口但生产从未满足:
- 底层 manager 无 List() 方法;
- wireFrontend 的 gateway.Deps{} 未传 Session/Personas/Memories。
→ /api/v1/sessions|personas 与 /api/v1/admin/{sessions,personas,memories} 恒返回 []。

改动:
- persona/registry:Registry.List() []Character(并发安全、值拷贝)。
- channel/session:SessionManager.List(ctx)(查 channel_sessions 表+按 actor_role 重建)。
- runtime/memory:Store.List(ctx)(全表,复用 scanItems)。
- gateway 三 Store 接口改为返回域类型(Character/MemoryItem),映射统一放 collect* 侧。
- cmd/cyrene/wire.go:Deps 补传 Session/Personas/Memories。
- .gitignore:cyrene→/cyrene(锚定根,不再误伤 cmd/cyrene 目录)。

实机验证:/api/v1/admin/personas 返回 {id:cyrene,name:昔涟,isDefault:true}。
2026-09-06 14:05:34 +08:00

173 lines
5.5 KiB
Go

package gateway
import (
"context"
"encoding/json"
"errors"
"net/http"
capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
)
// PluginSummary is the wire shape of a capability/plugin in the admin list.
type PluginSummary struct {
Name string `json:"name"`
Kind string `json:"kind"`
Description string `json:"description"`
AllowInGroup bool `json:"allowInGroup"`
AllowInStranger bool `json:"allowInStranger"`
}
// ChannelSummary is the wire shape of a registered channel adapter.
type ChannelSummary struct {
Name string `json:"name"`
Connected bool `json:"connected"`
}
// policyRequest is the POST /api/v1/admin/plugins/policy body.
type policyRequest struct {
Name string `json:"name"`
AllowInGroup bool `json:"allowInGroup"`
AllowInStranger bool `json:"allowInStranger"`
}
// adminListPersonas lists persona summaries (admin read).
func (s *Server) adminListPersonas(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.collectPersonas())
}
// adminListSessions lists channel sessions (admin read).
func (s *Server) adminListSessions(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.collectSessions(r.Context()))
}
// adminListMemories lists memory entries (admin read).
func (s *Server) adminListMemories(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, s.collectMemories(r.Context()))
}
// adminListPlugins lists every registered capability (admin read).
func (s *Server) adminListPlugins(w http.ResponseWriter, r *http.Request) {
out := make([]PluginSummary, 0)
if s.caps != nil {
for _, cap := range s.caps.List("") {
out = append(out, PluginSummary{
Name: cap.Name,
Kind: string(cap.Kind),
Description: cap.Description,
AllowInGroup: cap.Policy.AllowInGroup,
AllowInStranger: cap.Policy.AllowInStranger,
})
}
}
writeJSON(w, http.StatusOK, out)
}
// adminListChannels lists the registered channel adapters (admin read).
func (s *Server) adminListChannels(w http.ResponseWriter, r *http.Request) {
out := make([]ChannelSummary, 0)
if s.caps != nil {
for _, cap := range s.caps.List(capreg.KindChannel) {
ch, ok := cap.Value.(sdk.Channel)
if !ok {
continue
}
out = append(out, ChannelSummary{Name: ch.Name(), Connected: ch.IsConnected()})
}
}
writeJSON(w, http.StatusOK, out)
}
// adminListIoT lists IoT devices (admin read).
func (s *Server) adminListIoT(w http.ResponseWriter, r *http.Request) {
out := make([]iotDeviceSummary, 0)
if s.iot != nil {
devices, err := s.iot.List(r.Context())
if err != nil {
s.log.Error("list iot", "err", err)
writeError(w, apperr.Wrap(apperr.ErrInternal, "iot_failed", "failed to list devices"))
return
}
for _, d := range devices {
out = append(out, iotDeviceSummary{ID: d.ID, Name: d.Name, Type: d.Type, State: d.State, Online: d.Online})
}
}
writeJSON(w, http.StatusOK, out)
}
// adminSetPolicy upserts the reachability markers for a capability. It is the
// audited admin write example (owner-only, enforced by the route registration).
func (s *Server) adminSetPolicy(w http.ResponseWriter, r *http.Request) {
var req policyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid request body"))
return
}
if req.Name == "" {
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "name is required"))
return
}
if s.caps == nil {
s.auditClaims(r, "policy.set", "error")
writeError(w, apperr.Wrap(apperr.ErrInternal, "policy_unavailable", "capability service is not configured"))
return
}
err := s.caps.SetPolicy(req.Name, capreg.ToolPolicy{AllowInGroup: req.AllowInGroup, AllowInStranger: req.AllowInStranger})
if err != nil {
s.auditClaims(r, "policy.set", "error")
if errors.Is(err, capreg.ErrNotFound) {
writeError(w, apperr.Wrap(apperr.ErrNotFound, "not_found", "capability %q not found", req.Name))
return
}
writeError(w, apperr.Wrap(apperr.ErrInternal, "policy_failed", "failed to set policy"))
return
}
s.auditClaims(r, "policy.set", "ok")
writeJSON(w, http.StatusOK, map[string]any{"name": req.Name, "allowInGroup": req.AllowInGroup, "allowInStranger": req.AllowInStranger})
}
// collectSessions returns a non-nil session slice from the configured store.
func (s *Server) collectSessions(ctx context.Context) []SessionSummary {
out := make([]SessionSummary, 0)
if s.sessions == nil {
return out
}
list, err := s.sessions.List(ctx)
if err != nil {
s.log.Error("list sessions", "err", err)
return out
}
for _, sess := range list {
out = append(out, summaryOfSession(sess))
}
return out
}
// collectMemories returns a non-nil memory slice from the configured store.
func (s *Server) collectMemories(ctx context.Context) []MemorySummary {
out := make([]MemorySummary, 0)
if s.memories == nil {
return out
}
list, err := s.memories.List(ctx)
if err != nil {
s.log.Error("list memories", "err", err)
return out
}
for _, it := range list {
out = append(out, MemorySummary{ID: it.ID, Scope: string(it.Scope), Text: it.Content})
}
return out
}
// iotDeviceSummary is the wire shape of an IoT device.
type iotDeviceSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
State map[string]any `json:"state"`
Online bool `json:"online"`
}