Files
Cyrene_v2/internal/services/gateway/admin.go
T
AskaEth a0abf0199a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway)!: 后台思考档位精确化(+ 面板三态 + 契约 v0.69/v0.70)
- gateway:优先读落库的 rec.Urgency(精确档位,与安静时段窗口无关);仅空串才回落到既有下限推导,并打 urgencyLowerBound 标记(**只在推导值上为 true**)
- 面板三态:精确(例常/紧急(高)/紧急·安全域)|下限(一律加 ≥ 前缀)|未知;下限悬停说明写清「为什么旧行只能是下限」
- 实时帧零协议改动:Record 的 JSON 键就是 urgency ⇒ 帧自然带上;wsParse 早已防御性读取(只加一处 urgencyLowerBound 防御读)
- 契约:v0.69(directMsgPerMin 透传修复,逐字照抄附录 A)与 v0.70(精确化 + 更正 §4.10(b)「实时帧不带这两键」)一起登记
- 变异实测:旧行不标下限 ⇒ 3 条测试红(结构体对比);前端去掉 ≥ ⇒ 渲染测试红
- ⚠️ 诚实边界(§7.3 已登记):实时旧记录刷新前显示「档位未知」、刷新后显示「≥ 例常」——同一条记录两路下的已知差异
2026-10-05 14:15:55 +08:00

238 lines
10 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"`
// 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 的记忆切片。
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"`
}