Files
Cyrene_v2/internal/services/gateway/admin.go
T
AskaEth decf59c1b4
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway,admin-web): Skill 运行时安装 + backthink 打扰原因 + 建提醒入口(P0-19/P0-20/P0-22,审计 §4.3)
- P0-19:POST /admin/plugins/install 支持 `kind:"skill"`(此前一律 400)——yaml 或 path 二选一、
  name 一致性校验、显式错误码(bad_request/name_mismatch/skill_parse_failed/name_conflict)、
  装后以 kind:"skill" 出现在 GET /admin/plugins;**文件来源解析错误绝不回显文件内容**(变异验证实测确认
  该泄露向量真实存在:yaml 解码器会带出出错值前几字符 ⇒ 只回显路径 + 通用原因,细节进日志)
- P0-20:AdminBackthinkItem 补 route/detail/bypassed(DB 早已存,零迁移),面板 Backthink 展示
- P0-22:新增 POST /admin/automations/reminders(owner-only + 审计),at/message 必填、RFC3339 校验、
  409 already_exists;过去时间不作 400(下次轮询立即触发)
- 顺手修真缺陷:新建 id 原用 time.Now().UnixNano(),Windows 时钟粒度粗到毫秒 ⇒ 并行创建撞 id 得假 409;
  改用 crypto/rand 128 位 hex(仓库既有 skills/store.go 范式)
- 复核:gateway 包 build/vet/测试绿 + 前端 tsc 0 错;新增 12 用例(含 8 条非法输入显式码);
  **4 条变异反证**均已回滚
- 未做(登记):WS 帧侧的 route/detail 未在前端展示(`lib/wsParse.ts` 3 行,需放行);skill 未落
  skills.Store(重启后消失,注入点在 cmd/cyrene);提醒无删除入口(CancelReminder 已存在、无路由)
2026-10-04 14:21:41 +08:00

