Files
Cyrene_v2/internal/services/gateway/admin.go
T
AskaEth 47e56ca13c feat(admin): 面板暴露 contextWindow(可见可改)+ DocumentRef 语义注释同步
- 面板读写面与 maxTokens 逐处对齐:读 DTO 增 contextWindow(omitempty)、写请求 ContextWindow *int(缺失=保留、显式 0=未声明同义、负数 400)、回写 models.json;负数红线仍由 runtimeconfig.Group.Validate 唯一裁决,网关守卫只做「更早失败 + 面板可读字段名」。前端:类型 + 表单字段(提交前 /^\d+$/ 校验)+ 分组行角标 + Notice + 目录候选用早已解析却没人用的 contextLimit 预填。文案写明「重启后生效」(推导只在进程启动时算一次)。

- 顺手修既有红线违反:ProviderFields 87→76(抽 ApiKeyAndBudgetField)、LlmProviderForm 113→74(buildRequest 提为模块级纯函数)——本次碰过的文件里已无 >80 的函数,行为/文案逐字未变。

- DocumentRef 的 chars/truncated 注释从「抽取侧」改为「本回合注入侧」(全文长度只在 document-read 的 totalChars、extractTruncated 专指撞 8 MiB 安全上限);机械校验为空 ⇒ 代码零改动。

- 主会话独立验收:隔离 worktree Go 侧 build/vet + gateway/history 两包绿;主树 npx tsc --noEmit exit 0、npm run build 成功并 copy 到 internal/webui/dist;注释机械校验为空;变异由子代理复现(写路径不认该键 ⇒ 3 红;去掉网关负数守卫 ⇒ 400 变 500)。
2026-10-04 18:11:43 +08:00

217 lines
8.6 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,因此面板不必靠猜来解释
// 一次静默。
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"`
}