- gateway:优先读落库的 rec.Urgency(精确档位,与安静时段窗口无关);仅空串才回落到既有下限推导,并打 urgencyLowerBound 标记(**只在推导值上为 true**)
- 面板三态:精确(例常/紧急(高)/紧急·安全域)|下限(一律加 ≥ 前缀)|未知;下限悬停说明写清「为什么旧行只能是下限」
- 实时帧零协议改动:Record 的 JSON 键就是 urgency ⇒ 帧自然带上;wsParse 早已防御性读取(只加一处 urgencyLowerBound 防御读)
- 契约:v0.69(directMsgPerMin 透传修复,逐字照抄附录 A)与 v0.70(精确化 + 更正 §4.10(b)「实时帧不带这两键」)一起登记
- 变异实测:旧行不标下限 ⇒ 3 条测试红(结构体对比);前端去掉 ≥ ⇒ 渲染测试红
- ⚠️ 诚实边界(§7.3 已登记):实时旧记录刷新前显示「档位未知」、刷新后显示「≥ 例常」——同一条记录两路下的已知差异
238 lines
10 KiB
Go
238 lines
10 KiB
Go
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"`
|
||
}
|