212 lines
8.2 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 gateway
import "context"
// backthinkListLimit 是未指定 limit 时管理面板索取的
// 最近 think 记录条数。
const backthinkListLimit = 50
// PluginSummary 是管理列表中一个能力/插件的线上结构。
type PluginSummary struct {
Name string `json:"name"`
Kind string `json:"kind"`
Description string `json:"description"`
AllowInGroup bool `json:"allowInGroup"`
AllowInStranger bool `json:"allowInStranger"`
}
// ChannelSummary 是一个已注册渠道适配器的线上结构。Accounts
// 承载该平台已配置的账号(已脱敏,不含原始 token),使
// 面板能展示并编辑账号配置;当 s.channels 未接线
// 或该平台没有已配置账号时省略。
//
// NeedsRescan/LastError(v0.39)报告适配器需要主人重新授权
// (见 admin_channels_rescan.go 中的 rescanAware);二者仅在适配器
// 确实要求时才出现,否则一律省略。
type ChannelSummary struct {
Name string `json:"name"`
Connected bool `json:"connected"`
NeedsRescan bool `json:"needsRescan,omitempty"`
LastError string `json:"lastError,omitempty"`
Accounts []AdminChannelAccount `json:"accounts,omitempty"`
}
// policyRequest 是 POST /api/v1/admin/plugins/policy 的请求体。
type policyRequest struct {
Name string `json:"name"`
AllowInGroup bool `json:"allowInGroup"`
AllowInStranger bool `json:"allowInStranger"`
}
// AdminLlmReasoning 是一个 provider 组的推理开关形状声明(models.json 的
// `reasoning` 块的线上投影)。kind 为空串表示「未声明形状」(等同 auto:
// 按模型名/baseURL 推断,有歧义就不下发),而不是「后端没报告」——
// 后者由整个字段缺失表示。
type AdminLlmReasoning struct {
Kind string `json:"kind"`
}
// AdminLlmProvider 是管理配置中一个 LLM provider 的线上结构。
// API key 刻意缺位:它绝不返回给客户端。
//
// 这里列出的每个可编辑字段都必须在读侧如实回填:面板是「读回来 → 改一处 →
// 整组提交」,读侧少一个键就等于保存时把它抹掉。
type AdminLlmProvider struct {
ID string `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
BaseURL string `json:"baseUrl"`
Model string `json:"model"`
// Multimodal = **主模型自己能否收图**(裁决 14.4.1)。它不表示「本部署
// 有视觉能力」:图片识别/OCR 工具用的模型由 .env 的 LLM_VISION_MODEL /
// LLM_OCR_MODEL 单独配置,与这里无关。
Multimodal bool `json:"multimodal"`
Enabled bool `json:"enabled"`
Fallback string `json:"fallback,omitempty"`
// MaxTokens 是该分组自身的 token 预算(0 = 未设置;此时运行时
// 在最后一个 fallback 步骤回退到 LLM_FALLBACK_MAX_TOKENS,
// 再回退到适配器默认值)。
MaxTokens int `json:"maxTokens,omitempty"`
// Reasoning 是该组的推理开关形状(裁决 03.4.2 / 08.4.1)。始终下发
// (kind 可能为空串),使面板能把「未声明」当成一种明确状态回填。
Reasoning *AdminLlmReasoning `json:"reasoning"`
// PersonaModels 是 persona → 模型名的路由表(裁决 03.4.1),键为该组内
// 的 persona id;空/缺失 = 该组没有 per-persona 模型绑定。
PersonaModels map[string]string `json:"personaModels,omitempty"`
// PersonaProviders 是 persona → wire 协议的覆盖表,取值只能是本 build
// 承认的协议名(见 AdminLlmConfig.PersonaProviderOptions)。
PersonaProviders map[string]string `json:"personaProviders,omitempty"`
}
// AdminLlmConfig 是 GET /api/v1/admin/llm 的线上结构。
//
// ReasoningKinds / PersonaProviderOptions 是**面板表单选项的取值域**:它们由
// 配置层的常量清单直接生成,因此面板给出的候选与后端会接受的取值永远一致
// (面板不硬编码自己的一套枚举)。
type AdminLlmConfig struct {
Providers []AdminLlmProvider `json:"providers"`
// ReasoningKinds 是 responses 中 reasoning.kind 的合法取值(不含空串)。
ReasoningKinds []string `json:"reasoningKinds"`
// PersonaProviderOptions 是 personaProviders 值的合法取值(协议名)。
PersonaProviderOptions []string `json:"personaProviderOptions"`
}
// AdminAutomationRecord 是管理列表中一条自动化/提醒的线上结构。
// 对 cron 类型,Cron 承载 cron 表达式;lastRun 与
// nextRun 在值未知时省略。
type AdminAutomationRecord struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
Cron string `json:"cron,omitempty"`
Enabled bool `json:"enabled"`
LastRun string `json:"lastRun,omitempty"`
NextRun string `json:"nextRun,omitempty"`
}
// AdminUserRecord 是管理列表中一个账号/API key 的线上结构。
type AdminUserRecord struct {
ID string `json:"id"`
Username string `json:"username"`
Role string `json:"role"`
CreatedAt string `json:"createdAt,omitempty"`
}
// AdminBackthinkItem 是管理监控列表中一条后台思考记录的
// 线上结构。
//
// Pushed 只回答「发没发出」;Route/Detail/Bypassed(design/12 §4、
// design/10 §3 #12)回答「走了哪条路、为什么打扰/为什么不打扰」——
// 它们直接来自后端已存的 PushResult,因此面板不必靠猜来解释
// 一次静默。
type AdminBackthinkItem struct {
ID string `json:"id"`
Trigger string `json:"trigger"`
Conclusion string `json:"conclusion,omitempty"`
ShouldNotify bool `json:"shouldNotify"`
Pushed bool `json:"pushed"`
// Route 是投递路由:none | rate_limited | active_client | im_bridge。
Route string `json:"route,omitempty"`
// Detail 是打扰决策或投递的简短原因(如 quiet hours、无活跃客户端)。
Detail string `json:"detail,omitempty"`
// Bypassed 表示这是事件驱动的推送、绕过了限频闸("该发就发")。
Bypassed bool `json:"bypassed"`
At int64 `json:"at"`
}
// AdminRagDoc 是管理 RAG 列表中一个知识库文档(项目索引
// 条目)的线上结构。
type AdminRagDoc struct {
ID string `json:"id"`
Name string `json:"name"`
Desc string `json:"desc,omitempty"`
Chunks int `json:"chunks"`
Embedded bool `json:"embedded"`
}
// AdminRagHit 是一条 RAG 检索命中的线上结构。
type AdminRagHit struct {
DocID string `json:"docId"`
DocName string `json:"docName"`
Chapter string `json:"chapter,omitempty"`
Text string `json:"text"`
Score float64 `json:"score"`
Semantic bool `json:"semantic"`
}
// AdminRagResponse 是 GET /api/v1/admin/rag 的线上结构。Hits 仅
// 在提供了查询参数时出现,因此调用方能区分「未请求搜索」
// 与「搜索了但没找到」。
type AdminRagResponse struct {
Documents []AdminRagDoc `json:"documents"`
Hits []AdminRagHit `json:"hits,omitempty"`
}
// ragQueryRequest 是 POST /api/v1/admin/rag/query 的请求体。
type ragQueryRequest struct {
Query string `json:"query"`
}
// collectSessions 从已配置的存储返回非 nil 的会话切片。
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 从已配置的存储返回非 nil 的记忆切片。
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 是一个 IoT 设备的线上结构。
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"`
}