feat(gateway): 配置只读事实面 + 后台思考行「档位/穿透」(契约 v0.68)

- 新增 GET /api/v1/admin/config-facts:记忆整理 7 键 + 安静时段 2 键的**进程生效值** + 声明默认值(反射读 default 标签,不读 .env)
- 面板:记忆整理卡 + 安静时段卡(Miuix 只读),文案按新语义「只服务主动搭话,不静默回复」
- 后台思考行加 urgency(**只能是下限**,存档不可分 high/critical)+ quietHoursBypassed(与既有 bypassed 是两件事)
- 红线实测:往响应里塞 JWT ⇒ TestAdminConfigFacts_NeverLeaksSecretValues 打红(连断言消息都把整个响应打出来了)
- 前端:configFacts 24 checks / ConfigFactsCards 29 / BackthinkUrgencyTags 19 全过
- 契约 v0.68(纯增量:B12 新端点 + B8 两字段 + §4.10 形状与语义 + §7.4-17 下限限定)
This commit is contained in:
AskaEth committed 2026-10-05 13:54:56 +08:00
1 parent a057a0f61f
commit 34cef95e17
25 files changed
+1741 -14

No files matched your search

+1
View File
@@ -113,6 +113,7 @@ export type { ChannelActionResult, ChannelConfigSaveResult } from "./api/adminCh
export {
adminBackthink,
adminConfigFacts,
adminListNotifications,
adminObservabilityAudit,
adminObservabilityLogs,
+9
View File
@@ -7,6 +7,7 @@ import type {
AdminStorageStatus,
AdminTraceSpan,
AuditRecord,
AdminConfigFacts,
BackthinkItem,
SystemBackupResp,
SystemRestoreResp,
@@ -45,6 +46,14 @@ export function adminBackthink(limit?: number): Promise<BackthinkItem[]> {
return request<BackthinkItem[]>(`/admin/backthink${limitQuery(limit)}`);
}
/** GET /api/v1/admin/config-facts — **只读**配置事实面(契约 v0.68 §4.10(a)):
* 记忆整理 7 键 + 安静时段两键的**进程生效值** + 声明默认值,外加安静时段窗口的
* 解释(跨午夜 / 已关闭)。owner/admin 档;**不含任何凭据值**(后端有哨兵测试钉死),
* 网关也**不读 `.env`** —— 值就是运行实例真正在用的数。 */
export function adminConfigFacts(): Promise<AdminConfigFacts> {
return request<AdminConfigFacts>("/admin/config-facts");
}
/** GET /api/v1/admin/notifications?type=&before=&limit= — the unified notification
* feed (design/10 §3 #16: audit + channel + backthink + system aggregated).
* `type` omitted = all types, `before` = createdAt(ms) cursor for older pages,
+1
View File
@@ -12,3 +12,4 @@ export * from "./types/admin";
export * from "./types/adminWrite";
export * from "./types/kook";
export * from "./types/notification";
export * from "./types/configFacts";
+11 -1
View File
@@ -42,7 +42,17 @@ export interface BackthinkItem {
route?: string;
/** 打扰决策或投递的简短原因(后端 PushResult.Detail),例如 quiet hours、无活跃客户端。 */
detail?: string;
/** 事件驱动推送绕过了限频闸(后端 PushResult.Bypassed,"该发就发")。 */
/** 事件驱动推送绕过了限频闸(后端 PushResult.Bypassed,"该发就发")。
* ⚠️ 与 `quietHoursBypassed`(安静时段穿透)**是两件事**:这个说的是限频门。 */
bypassed?: boolean;
/** 该轮紧急度的**存档可证下限**(后端 `urgency`,`internal/urgency` 档位词汇:
* `routine` | `warning` | `critical`)。`warning` 读作「至少 high 档(可能更高)」。
* 缺席 = 后端没有这个事实(轮次不在安静时段窗口内时无从反推),**不是**「例常」。
* ⚠️ 实时帧走的是 bus 上原始的 `backthink.Record`,它目前**不带**档位 ⇒ 实时行显示
* 「档位未知」,重开页面由历史拉取(`GET /admin/backthink`)补齐。 */
urgency?: string;
/** 该轮**穿透了安静时段**(轮次时间落在安静时段窗口内且该轮判定要推送)。后端只在
* `true` 时下发该键;缺席 = 没有这件事,**不等于**「确认没穿透」。 */
quietHoursBypassed?: boolean;
at: number;
}
+33
View File
@@ -0,0 +1,33 @@
// 管理面「配置事实」的线上类型(契约 v0.68 §4.10(a))。
// 唯一真源:internal/services/gateway/admin_config_facts.go。
// 后端只回**非敏感调参**(记忆整理 7 键 + 安静时段 2 键)——响应里没有、也不会
// 有凭据值,因此这里也刻意不存在任何 token / key / secret 字段。
export interface AdminConfigFact {
/** env 键名,与 `.env.example` 逐字一致(后端有测试钉住)。 */
key: string;
/** **进程生效值**(已解析后的值,不是 `.env` 原文)。 */
value: string;
/** 该键的声明默认值(`internal/config` 的 `default` 标签)。 */
default: string;
/** 生效值是否等于声明默认值:true 显示「默认」,false 显示「已改」。
* ⚠️ 它**不是**「`.env` 没写过这个键」的断言(网关不读 `.env`)。 */
isDefault: boolean;
}
export interface AdminConfigQuietHours {
startHour: number;
endHour: number;
/** 窗口已关闭(两个相等的**非 0** 钟点):此时安静时段**从不生效**。 */
disabled: boolean;
/** 后端给的显示文案:`"23:00-07:00"` 或 `"已关闭"`。 */
label: string;
/** 窗口两端都被写成 0 ⇒ 视为「未填」,运行时回落到默认窗口(后端已按回落后的
* 钟点填 startHour/endHour)。0/0 **不是**「关闭」——关闭要写两个相等的非 0 值。 */
fallbackToDefault?: boolean;
}
export interface AdminConfigFacts {
facts: AdminConfigFact[];
quietHours: AdminConfigQuietHours;
}
+21 -1
View File
@@ -301,7 +301,27 @@ export function parseBackthinkItem(v: unknown): BackthinkItem | null {
const detail = firstPushText(pr.Detail, pr.detail, r.detail, r.push_detail);
const bypassedRaw = pr.Bypassed ?? pr.bypassed ?? r.bypassed ?? r.push_bypassed;
const bypassed = typeof bypassedRaw === "boolean" ? bypassedRaw : undefined;
return { id: String(idRaw), trigger, conclusion, shouldNotify, pushed, route, detail, bypassed, at };
// v0.68 的档位/穿透:**防御性读取**(兼容 camelCase 与 Go 字段名)。当前 bus 上发布的
// 是原始 `backthink.Record`,它**不带**这两个字段 ⇒ 今天恒为 undefined(本次会话新到
// 的行显示「档位未知」,重开页面由历史拉取补真值)。一旦后端把这两个事实放进帧里,
// 这里无需改动就会显示出来 —— 但绝不因为「帧里没有」就编一个值。
const urgencyRaw = r.urgency ?? r.Urgency ?? r.urgency_level ?? r.urgencyLevel;
const urgency = typeof urgencyRaw === "string" && urgencyRaw ? urgencyRaw : undefined;
const qhRaw = r.quietHoursBypassed ?? r.QuietHoursBypassed;
const quietHoursBypassed = typeof qhRaw === "boolean" ? qhRaw : undefined;
return {
id: String(idRaw),
trigger,
conclusion,
shouldNotify,
pushed,
route,
detail,
bypassed,
urgency,
quietHoursBypassed,
at,
};
}
/** 取 PushResult 文本字段的第一个非空值(先 Go 字段名,再 camelCase)。空串按