feat(sense): T-G7 审计+前端(live 分流接线 + tier 指标卡 + Settings 灰度区块)

- proxy/routes:sense.mode=live 时转发前调 Grader 分流,档位 -> 池条目
  (tier_pool_hint 映射),响应头 X-Campus-Tier 透出(可解释性);
  T1 决策按 review.sample_rate 抽样入 ReviewQueue(sense_t1 标签,
  人工核"判 T1 实为更高档");分级故障不影响代理可用性(D-G4)
- /api/metrics:sense 段(enabled/mode/观察数/已标签/档位分布/shadow 一致率)
- 前端:MetricsView「语义分级」卡片(观察/标签进度/一致率/晋升门提示);
  SettingsView「Sense 灰度控制」区块(总开关 + collect/shadow/live 切换 +
  标签进度),npm run build 产物更新
- 全量 410 passed
This commit is contained in:
tzt
2026-09-05 15:06:27 +08:00
parent 204789aed6
commit a636b989e5
7 changed files with 171 additions and 6 deletions
@@ -1,14 +1,14 @@
{
"schemaVersion": "mimosa-hook-status/v1",
"recordedAt": "2026-09-01T14:21:11.564Z",
"recordedAt": "2026-09-05T07:03:44.339Z",
"sessionId": "sess_e50d4f25-3ac6-43f2-b2f3-8833b3150465",
"event": "PostToolUse",
"toolName": "Edit",
"file": "src/views/AgentView.vue",
"file": "src/views/MetricsView.vue",
"outcome": "clear",
"coverage": "complete",
"findingCount": 0,
"durationMs": 7,
"durationMs": 2,
"hostState": "hook_complete",
"reportHint": ".mimosa/reports/"
}
+9
View File
@@ -114,4 +114,13 @@ export interface Metrics {
cache: Record<string, unknown>
v2?: Record<string, unknown>
review?: { pending: number; total: number }
sense?: {
enabled: boolean
mode: string
observations: number
labeled: number
by_decided_tier?: Record<string, number>
agreement?: number
min_labels?: number
}
}
+17
View File
@@ -58,6 +58,21 @@
<p class="hint">单价来自模型池条目$/1M tokens经典设置下的模型成本不计入</p>
</div>
<div v-if="sense && sense.observations" class="metric-card">
<h3>语义分级sense · {{ sense.mode }}</h3>
<div class="kv-list">
<span>观察数</span><b>{{ sense.observations }}</b>
<span>已标签</span><b>{{ sense.labeled }} / {{ sense.min_labels }}</b>
<span>一致率</span><b>{{ ((sense.agreement ?? 0) * 100).toFixed(1) }}%</b>
<template v-for="(v, k) in sense.by_decided_tier" :key="k">
<span> {{ k }}</span><b>{{ v }}</b>
</template>
</div>
<p class="hint">
晋升门一致率 85% + 标签 {{ sense.min_labels }} + 审计无误判collect 攒满前不开 live
</p>
</div>
<div v-if="data.review" class="metric-card review-card">
<h3>人工检验</h3>
<div class="review-stats">
@@ -115,6 +130,8 @@ const byModel = computed(() => {
return (v2?.by_model as Record<string, ModelBucket>) || null
})
const sense = computed(() => data.value?.sense || null)
async function load() {
loading.value = true
error.value = ''
+68 -1
View File
@@ -389,6 +389,45 @@
</div>
</section>
<!-- 🧠 语义分级sense灰度控制 -->
<section class="settings-section">
<div class="section-title">
<span>🧠 语义分级Sense · 三级任务分级</span>
<div class="title-right">
<span :class="['server-badge', senseCfg.enabled ? 'badge-running' : 'badge-stopped']">
{{ senseCfg.enabled ? `${senseCfg.mode}` : '○ 未启用' }}
</span>
</div>
</div>
<div class="section-body">
<p class="role-hint">
灰度纪律collect只记不决策攒满 {{ senseCfg.min_labels }} 条标签并过晋升门
一致率 85% + conformal 覆盖 α+2% + 审计无误判后才可开 live
</p>
<div class="form-grid">
<div class="field">
<label class="checkbox-label">
<input type="checkbox" v-model="senseCfg.enabled" @change="saveSense" />
启用语义分析器总开关改后保存生效
</label>
</div>
<div class="field">
<label>灰度模式</label>
<select v-model="senseCfg.mode" @change="saveSense"
:disabled="!senseCfg.enabled">
<option value="collect">collect 只采集观察</option>
<option value="shadow">shadow 决策只比对不执行</option>
<option value="live">live 真分流需晋升门通过</option>
</select>
</div>
<div class="field">
<label>观察/标签数min_labels={{ senseCfg.min_labels }}</label>
<input :value="`${senseCfg.obs} / ${senseCfg.labeled}`" disabled />
</div>
</div>
</div>
</section>
<!-- 操作按钮 -->
<div class="actions">
<button class="btn-primary" :disabled="saving || !dirty" @click="save">
@@ -412,6 +451,7 @@ import {
getLlamaStatus, listLocalModels, startLlama, stopLlama,
downloadModel, watchDownload,
getPool, upsertPoolEntry, deletePoolEntry, setPoolRoles, testPoolEntry, listPoolModels,
getMetrics,
} from '@/api'
import type {
ModelSettings, ModelInfo, LlamaStatus, LocalModel,
@@ -961,7 +1001,34 @@ async function reset() {
}
}
onMounted(load)
// 语义分级(sense)灰度
const senseCfg = reactive({ enabled: false, mode: 'collect', obs: 0, labeled: 0, min_labels: 500 })
async function loadSense() {
try {
const [cfg, m] = await Promise.all([getConfig(), getMetrics()])
const s = (cfg as any).sense || {}
senseCfg.enabled = !!s.enabled
senseCfg.mode = s.mode || 'collect'
senseCfg.min_labels = s.policy?.min_labels ?? 500
const sm = (m as any).sense || {}
senseCfg.obs = sm.observations ?? 0
senseCfg.labeled = sm.labeled ?? 0
} catch { /* 静默 */ }
}
async function saveSense() {
try {
await updateConfig({ sense: { enabled: senseCfg.enabled, mode: senseCfg.mode } } as any)
saveMsg.value = { ok: true, msg: `✅ 语义分级已${senseCfg.enabled ? '启用(' + senseCfg.mode + '' : '关闭'}` }
} catch (e: any) {
saveMsg.value = { ok: false, msg: '❌ 保存失败:' + (e?.response?.data?.detail || e?.message || e) }
await loadSense()
}
setTimeout(() => (saveMsg.value = null), 3000)
}
onMounted(() => { load(); loadSense() })
onUnmounted(() => { _dlSSE?.close() })
</script>