Files
Cyrene_v2/internal/services/gateway/admin.go
T
AskaEth 1ac02bd77e
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway,admin-web): 面板可编辑 provider 推理形状 / 角色级路由 / multimodal 真语义(裁决 03.4.2/03.4.1)
- 后端:AdminLlmProvider 暴露 reasoning{kind}(恒下发,""=未声明=auto)、personaModels、personaProviders;
  顶层新增 reasoningKinds / personaProviderOptions 两个**取值域**(唯一来源= runtimeconfig 导出枚举,
  校验错误文案也由枚举拼接 ⇒ 不可能漂移)
- 读写共用同一映射(admin_llm_provider.go),避免「读侧少一个键 ⇒ 面板一保存就抹掉」
- 写侧语义:缺失=保留、给出=整块/整表替换;非法值一律 runtimeconfig.Group.Validate() ⇒ 400 bad_request
  (实测非法 kind 返回 400 且 models.json 完全不动,而非 500 静默失败)
- 前端:新增 LlmProviderRouting(推理形状下拉 + multimodal 开关 + 按角色路由编辑器);
  目录能力位改三态(true/false/未标注),「未标注」不再谎称「不支持」;目录/角色清单不可用时
  **不阻塞编辑**且与「字段为空」是不同状态;绝无 mock/sample 回退
- 复核:build/vet 绿 + gateway/config 包测试绿 + tsc 0 错 + vite build 成功;2 条变异反证(删 reasoning
  补丁 → 2 例红;删 Group.Validate → 该 400 用例红)已还原
- 注意(主会话已知):npm run build 把新 SPA 写进 frontend/web/dist,而运行中的二进制优先从磁盘读该目录
  ⇒ :10942 目前是「新面板 + 旧后端」,等阶段 5 全绿后按红线「先 build 到位、再一次 restart」切换
2026-10-04 13:06:54 +08:00

201 lines
7.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"`
// 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 是管理监控列表中一条后台思考记录的
// 线上结构。
type AdminBackthinkItem struct {
ID string `json:"id"`
Trigger string `json:"trigger"`
Conclusion string `json:"conclusion,omitempty"`
ShouldNotify bool `json:"shouldNotify"`
Pushed bool `json:"pushed"`
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"`
}