From f9e9a1572f15b334eb50d617133cf8d255d83125 Mon Sep 17 00:00:00 2001
From: Sakurasan <26715255+Sakurasan@users.noreply.github.com>
Date: Tue, 1 Sep 2026 02:32:31 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B3=BB=E7=BB=9F=E8=AE=BE=E7=BD=AE?=
=?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8E=9F=E5=A7=8B=E8=AF=B7=E6=B1=82/?=
=?UTF-8?q?=E5=93=8D=E5=BA=94=E8=AE=B0=E5=BD=95=E5=BC=80=E5=85=B3=EF=BC=88?=
=?UTF-8?q?=E4=BB=85=E7=AE=A1=E7=90=86=E5=91=98=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 系统配置键 log_raw_requests:开启后,仅管理员账号的每次请求
在用量明细中保存客户端原始请求体与上游原始响应体
(流式含全部 SSE 事件),用于排障
- UsageLog 新增 raw_request / raw_response 字段(type:text)
- AuthLLM 附带 user_role 供网关判断管理员
- gateway:10s TTL 缓存开关;streamResponse/bufferResponse
支持累积上游原始响应;recordUsage 填充原始字段
- 前端 SystemConfig 新增开关(会显著增加存储的提示)
- 新增 doc/flow.md 网关调用流程示意图
---
backend/internal/proxy/gateway.go | 66 ++++-
backend/internal/store/models.go | 2 +
backend/internal/usage/recorder.go | 4 +
backend/middleware/auth_llm.go | 8 +
doc/flow.md | 276 ++++++++++++++++++
frontend/src/views/dashboard/SystemConfig.vue | 41 +++
6 files changed, 393 insertions(+), 4 deletions(-)
create mode 100644 doc/flow.md
diff --git a/backend/internal/proxy/gateway.go b/backend/internal/proxy/gateway.go
index e6a2590..e2550de 100644
--- a/backend/internal/proxy/gateway.go
+++ b/backend/internal/proxy/gateway.go
@@ -41,6 +41,11 @@ type Gateway struct {
modelDAO *dao.ModelDAO
channelSvc *channel.Service
usageRec *usage.Recorder
+
+ // 原始请求/响应记录开关(系统配置 log_raw_requests,带 TTL 缓存避免每次查库)。
+ rawLogMu sync.Mutex
+ rawLogVal bool
+ rawLogSet time.Time
}
func NewGateway(ctx context.Context, cfg *config.Config, db *gorm.DB, wg *sync.WaitGroup, userDAO *dao.UserDAO, apiKeyDAO *dao.ApiKeyDAO, usageDAO *dao.UsageDAO, dailyDAO *dao.DailyUsageDAO) *Gateway {
@@ -79,6 +84,22 @@ func (g *Gateway) SetUsageRecorder(r *usage.Recorder) {
g.usageRec = r
}
+// rawLogEnabled 读取系统配置 log_raw_requests(10s TTL 缓存),决定是否记录原始请求/响应。
+func (g *Gateway) rawLogEnabled() bool {
+ g.rawLogMu.Lock()
+ defer g.rawLogMu.Unlock()
+ if time.Since(g.rawLogSet) < 10*time.Second {
+ return g.rawLogVal
+ }
+ var sc store.SystemConfig
+ g.rawLogVal = false
+ if err := g.db.Where("key = ?", "log_raw_requests").First(&sc).Error; err == nil {
+ g.rawLogVal = strings.TrimSpace(sc.Value) == "true"
+ }
+ g.rawLogSet = time.Now()
+ return g.rawLogVal
+}
+
// generateRequestID 生成请求级唯一 ID,用于用量明细关联与排障。
func generateRequestID() string {
b := make([]byte, 12)
@@ -98,6 +119,10 @@ type Request struct {
UserID uint64
KeyID uint64
RequestID string
+
+ CaptureRaw bool // 原始请求/响应记录(管理员 + 系统开关开启)
+
+ rawBuf *strings.Builder // 上游原始响应累积器(仅 CaptureRaw 时非 nil)
}
// ParseRequest parses the incoming request and extracts key fields
@@ -109,6 +134,7 @@ func (g *Gateway) ParseRequest(c *gin.Context, protocol string) (*Request, error
apiKey, _ := c.Get("api_key")
userID, _ := c.Get("user_id")
+ userRole, _ := c.Get("user_role")
req := &Request{
Protocol: protocol,
@@ -124,6 +150,11 @@ func (g *Gateway) ParseRequest(c *gin.Context, protocol string) (*Request, error
req.APIKey = ak
}
+ // 原始请求/响应记录:仅管理员 且 系统开关 log_raw_requests 开启。
+ if role, _ := userRole.(string); role == store.RoleAdmin && g.rawLogEnabled() {
+ req.CaptureRaw = true
+ }
+
// Parse model and stream based on protocol
switch protocol {
case "chat":
@@ -160,6 +191,12 @@ func (g *Gateway) Dispatch(c *gin.Context, req *Request) {
return
}
+ // 原始请求/响应捕获:仅管理员 + 系统开关开启(req.CaptureRaw 已在 ParseRequest 判定)。
+ // 客户端原始请求体即 req.Body;上游原始响应由 stream/bufferResponse 累积进 rawBuf。
+ if req.CaptureRaw {
+ req.rawBuf = &strings.Builder{}
+ }
+
cands := g.channelSvc.Candidates(req.Model)
// 内存健康过滤:连续失败进入 cooldown 的渠道不再尝试(渠道级健康自愈靠冷却过期)。
cands = g.channelSvc.FilterHealthy(cands)
@@ -245,6 +282,9 @@ func (g *Gateway) Dispatch(c *gin.Context, req *Request) {
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
log.Printf("Upstream error: status=%d body=%s", resp.StatusCode, string(body))
+ if req.rawBuf != nil {
+ req.rawBuf.Write(body)
+ }
g.recordUsage(req, cand, ch, usage.Event{
IsError: true,
ErrorCode: fmt.Sprintf("upstream_%d", resp.StatusCode),
@@ -264,9 +304,9 @@ func (g *Gateway) Dispatch(c *gin.Context, req *Request) {
// Stream or buffer response;tok 从上游响应(SSE usage 块或非流式 JSON)提取。
var tok convert.TokenUsage
if req.Stream {
- tok = g.streamResponse(c, resp, req.Protocol, targetFormat)
+ tok = g.streamResponse(c, resp, req.Protocol, targetFormat, req.rawBuf)
} else {
- tok = g.bufferResponse(c, resp, req.Protocol, targetFormat)
+ tok = g.bufferResponse(c, resp, req.Protocol, targetFormat, req.rawBuf)
}
resp.Body.Close()
// 成功记录:用量 + 定价计费。
@@ -320,6 +360,13 @@ func (g *Gateway) recordUsage(req *Request, cand *channel.Candidate, ch *store.C
ev.CompletionTokens = tok.OutputTokens
ev.CacheReadTokens = tok.CacheReadTokens
ev.CacheCreationTokens = tok.CacheCreationTokens
+ // 原始请求/响应(仅管理员+开关开启时捕获)。
+ if req.CaptureRaw {
+ ev.RawRequest = string(req.Body)
+ if req.rawBuf != nil {
+ ev.RawResponse = req.rawBuf.String()
+ }
+ }
// 定价与成本(价格按每百万 token 的 USD 单价)。
// 成本口径:非缓存输入 × 输入价 + 缓存读 × 缓存价 + 缓存写与输出 × 输出价。
if ev.ModelID != 0 {
@@ -385,7 +432,8 @@ func (g *Gateway) setHeaders(req *http.Request, ch *store.Channel, apiKey string
// streamResponse 流式响应:按 \n\n 分块零缓冲转发;跨协议时逐行转换。
// 返回从上游 SSE usage 块累计的 token 用量(按上游协议解析)。
-func (g *Gateway) streamResponse(c *gin.Context, resp *http.Response, clientProto, upstreamProto string) convert.TokenUsage {
+// capture 非 nil 时把上游原始行累积进去(原始响应记录)。
+func (g *Gateway) streamResponse(c *gin.Context, resp *http.Response, clientProto, upstreamProto string, capture *strings.Builder) convert.TokenUsage {
w := c.Writer
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
@@ -429,6 +477,11 @@ func (g *Gateway) streamResponse(c *gin.Context, resp *http.Response, clientProt
}
}
+ // 原始响应捕获(仅管理员+开关开启时启用)。
+ if capture != nil {
+ capture.Write(buf)
+ }
+
// 先解析用量(data: {...} 行),再决定转发内容。
for _, data := range sseDataPayloads(buf) {
accum.Feed(data, upstreamProto)
@@ -493,13 +546,18 @@ func sseDataPayloads(chunk []byte) [][]byte {
return out
}
-func (g *Gateway) bufferResponse(c *gin.Context, resp *http.Response, clientProto, upstreamProto string) convert.TokenUsage {
+func (g *Gateway) bufferResponse(c *gin.Context, resp *http.Response, clientProto, upstreamProto string, capture *strings.Builder) convert.TokenUsage {
body, err := io.ReadAll(resp.Body)
if err != nil {
g.writeError(c, http.StatusBadGateway, "failed to read response")
return convert.TokenUsage{}
}
+ // 原始响应捕获(仅管理员+开关开启时启用)。
+ if capture != nil {
+ capture.Write(body)
+ }
+
// 用量从上游原始响应体提取(先于转换,转换会改字段名)。
tok, _ := convert.ExtractUsageJSON(body, upstreamProto)
diff --git a/backend/internal/store/models.go b/backend/internal/store/models.go
index fc7ed26..5c02b39 100644
--- a/backend/internal/store/models.go
+++ b/backend/internal/store/models.go
@@ -174,6 +174,8 @@ type UsageLog struct {
LatencyMS int `json:"latency_ms"`
Status string `gorm:"size:16;not null" json:"status"`
ErrorCode *string `json:"error_code,omitempty"`
+ RawRequest string `gorm:"type:text" json:"raw_request,omitempty"` // 客户端原始请求体(未转换;仅管理员+开关开启时记录)
+ RawResponse string `gorm:"type:text" json:"raw_response,omitempty"` // 上游原始响应(未转换;流式为全部 SSE 事件)
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
diff --git a/backend/internal/usage/recorder.go b/backend/internal/usage/recorder.go
index 074512e..b2d4e81 100644
--- a/backend/internal/usage/recorder.go
+++ b/backend/internal/usage/recorder.go
@@ -32,6 +32,8 @@ type Event struct {
CacheReadPrice float64
TraceID string // TraceID for distributed tracing
ModelID uint64 // Model ID from channel-model binding
+ RawRequest string // 客户端原始请求体(仅管理员+开关开启时记录)
+ RawResponse string // 上游原始响应(未转换;流式为全部 SSE 事件)
}
// Recorder handles async usage recording
@@ -153,6 +155,8 @@ func (r *Recorder) flush(events []Event) {
ErrorCode: errCode,
RequestID: e.RequestID,
TraceID: e.TraceID,
+ RawRequest: e.RawRequest,
+ RawResponse: e.RawResponse,
}
logs = append(logs, log)
}
diff --git a/backend/middleware/auth_llm.go b/backend/middleware/auth_llm.go
index f33cc51..df64de1 100644
--- a/backend/middleware/auth_llm.go
+++ b/backend/middleware/auth_llm.go
@@ -40,8 +40,16 @@ func AuthLLM(db *gorm.DB) gin.HandlerFunc {
return
}
+ // 附带用户角色(判断是否管理员,供原始请求/响应记录等管理能力使用)。
+ var user store.User
+ if err := db.First(&user, apiKey.UserID).Error; err != nil {
+ unauthorized(c, "无效的API密钥")
+ return
+ }
+
c.Set("api_key", &apiKey)
c.Set("user_id", apiKey.UserID)
+ c.Set("user_role", user.Role)
c.Next()
}
}
diff --git a/doc/flow.md b/doc/flow.md
new file mode 100644
index 0000000..c76eb57
--- /dev/null
+++ b/doc/flow.md
@@ -0,0 +1,276 @@
+# 网关调用流程示意图
+
+> 对应实现:`backend/router/setRouter.go`(路由注册)、`backend/middleware/auth_llm.go`(鉴权)、
+> `backend/internal/proxy/{gateway.go,handlers.go,convert/*}`(网关与三协议互转)、
+> `backend/internal/channel/{channel.go,health.go}`(渠道路由与健康)、
+> `backend/internal/usage/recorder.go`(用量异步落库)。
+
+## 0. 总览
+
+```
+ ┌────────────────────────────────────────────────┐
+ │ Gin Router (/v1) │
+ │ ┌──────────────────────────────────────────┐ │
+ 客户端 ───────────▶│ │ middleware.AuthLLM (密钥鉴权, 401拦截) │ │
+ Bearer sk-ot-… │ └──────────────────────────────────────────┘ │
+ │ ┌───────┬────────┬─────────┬─────────┐ │
+ │ │ chat │messages│responses│ models │ │
+ │ │Handle │Handle │ Handle │ Handle │ │
+ │ │Chat │Messages│Responses│ Models │ │
+ │ └───┬───┴───┬────┴────┬────┴─────────┘ │
+ │ └───────┴────┬────┘ │
+ │ ParseRequest │
+ │ (model/stream) │
+ │ │ │
+ │ Dispatch ◀── Candidates/Pick │
+ │ (转换+故障转移+用量记录) │
+ └───────────────────┼──────────────────────────────┘
+ │
+ ▼
+ 上游 /v1/* (chat|messages|responses)
+```
+
+## 1. 请求入口与鉴权
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant C as 客户端
+ participant R as Gin /v1 路由
+ participant A as AuthLLM
+ participant DB as SQLite(APIKey)
+ participant H as HandleXxx
+
+ C->>R: POST /v1/chat/completions 等
+ Note over R: /v1 组挂 middleware.AuthLLM
+ R->>A: 进入中间件
+ A->>A: 提取 Bearer token(兼容无前缀直传)
+ alt 未携带 Authorization
+ A-->>C: 401 「未提供认证信息」
+ else token 长度 < 12 或 prefix 不匹配
+ A-->>C: 401 「无效的API密钥」
+ else prefix 命中
+ A->>DB: SELECT * WHERE key_prefix=? AND status=active
+ A->>A: sha256(token) == KeyHash ?
+ alt 哈希不一致
+ A-->>C: 401 「无效的API密钥」
+ else 校验通过
+ A->>H: c.Set(api_key, user_id) → 放行
+ end
+ end
+```
+
+> 关键点:`key_prefix` 取 `sk-ot-` 后前 12 位(`api.go` 创建密钥时 `keyValue[:12]`),
+> `auth_llm.go` 用同一常量 `keyPrefixLen=12` 切片,避免越界 panic。
+
+## 2. 三协议主调用流程
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant C as 客户端
+ participant H as HandleChat/Messages/Responses
+ participant P as ParseRequest
+ participant G as Dispatch
+ participant S as ChannelService
+ participant U as usage.Recorder
+ participant UP as 上游(OpenRouter等)
+
+ C->>H: 请求体 (model, stream, messages/input…)
+ H->>P: ParseRequest(protocol)
+ P->>P: 读 body → 解析 model / stream
+ P-->>H: Request{Model, Stream, Protocol, Body, …}
+ H->>G: Dispatch(req)
+
+ G->>S: Candidates(req.Model)
+ S-->>G: []Candidate{Channel, Binding?}
+ G->>S: FilterHealthy(cands)
+ G->>S: Pick(cands) → 加权随机选定一个候选
+ Note over G,S: 绑定优先(携带 upstream_model 映射);
无绑定回退到权重最低的健康备用渠道
+
+ loop 故障转移(候选耗尽前)
+ G->>G: conversionTarget(ch, proto) → 渠道首选协议
+ alt 客户端协议 ≠ 渠道协议
+ G->>G: ConvertRequest(body, from, to) 转换请求体
+ Note over G: 走 convert 包(chat/messages/responses 互转)
+ end
+ alt 有 Binding.UpstreamModel
+ G->>G: rewriteModel(body, upstreamModel) 别名映射
+ end
+ G->>UP: POST {base}/v1/{path} (按渠道协议拼 URL/头)
+ alt 连接失败 或 429/5xx
+ S->>S: RecordFailure(ch) → 连续2次 degraded 熔断
+ G->>U: Record(error 事件, error_code)
+ Note over G: continue → 换下一个候选渠道
+ else 4xx
+ G-->>C: 透传上游错误体 (不重试)
+ else 2xx
+ S->>S: RecordSuccess(ch)
+ alt stream=true
+ G->>G: streamResponse → 逐块转发 + 累计 usage
+ else
+ G->>G: bufferResponse → 整体转发 + 提取 usage
+ end
+ G->>G: recordUsage (按模型定价计算 cost)
+ G->>U: Record(成功事件, tokens, cost)
+ G-->>C: 响应
+ end
+ end
+ Note over G,C: 全部候选失败 → 502/503
+```
+
+> **故障转移规则**(对齐参考实现 `doProxy`):
+> - 连接错误、429、5xx → 可重试,换下一个候选;
+> - 4xx(如 400 参数错误)→ 透传上游错误体,不重试;
+> - 无可用渠道(全部不健康/无绑定且无备用)→ 502/503 + `error_code=no_channel`。
+
+## 3. 路由选择细节
+
+```mermaid
+flowchart TD
+ A[客户端 model 名] --> B{存在启用模型行?}
+ B -- 是 --> C{有绑定且渠道健康?}
+ C -- 是 --> D[候选 = 绑定该模型的渠道
排序 priority ASC, weight DESC, id ASC]
+ C -- 否 --> E
+ B -- 否 --> E[候选 = 全部启用渠道
取权重最低的健康备用渠道]
+ D --> F[FilterHealthy 内存熔断过滤]
+ E --> F
+ F --> G[Pick 加权随机选中一个]
+ G --> H[Dispatch 开始尝试]
+ H --> I{尝试成功?}
+ I -- 失败可重试 --> J[RecordFailure + 换下一个]
+ J --> H
+ I -- 成功 --> K[RecordSuccess + 响应 + 记账]
+ J -. 全部耗尽 .-> L[502/503]
+```
+
+## 4. 跨协议转换(client ↔ 渠道原生协议)
+
+```mermaid
+flowchart LR
+ subgraph 客户端协议
+ CHAT[/"chat
chat/completions"/]
+ MSG[/"messages
(Anthropic)"/]
+ RESP[/"responses
(OpenAI)"/]
+ end
+ subgraph 中间模型
+ MID["Chat 形状
(标准中间模型)"]
+ end
+ subgraph 渠道协议
+ UCHAT[/"chat"/]
+ UMSG[/"messages"/]
+ URESP[/"responses"/]
+ end
+
+ CHAT -->|直通| UCHAT
+ MSG -->|messagesToChat| MID -->|chatToMessages| UMSG
+ MSG -->|messagesToChat| MID -->|chatToResponses| URESP
+ RESP -->|responsesToChat| MID -->|chatToResponses| URESP
+ RESP -->|responsesToChat| MID -->|chatToMessages| UMSG
+```
+
+> 转换入口:`convert.ConvertRequest`(请求体)、`convert.ConvertResponse`(非流式响应)、
+> `convert.NewStreamTransformer`(流式 SSE 逐行转换)。跨两跳时经 Chat 中转(如
+> `responses→messages` = `responsesToChatReq` + `chatToMessagesReq`)。
+
+## 5. 流式 / 非流式响应与用量提取
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant G as Dispatch
+ participant S as streamResponse
+ participant B as bufferResponse
+ participant ACC as StreamUsageAccum
+ participant W as 客户端 Writer
+ participant UP as 上游
+
+ alt stream=true
+ G->>S: streamResponse(resp, clientProto, upstreamProto)
+ S->>UP: 按 \n\n 读块 (bufio)
+ loop 每个 SSE 块
+ S->>ACC: sseDataPayloads(chunk) → Feed(data, upstreamProto)
+ Note over ACC: 逐协议累计 usage 字段
+ alt 跨协议
+ S->>S: NewStreamTransformer(upstream→client).line(chunk)
+ end
+ S->>W: 写块 + Flush
+ alt 遇到流结束标记
+ Note over S: chat: data:[DONE]
messages: message_stop
responses: response.completed
+ S-->>G: 返回累计 TokenUsage
+ end
+ end
+ else stream=false
+ G->>B: bufferResponse(resp, clientProto, upstreamProto)
+ B->>B: io.ReadAll
+ B->>B: ExtractUsageJSON(body, upstreamProto)
+ alt 跨协议
+ B->>B: ConvertResponse(body, upstream→client)
+ else 直通
+ B->>B: CleanJSON(body) 去空白/SSE注释前缀
+ end
+ B-->>G: 返回 TokenUsage
+ end
+ G->>G: recordUsage(req, cand, ch, ev, tok)
+ Note over G: 定价 cost = (非缓存输入×输入价 + 缓存读×缓存价
+ 缓存写×输出价 + 输出×输出价) / 1e6
+ G->>G: usageRec.Record(Event)
+```
+
+## 6. 用量异步落库
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant G as Gateway
+ participant R as usage.Recorder
+ participant U as UsageDAO
+ participant D as DailyUsageDAO
+ participant DB as SQLite
+
+ G->>R: Record(Event) 每次请求(成功/错误/取消)
+ Note over R: 缓冲 channel (10000), 每 5s 或满 100 条 flush
+ R->>U: BatchCreate(UsageLog[])
+ R->>D: UpsertDailyUsage(UsageDaily) 按(user_id,model_id,date)增量累加
+ U->>DB: INSERT usage_logs
+ D->>DB: ON CONFLICT 累加 requests/input/output/cache/cost
+```
+
+> `usage_dailies` 用 `gorm.Expr("requests + ?")` 增量累加而非覆盖,保证多次 flush 不互相清零。
+
+## 7. 渠道健康与熔断
+
+```mermaid
+flowchart TD
+ A[请求失败] --> B[RecordFailure: consecutive++]
+ B --> C{consecutive >= 2?}
+ C -- 是 --> D[status=degraded + 5min cooldown]
+ C -- 否 --> E[仅计数]
+ D --> F{后续请求 Candidates}
+ F --> G{FilterHealthy 该渠道}
+ G -- degraded/cooldown 未过期 --> H[排除, 走其他渠道/备用]
+ G -- healthy --> I[参与选择]
+ D -. 冷却过期 .-> J[复位 healthy]
+ J --> I
+ K[健康检查周期探测成功] --> L[RecordSuccess: 复位 healthy]
+```
+
+> 说明:`health.go` 的 `StartPeriodicCheck`(默认 5min)会探测各渠道 `/models`,
+> 成功调 `RecordSuccess` 复位;失败调 `RecordFailure` 进入熔断计数。
+
+## 8. 关键代码锚点
+
+| 环节 | 位置 |
+|---|---|
+| /v1 路由注册 + AuthLLM | `router/setRouter.go:146` |
+| 密钥鉴权 | `middleware/auth_llm.go` |
+| 请求解析 | `proxy/gateway.go:104 ParseRequest` |
+| 主调度 + 故障转移 | `proxy/gateway.go:157 Dispatch` |
+| 流式转发 + 结束检测 | `proxy/gateway.go:388 streamResponse` |
+| 非流式转发 | `proxy/gateway.go:496 bufferResponse` |
+| 用量记账 | `proxy/gateway.go:302 recordUsage` |
+| 候选构建 | `channel/channel.go:51 Candidates` |
+| 内存健康过滤 | `channel/channel.go:252 FilterHealthy` |
+| 加权选择 | `channel/channel.go:222 Pick` |
+| 失败熔断 | `channel/channel.go:139 RecordFailure` |
+| 三协议互转 | `proxy/convert/{convert.go,json_chat.go,json_responses.go,stream_transform.go}` |
+| 用量异步落库 | `usage/recorder.go:114 flush` |
diff --git a/frontend/src/views/dashboard/SystemConfig.vue b/frontend/src/views/dashboard/SystemConfig.vue
index 259bcea..82dd13b 100644
--- a/frontend/src/views/dashboard/SystemConfig.vue
+++ b/frontend/src/views/dashboard/SystemConfig.vue
@@ -13,6 +13,7 @@ const saving = ref(false)
const registrationEnabled = ref(true)
const passwordLoginEnabled = ref(true)
+const logRawRequests = ref(false)
async function load() {
loading.value = true
@@ -23,6 +24,9 @@ async function load() {
])
registrationEnabled.value = regRes.data.data.enabled
passwordLoginEnabled.value = pwdRes.data.data.enabled
+ // 原始请求/响应记录开关(通用配置键 log_raw_requests)
+ const cfgRes = await request.get('/admin/config')
+ logRawRequests.value = cfgRes.data?.data?.log_raw_requests === 'true'
} catch (e) {
setToast(errMsg(e), 'error')
} finally {
@@ -56,6 +60,19 @@ async function savePasswordLogin(enabled: boolean) {
}
}
+async function saveLogRawRequests(enabled: boolean) {
+ saving.value = true
+ try {
+ await request.put('/admin/config', { log_raw_requests: enabled ? 'true' : 'false' })
+ logRawRequests.value = enabled
+ setToast(enabled ? '已开启原始请求/响应记录' : '已关闭原始请求/响应记录', 'success')
+ } catch (e) {
+ setToast(errMsg(e), 'error')
+ } finally {
+ saving.value = false
+ }
+}
+
onMounted(load)
@@ -116,6 +133,30 @@ onMounted(load)
+
+
+
仅对管理员账号生效:在用量明细中保存每次请求的客户端原始请求体与上游原始响应体(流式含全部 SSE 事件),用于排障。会显著增加存储。
+