请求明细支持查看原始请求/响应:OT_PROXY_LOG_RAW 开关控制,仅管理员记录与可见,流式全量捕获
- 配置: ProxyConfig.LogRaw (OT_PROXY_LOG_RAW, 默认 false) - 存储: usage_logs 新增 raw_request/raw_response 文本列 (AutoMigrate) - 网关: NewGateway 接收 logRaw 参数 - handlers: 三个协议入口按 开关+管理员 条件记录原始请求体 - passthrough: 非流式 copyAndCapture 捕获响应, 流式 streamCopy 累积全部原始 SSE 行, finishUsage 统一写入 - admin API: AdminUsage 返回 raw_request/raw_response (仅管理员) - 前端: 用量页新增查看入口, 弹窗 tab 切换请求/响应 - gitignore: 修正 server/web/ 忽略规则(尾随空格导致未生效)
This commit is contained in:
@@ -33,6 +33,9 @@ OT_PROXY_TIMEOUT=120s
|
|||||||
OT_PROXY_HEALTH_INTERVAL=60s
|
OT_PROXY_HEALTH_INTERVAL=60s
|
||||||
OT_PROXY_HEALTH_FAIL_THRESHOLD=2
|
OT_PROXY_HEALTH_FAIL_THRESHOLD=2
|
||||||
|
|
||||||
|
# 调试:记录管理员的原始请求与响应到请求明细(默认关闭;流式记录全部事件)
|
||||||
|
OT_PROXY_LOG_RAW=false
|
||||||
|
|
||||||
# 限流(内存计数,Redis 后置):用户级每秒请求数上限(0=不限制)
|
# 限流(内存计数,Redis 后置):用户级每秒请求数上限(0=不限制)
|
||||||
OT_RATELIMIT_USER_RPS=20
|
OT_RATELIMIT_USER_RPS=20
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@ dist/
|
|||||||
# Go
|
# Go
|
||||||
server/bin/
|
server/bin/
|
||||||
server/data/
|
server/data/
|
||||||
server/web/ # 本地静态托管软链(指向 ../web/dist),不入库
|
# 本地静态托管软链(指向 ../web/dist),不入库
|
||||||
|
server/web/
|
||||||
scripts/mockupstream/bin/
|
scripts/mockupstream/bin/
|
||||||
|
|
||||||
# TypeScript 增量构建产物
|
# TypeScript 增量构建产物
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer a.Shutdown(context.Background())
|
defer a.Shutdown(context.Background())
|
||||||
|
|
||||||
gw := proxy.NewGateway(a.DB, a.Enc, a.Usage, a.Limit, cfg.RateLimit.UserRPS)
|
gw := proxy.NewGateway(a.DB, a.Enc, a.Usage, a.Limit, cfg.RateLimit.UserRPS, cfg.Proxy.LogRaw)
|
||||||
router := api.NewRouter(a, gw)
|
router := api.NewRouter(a, gw)
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ func (h *Handler) AdminUsage(c *gin.Context) {
|
|||||||
"input_tokens": l.InputTokens, "output_tokens": l.OutputTokens,
|
"input_tokens": l.InputTokens, "output_tokens": l.OutputTokens,
|
||||||
"cache_read_tokens": l.CacheReadTokens, "cost": l.Cost,
|
"cache_read_tokens": l.CacheReadTokens, "cost": l.Cost,
|
||||||
"latency_ms": l.LatencyMS, "status": l.Status, "error_code": l.ErrorCode,
|
"latency_ms": l.LatencyMS, "status": l.Status, "error_code": l.ErrorCode,
|
||||||
|
"raw_request": l.RawRequest, "raw_response": l.RawResponse,
|
||||||
"created_at": l.CreatedAt,
|
"created_at": l.CreatedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ type ProxyConfig struct {
|
|||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
HealthInterval time.Duration // 渠道健康检查周期
|
HealthInterval time.Duration // 渠道健康检查周期
|
||||||
HealthFailThreshold int // 连续失败 N 次进 cooldown
|
HealthFailThreshold int // 连续失败 N 次进 cooldown
|
||||||
|
LogRaw bool // 记录管理员原始请求体+响应到 usage_logs(调试用,默认关)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadDotEnv 读取 .env 并把 KEY=VALUE 注入环境变量(AutomaticEnv 自动映射 OT_ 前缀)。
|
// loadDotEnv 读取 .env 并把 KEY=VALUE 注入环境变量(AutomaticEnv 自动映射 OT_ 前缀)。
|
||||||
@@ -129,6 +130,7 @@ func Load() (*Config, error) {
|
|||||||
v.SetDefault("proxy.timeout", "120s")
|
v.SetDefault("proxy.timeout", "120s")
|
||||||
v.SetDefault("proxy.health_interval", "60s")
|
v.SetDefault("proxy.health_interval", "60s")
|
||||||
v.SetDefault("proxy.health_fail_threshold", 2)
|
v.SetDefault("proxy.health_fail_threshold", 2)
|
||||||
|
v.SetDefault("proxy.log_raw", false)
|
||||||
|
|
||||||
v.SetDefault("ratelimit.user_rps", 20)
|
v.SetDefault("ratelimit.user_rps", 20)
|
||||||
|
|
||||||
@@ -168,6 +170,7 @@ func Load() (*Config, error) {
|
|||||||
Timeout: v.GetDuration("proxy.timeout"),
|
Timeout: v.GetDuration("proxy.timeout"),
|
||||||
HealthInterval: v.GetDuration("proxy.health_interval"),
|
HealthInterval: v.GetDuration("proxy.health_interval"),
|
||||||
HealthFailThreshold: v.GetInt("proxy.health_fail_threshold"),
|
HealthFailThreshold: v.GetInt("proxy.health_fail_threshold"),
|
||||||
|
LogRaw: v.GetBool("proxy.log_raw"),
|
||||||
},
|
},
|
||||||
RateLimit: RateLimitConfig{
|
RateLimit: RateLimitConfig{
|
||||||
UserRPS: v.GetInt("ratelimit.user_rps"),
|
UserRPS: v.GetInt("ratelimit.user_rps"),
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type Gateway struct {
|
|||||||
enc *crypto.Encryptor
|
enc *crypto.Encryptor
|
||||||
lim *ratelimit.Limiter
|
lim *ratelimit.Limiter
|
||||||
userRPS int
|
userRPS int
|
||||||
|
logRaw bool
|
||||||
hc *http.Client
|
hc *http.Client
|
||||||
|
|
||||||
policyMu sync.Mutex
|
policyMu sync.Mutex
|
||||||
@@ -108,7 +109,7 @@ func contains(list []string, s string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGateway(db *gorm.DB, enc *crypto.Encryptor, rec *usage.Recorder, lim *ratelimit.Limiter, userRPS int) *Gateway {
|
func NewGateway(db *gorm.DB, enc *crypto.Encryptor, rec *usage.Recorder, lim *ratelimit.Limiter, userRPS int, logRaw bool) *Gateway {
|
||||||
return &Gateway{
|
return &Gateway{
|
||||||
db: db,
|
db: db,
|
||||||
ch: channel.NewService(db, enc),
|
ch: channel.NewService(db, enc),
|
||||||
@@ -116,6 +117,7 @@ func NewGateway(db *gorm.DB, enc *crypto.Encryptor, rec *usage.Recorder, lim *ra
|
|||||||
enc: enc,
|
enc: enc,
|
||||||
lim: lim,
|
lim: lim,
|
||||||
userRPS: userRPS,
|
userRPS: userRPS,
|
||||||
|
logRaw: logRaw,
|
||||||
hc: &http.Client{Timeout: 120 * time.Second},
|
hc: &http.Client{Timeout: 120 * time.Second},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/openteam/server/internal/proxy/convert"
|
"github.com/openteam/server/internal/proxy/convert"
|
||||||
|
"github.com/openteam/server/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// chatCompletions POST /v1/chat/completions
|
// chatCompletions POST /v1/chat/completions
|
||||||
@@ -23,6 +24,7 @@ func (g *Gateway) chatCompletions(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.Set("protocol", convert.ProtoChat)
|
c.Set("protocol", convert.ProtoChat)
|
||||||
c.Set("model_name", br.Model)
|
c.Set("model_name", br.Model)
|
||||||
|
g.recordRawRequest(c, u, body)
|
||||||
if !g.checkModelAllowed(u, br.Model) {
|
if !g.checkModelAllowed(u, br.Model) {
|
||||||
apiError(c, http.StatusForbidden, "model_not_allowed", "模型未对你开放,请联系管理员")
|
apiError(c, http.StatusForbidden, "model_not_allowed", "模型未对你开放,请联系管理员")
|
||||||
return
|
return
|
||||||
@@ -55,6 +57,7 @@ func (g *Gateway) responses(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.Set("protocol", convert.ProtoResponses)
|
c.Set("protocol", convert.ProtoResponses)
|
||||||
c.Set("model_name", br.Model)
|
c.Set("model_name", br.Model)
|
||||||
|
g.recordRawRequest(c, u, body)
|
||||||
if !g.checkModelAllowed(u, br.Model) {
|
if !g.checkModelAllowed(u, br.Model) {
|
||||||
apiError(c, http.StatusForbidden, "model_not_allowed", "模型未对你开放,请联系管理员")
|
apiError(c, http.StatusForbidden, "model_not_allowed", "模型未对你开放,请联系管理员")
|
||||||
return
|
return
|
||||||
@@ -87,6 +90,7 @@ func (g *Gateway) messages(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.Set("protocol", convert.ProtoMessages)
|
c.Set("protocol", convert.ProtoMessages)
|
||||||
c.Set("model_name", br.Model)
|
c.Set("model_name", br.Model)
|
||||||
|
g.recordRawRequest(c, u, body)
|
||||||
if !g.checkModelAllowed(u, br.Model) {
|
if !g.checkModelAllowed(u, br.Model) {
|
||||||
apiError(c, http.StatusForbidden, "model_not_allowed", "模型未对你开放,请联系管理员")
|
apiError(c, http.StatusForbidden, "model_not_allowed", "模型未对你开放,请联系管理员")
|
||||||
return
|
return
|
||||||
@@ -108,6 +112,14 @@ type sinkHolder struct {
|
|||||||
sink *usageSink
|
sink *usageSink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordRawRequest 记录管理员原始请求体到 context(供 finishUsage 落库)。
|
||||||
|
// 仅当开关开启且用户为管理员时记录;响应侧以 c.Get("raw_request") 是否非空判断是否需要捕获响应。
|
||||||
|
func (g *Gateway) recordRawRequest(c *gin.Context, u *store.User, body []byte) {
|
||||||
|
if g.logRaw && u.Role == store.RoleAdmin {
|
||||||
|
c.Set("raw_request", string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// apiError 按客户端协议返回错误体(PLANNING §5.1.4)。
|
// apiError 按客户端协议返回错误体(PLANNING §5.1.4)。
|
||||||
func apiError(c *gin.Context, status int, code, message string) {
|
func apiError(c *gin.Context, status int, code, message string) {
|
||||||
if p, _ := c.Get("protocol"); p == convert.ProtoMessages {
|
if p, _ := c.Get("protocol"); p == convert.ProtoMessages {
|
||||||
|
|||||||
@@ -272,6 +272,9 @@ func (g *Gateway) copyAndCapture(c *gin.Context, ch *store.Channel, r io.Reader,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_, _ = c.Writer.Write(out)
|
_, _ = c.Writer.Write(out)
|
||||||
|
if _, ok := c.Get("raw_request"); ok {
|
||||||
|
c.Set("raw_response", string(data)) // 上游原始响应(未转换)
|
||||||
|
}
|
||||||
g.finishUsage(c, ch, start, store.UsageStatusSuccess, "")
|
g.finishUsage(c, ch, start, store.UsageStatusSuccess, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,10 +286,24 @@ func (g *Gateway) streamCopy(c *gin.Context, ch *store.Channel, r io.Reader, sta
|
|||||||
flusher = nopFlusher{}
|
flusher = nopFlusher{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 原始响应捕获:仅管理员且开关开启(raw_request 已 set)时累积上游原始行
|
||||||
|
_, capture := c.Get("raw_request")
|
||||||
|
var rawResp strings.Builder
|
||||||
|
|
||||||
|
// commitRaw 在记账前把已累积的原始响应写入 context
|
||||||
|
commitRaw := func() {
|
||||||
|
if capture {
|
||||||
|
c.Set("raw_response", rawResp.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
scanner := newSSEScanner(r)
|
scanner := newSSEScanner(r)
|
||||||
for {
|
for {
|
||||||
line, err := scanner.Next()
|
line, err := scanner.Next()
|
||||||
if line != nil {
|
if line != nil {
|
||||||
|
if capture {
|
||||||
|
rawResp.Write(line)
|
||||||
|
}
|
||||||
out := line
|
out := line
|
||||||
if lineConv != nil {
|
if lineConv != nil {
|
||||||
out = lineConv(line)
|
out = lineConv(line)
|
||||||
@@ -294,6 +311,7 @@ func (g *Gateway) streamCopy(c *gin.Context, ch *store.Channel, r io.Reader, sta
|
|||||||
if out != nil {
|
if out != nil {
|
||||||
if _, werr := w.Write(out); werr != nil {
|
if _, werr := w.Write(out); werr != nil {
|
||||||
// 客户端意外断开:按已生成部分收费(canceled)
|
// 客户端意外断开:按已生成部分收费(canceled)
|
||||||
|
commitRaw()
|
||||||
g.finishUsage(c, ch, start, store.UsageStatusCanceled, "client_disconnect")
|
g.finishUsage(c, ch, start, store.UsageStatusCanceled, "client_disconnect")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -307,6 +325,7 @@ func (g *Gateway) streamCopy(c *gin.Context, ch *store.Channel, r io.Reader, sta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
commitRaw()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
g.finishUsage(c, ch, start, store.UsageStatusSuccess, "")
|
g.finishUsage(c, ch, start, store.UsageStatusSuccess, "")
|
||||||
} else if c.Request.Context().Err() != nil {
|
} else if c.Request.Context().Err() != nil {
|
||||||
@@ -578,6 +597,14 @@ func (g *Gateway) finishUsage(c *gin.Context, ch *store.Channel, start time.Time
|
|||||||
chID = ch.ID
|
chID = ch.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rawReq, rawResp string
|
||||||
|
if v, ok := c.Get("raw_request"); ok {
|
||||||
|
rawReq, _ = v.(string)
|
||||||
|
}
|
||||||
|
if v, ok := c.Get("raw_response"); ok {
|
||||||
|
rawResp, _ = v.(string)
|
||||||
|
}
|
||||||
|
|
||||||
// 密钥今日 token 用量累计(配额检查用)
|
// 密钥今日 token 用量累计(配额检查用)
|
||||||
if g.lim != nil && kidVal > 0 {
|
if g.lim != nil && kidVal > 0 {
|
||||||
g.lim.AddTokens(kidVal, in+out)
|
g.lim.AddTokens(kidVal, in+out)
|
||||||
@@ -603,6 +630,8 @@ func (g *Gateway) finishUsage(c *gin.Context, ch *store.Channel, start time.Time
|
|||||||
LatencyMS: latency,
|
LatencyMS: latency,
|
||||||
Status: status,
|
Status: status,
|
||||||
ErrorCode: errCodePtr,
|
ErrorCode: errCodePtr,
|
||||||
|
RawRequest: rawReq,
|
||||||
|
RawResponse: rawResp,
|
||||||
CreatedAt: time.Now().UTC(),
|
CreatedAt: time.Now().UTC(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ type UsageLog struct {
|
|||||||
LatencyMS int `json:"latency_ms"`
|
LatencyMS int `json:"latency_ms"`
|
||||||
Status string `gorm:"size:16;not null" json:"status"`
|
Status string `gorm:"size:16;not null" json:"status"`
|
||||||
ErrorCode *string `json:"error_code,omitempty"`
|
ErrorCode *string `json:"error_code,omitempty"`
|
||||||
|
RawRequest string `gorm:"type:text" json:"raw_request"` // 客户端原始请求体(未转换)
|
||||||
|
RawResponse string `gorm:"type:text" json:"raw_response"` // 上游原始响应(未转换;流式为全部 SSE 事件)
|
||||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ export interface UsageLog {
|
|||||||
latency_ms: number
|
latency_ms: number
|
||||||
status: string
|
status: string
|
||||||
error_code: string | null
|
error_code: string | null
|
||||||
|
raw_request?: string
|
||||||
|
raw_response?: string
|
||||||
created_at: string
|
created_at: string
|
||||||
user?: string
|
user?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { protocolShort } from '@/lib/protocol'
|
|||||||
import { fmtCost, fmtTime } from '@/lib/format'
|
import { fmtCost, fmtTime } from '@/lib/format'
|
||||||
import Badge from '@/components/ui/Badge.vue'
|
import Badge from '@/components/ui/Badge.vue'
|
||||||
import Button from '@/components/ui/Button.vue'
|
import Button from '@/components/ui/Button.vue'
|
||||||
|
import Modal from '@/components/ui/Modal.vue'
|
||||||
import type { UsageLog } from '@/types'
|
import type { UsageLog } from '@/types'
|
||||||
|
|
||||||
const toast = useToastStore()
|
const toast = useToastStore()
|
||||||
@@ -15,6 +16,14 @@ const page = ref(1)
|
|||||||
const modelFilter = ref('')
|
const modelFilter = ref('')
|
||||||
const pageSize = 15
|
const pageSize = 15
|
||||||
|
|
||||||
|
const viewing = ref<UsageLog | null>(null)
|
||||||
|
const viewTab = ref<'request' | 'response'>('request')
|
||||||
|
|
||||||
|
function openRaw(l: UsageLog) {
|
||||||
|
viewing.value = l
|
||||||
|
viewTab.value = 'request'
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
try {
|
try {
|
||||||
const { data } = await http.get(
|
const { data } = await http.get(
|
||||||
@@ -77,6 +86,7 @@ onMounted(load)
|
|||||||
<span class="mono-num">{{ l.input_tokens }}/{{ l.output_tokens }} tok</span>
|
<span class="mono-num">{{ l.input_tokens }}/{{ l.output_tokens }} tok</span>
|
||||||
<span class="mono-num">{{ l.latency_ms }}ms</span>
|
<span class="mono-num">{{ l.latency_ms }}ms</span>
|
||||||
<span class="mono-num w-full">{{ fmtTime(l.created_at) }}</span>
|
<span class="mono-num w-full">{{ fmtTime(l.created_at) }}</span>
|
||||||
|
<Button v-if="l.raw_request" size="sm" variant="ghost" class="ml-auto" @click="openRaw(l)">查看原始</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="logs.length === 0" class="card px-4 py-8 text-center text-sm text-muted">暂无请求记录</p>
|
<p v-if="logs.length === 0" class="card px-4 py-8 text-center text-sm text-muted">暂无请求记录</p>
|
||||||
@@ -95,6 +105,7 @@ onMounted(load)
|
|||||||
<th scope="col" class="px-4 py-2.5 font-medium">耗时</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">耗时</th>
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">状态</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">状态</th>
|
||||||
<th scope="col" class="px-4 py-2.5 font-medium">时间</th>
|
<th scope="col" class="px-4 py-2.5 font-medium">时间</th>
|
||||||
|
<th scope="col" class="px-4 py-2.5 font-medium">原始</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -109,9 +120,13 @@ onMounted(load)
|
|||||||
<Badge :variant="l.status === 'success' ? 'ok' : l.status === 'canceled' ? 'neutral' : 'err'">{{ l.status }}</Badge>
|
<Badge :variant="l.status === 'success' ? 'ok' : l.status === 'canceled' ? 'neutral' : 'err'">{{ l.status }}</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ fmtTime(l.created_at) }}</td>
|
<td class="px-4 py-2.5 font-mono text-xs text-muted">{{ fmtTime(l.created_at) }}</td>
|
||||||
|
<td class="px-4 py-2.5">
|
||||||
|
<Button v-if="l.raw_request" size="sm" variant="ghost" @click="openRaw(l)">查看</Button>
|
||||||
|
<span v-else class="text-xs text-muted">-</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="logs.length === 0">
|
<tr v-if="logs.length === 0">
|
||||||
<td colspan="8" class="px-4 py-10 text-center text-sm text-muted">暂无请求记录</td>
|
<td colspan="9" class="px-4 py-10 text-center text-sm text-muted">暂无请求记录</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -124,5 +139,42 @@ onMounted(load)
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal :open="!!viewing" title="原始请求与响应" width="max-w-3xl" @close="viewing = null">
|
||||||
|
<div v-if="viewing" class="space-y-3">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="text-xs text-muted">
|
||||||
|
<span class="font-mono text-ink">{{ viewing.model }}</span>
|
||||||
|
<span class="mx-1.5">·</span>
|
||||||
|
<span class="font-mono">{{ viewing.user || '-' }}</span>
|
||||||
|
<span class="mx-1.5">·</span>
|
||||||
|
<span class="mono-num">{{ fmtTime(viewing.created_at) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-1 rounded-md border border-edge p-0.5">
|
||||||
|
<button
|
||||||
|
class="rounded px-2.5 py-1 text-xs transition"
|
||||||
|
:class="viewTab === 'request' ? 'bg-surface2 text-ink' : 'text-muted'"
|
||||||
|
@click="viewTab = 'request'"
|
||||||
|
>
|
||||||
|
请求
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded px-2.5 py-1 text-xs transition"
|
||||||
|
:class="viewTab === 'response' ? 'bg-surface2 text-ink' : 'text-muted'"
|
||||||
|
@click="viewTab = 'response'"
|
||||||
|
>
|
||||||
|
响应
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
v-if="viewTab === 'request' ? viewing.raw_request : viewing.raw_response"
|
||||||
|
class="max-h-[60vh] overflow-auto rounded-md border border-edge bg-surface p-3 font-mono text-xs leading-relaxed text-ink whitespace-pre-wrap break-all"
|
||||||
|
>{{ viewTab === 'request' ? viewing.raw_request : viewing.raw_response }}</pre>
|
||||||
|
<p v-else class="rounded-md border border-edge bg-surface p-4 text-center text-xs text-muted">
|
||||||
|
该请求未记录{{ viewTab === 'request' ? '原始请求' : '原始响应' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user