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);二者仅在适配器 // 确实要求时才出现,否则一律省略。 // // Pending/Ready/Reason(v0.68)报告 `connected=false` 时**到底是哪种没连上** // (见 admin_channel_connect_outcome.go 中的 connectOutcomeAware):pending = // 已在后台重试、当前未连上(面板读作「连接中」),ready = 已就绪、等待对端接入。 // 三者同样只在适配器确实自报时才出现 —— 没有这条状态面的平台逐字节不受影响。 type ChannelSummary struct { Name string `json:"name"` Connected bool `json:"connected"` NeedsRescan bool `json:"needsRescan,omitempty"` LastError string `json:"lastError,omitempty"` Pending bool `json:"pending,omitempty"` Ready bool `json:"ready,omitempty"` Reason string `json:"reason,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"` // ContextWindow 是该分组声明的**模型上下文窗口**(token)。0(= 键被省略, // 与 MaxTokens 同形)表示未声明:运行时按回落窗口保守推导并记一条 WARN, // 只有声明了真实窗口才能吃到按窗口推导的上下文预算。它是 .env 侧 // LLM_CONTEXT_WINDOW 的对偶(per-group 播种)。 ContextWindow int `json:"contextWindow,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,因此面板不必靠猜来解释 // 一次静默。 // // Urgency / UrgencyLowerBound / QuietHoursBypassed 回答「这一轮有多紧急、 // 是不是穿透了安静时段」。三者都**只在存档可证时**出现,缺席 ≠ 常规档位: // 取值来源与优先级见 admin_backthink_urgency.go —— 优先读每轮落库的**精确** // 档位;只有 `urgencyLowerBound:true` 的行才是**推导出的下限**(旧行按安静时段 // 窗口规则反推),窗口之外且无落库档位的旧行干脆不下发,而不是编一个 routine。 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"` // Urgency 是这一轮的紧急度档位(urgency 档位词汇:routine | warning | // critical)。**默认读到的是该轮实际生效的精确档位**(每一轮随记录落库); // 只有与 `urgencyLowerBound:true` 同时出现时才读作「≥ 该档(可能更高)」; // 无法证明时省略该键(面板显示「未知」,**不是**「例常」)。 Urgency string `json:"urgency,omitempty"` // UrgencyLowerBound 为 true 时表示 Urgency 是**推导出的下限**而不是精确档位: // 该行没有落库档位(`backthink_records.urgency` 列出现之前的旧行,或从未走到 // 档位判定点的行),值由安静时段窗口规则反推(见 admin_backthink_urgency.go)。 // 只在为 true 时出现:精确档位与「无档位」都不必在线上区分。客户端**不得**把 // 它显示成精确档位。 UrgencyLowerBound bool `json:"urgencyLowerBound,omitempty"` // QuietHoursBypassed 为 true 时表示这一轮**穿透了安静时段**(轮次时间落在 // 安静时段窗口内且该轮判定要推送)。只在为 true 时出现:安静时段之外的轮次 // 与「没穿透」不必在线上区分,缺席即「没有这件事」。 QuietHoursBypassed bool `json:"quietHoursBypassed,omitempty"` } // 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 的记忆切片。 // // 存储层的 List 已经过 status 闸(只给在册条目),因此面板记忆列表天然 // 不显示被软合并/被归档的行 —— 这里刻意**不加**第二次过滤,避免两处 // 口径各自漂移(design/23 §8/§10)。 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"` }