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