feat(monitor): 30-day raw retention + timeline 4-tier style + CC template seed + JSON format button
- History retention 1d → 30d(60s × 30d ≈ 43200 行/model,PG 无压力); ComputeAvailability* 不再 UNION rollup 表,直接扫 histories 精度更高。 - Timeline bar 四级高度+颜色双重编码:operational 高+绿 / degraded 中+黄 / failed+error 短+红 / 未测试 很短+灰。 - migration 113 seed「Claude Code 伪装」模板(ON CONFLICT DO NOTHING)。 user_id 用 legacy 格式(user_<64hex>_account_<uuid>_session_<uuid>), 避免新版 JSON 字符串内嵌 JSON 在编辑器里一长串 \" 难读。 - MonitorAdvancedRequestConfig 加「格式化」按钮 + white-space:pre 让 body textarea 对长字符串不压扁。
This commit is contained in:
@@ -38,12 +38,24 @@
|
||||
|
||||
<!-- Body JSON (仅当 mode != off) -->
|
||||
<div v-if="bodyOverrideMode !== 'off'">
|
||||
<label class="input-label">{{ t('admin.channelMonitor.advanced.bodyJson') }}</label>
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<label class="input-label !mb-0">{{ t('admin.channelMonitor.advanced.bodyJson') }}</label>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-primary-600 hover:underline disabled:cursor-not-allowed disabled:text-gray-400 disabled:no-underline dark:text-primary-400"
|
||||
:disabled="!bodyText.trim()"
|
||||
@click="formatBody"
|
||||
>
|
||||
{{ t('admin.channelMonitor.advanced.bodyJsonFormat') }}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="bodyText"
|
||||
rows="8"
|
||||
rows="10"
|
||||
:placeholder="bodyPlaceholder"
|
||||
class="input font-mono text-xs"
|
||||
style="white-space: pre; overflow-wrap: normal; overflow-x: auto;"
|
||||
spellcheck="false"
|
||||
@blur="commitBody"
|
||||
/>
|
||||
<p v-if="bodyError" class="mt-1 text-xs text-red-500">{{ bodyError }}</p>
|
||||
@@ -158,6 +170,25 @@ function commitBody() {
|
||||
}
|
||||
}
|
||||
|
||||
function formatBody() {
|
||||
const trimmed = bodyText.value.trim()
|
||||
if (trimmed === '') return
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
bodyText.value = JSON.stringify(parsed, null, 2)
|
||||
bodyError.value = ''
|
||||
// 同步把校验过的对象提交,避免格式化后焦点未移走时父组件读到旧值
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
emit('update:bodyOverride', parsed as Record<string, unknown>)
|
||||
}
|
||||
} catch (e) {
|
||||
bodyError.value =
|
||||
t('admin.channelMonitor.advanced.bodyJsonError') +
|
||||
': ' +
|
||||
(e instanceof Error ? e.message : String(e))
|
||||
}
|
||||
}
|
||||
|
||||
function serializeBody(body: Record<string, unknown> | null): string {
|
||||
if (!body || Object.keys(body).length === 0) return ''
|
||||
return JSON.stringify(body, null, 2)
|
||||
|
||||
@@ -59,19 +59,21 @@ interface Bar {
|
||||
title: string
|
||||
}
|
||||
|
||||
// 4 级高度 + 颜色双重编码:高=好+绿,短=坏+红,灰=未测试。
|
||||
// 长绿(正常) > 中黄(降级) > 短红(失败/系统错误) > 很短灰(未测试)。
|
||||
const STATUS_HEIGHT: Record<string, number> = {
|
||||
operational: 100,
|
||||
degraded: 70,
|
||||
failed: 55,
|
||||
degraded: 65,
|
||||
failed: 35,
|
||||
error: 35,
|
||||
empty: 20,
|
||||
empty: 15,
|
||||
}
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
operational: 'bg-emerald-500',
|
||||
degraded: 'bg-amber-500',
|
||||
failed: 'bg-red-500',
|
||||
error: 'bg-gray-400 dark:bg-dark-500',
|
||||
error: 'bg-red-500',
|
||||
empty: 'bg-gray-300 dark:bg-dark-600',
|
||||
}
|
||||
|
||||
|
||||
@@ -2173,6 +2173,7 @@ export default {
|
||||
bodyModeHintMerge: 'Shallow-merge with the default body; user fields win but model / messages / contents are protected (use Replace to change those).',
|
||||
bodyModeHintReplace: 'Use the JSON below as the complete body. Challenge validation is skipped; HTTP 2xx + non-empty response text is treated as operational.',
|
||||
bodyJson: 'Body JSON',
|
||||
bodyJsonFormat: 'Format',
|
||||
bodyJsonHint: 'Parsed on blur. Empty means no override.',
|
||||
bodyJsonError: 'JSON parse failed',
|
||||
bodyJsonObjectError: 'Body must be a JSON object (no arrays or primitives)'
|
||||
|
||||
@@ -2252,6 +2252,7 @@ export default {
|
||||
bodyModeHintMerge: '与默认请求体浅合并,用户字段优先;但 model / messages / contents 会被保护不允许覆盖(动这些字段请用「覆盖」模式)。',
|
||||
bodyModeHintReplace: '完全用下方 JSON 作为请求体。注意:此模式下跳过 challenge 校验,改为 HTTP 2xx + 响应文本非空即视为可用。',
|
||||
bodyJson: 'Body JSON',
|
||||
bodyJsonFormat: '格式化',
|
||||
bodyJsonHint: '失焦时自动解析校验。留空等价于没有覆盖。',
|
||||
bodyJsonError: 'JSON 解析失败',
|
||||
bodyJsonObjectError: '请求体必须是一个 JSON 对象(不能是数组或基本类型)'
|
||||
|
||||
Reference in New Issue
Block a user