- 后端: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」切换
201 lines
7.6 KiB
Go
201 lines
7.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"`
|
||
// 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"`
|
||
}
|