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:
1 parent
a057a0f61f
commit
34cef95e17
25 files changed
+1741
-14
No files matched your search
@@ -113,6 +113,7 @@ export type { ChannelActionResult, ChannelConfigSaveResult } from "./api/adminCh
|
||||
|
||||
export {
|
||||
adminBackthink,
|
||||
adminConfigFacts,
|
||||
adminListNotifications,
|
||||
adminObservabilityAudit,
|
||||
adminObservabilityLogs,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from "./types/admin";
|
||||
export * from "./types/adminWrite";
|
||||
export * from "./types/kook";
|
||||
export * from "./types/notification";
|
||||
export * from "./types/configFacts";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)。空串按
|
||||
|
||||
Reference in new issue
Block a user