Files
Cyrene_v2/frontend/web/src/admin/modules/configFacts.ts
T
AskaEth 34cef95e17 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 下限限定)
2026-10-05 13:54:56 +08:00

111 lines
4.6 KiB
TypeScript

import type { AdminConfigFact, AdminConfigFacts, AdminConfigQuietHours } from "../../lib/types";
/**
* 配置只读事实卡的**纯逻辑**(契约 v0.68 §4.10(a))。这里只做形状 → 文案的映射,
* 不发请求、不碰 DOM,因此可以用 plain-node 检查跑(见 configFacts.test.ts)。
*
* 三条不变量(与后端 admin_config_facts.go 同一口径):
* 1. 值一律取后端给的**进程生效值**,本层不推算、不格式化数值(只补单位/说明);
* 2. 「默认 / 已改」只由 `isDefault` 决定,**不**反推 `.env` 写过什么(面板读不到);
* 3. 后端没给的键如实显示「未报告」,**绝不**编一个值出来。
*/
/** 后端没给这条事实时的显示值(不是 0,也不是默认值)。 */
export const FACT_MISSING_VALUE = "未报告";
/** 记忆整理的 7 个键(与后端 configFactSpecs 的前 7 条同序)。 */
export const MEMORY_TIDY_KEYS: readonly string[] = [
"MEMORY_TIDY_ENABLED",
"MEMORY_TIDY_INTERVAL_SEC",
"MEMORY_TIDY_QUIET_START_HOUR",
"MEMORY_TIDY_QUIET_END_HOUR",
"MEMORY_TIDY_RECALL_THRESHOLD",
"MEMORY_TIDY_SCAN_LIMIT",
"MEMORY_TIDY_CANDIDATE_LIMIT",
];
/** 安静时段的两个键(后端事实表后两条)。 */
export const QUIET_HOURS_KEYS: readonly string[] = ["QUIET_START_HOUR", "QUIET_END_HOUR"];
export interface FactRow {
key: string;
/** 中文说明(这个键管什么)。 */
label: string;
/** 显示值(生效值 + 单位)。 */
value: string;
/** `"默认"` / `"已改"` / `"未报告"`。 */
state: string;
}
interface FactMeta {
label: string;
/** 值后缀(单位);空串表示不加。 */
unit?: string;
}
const FACT_META: Record<string, FactMeta> = {
MEMORY_TIDY_ENABLED: { label: "记忆整理总开关" },
MEMORY_TIDY_INTERVAL_SEC: { label: "整理周期", unit: " 秒" },
MEMORY_TIDY_QUIET_START_HOUR: { label: "整理低峰窗口起点", unit: " 时" },
MEMORY_TIDY_QUIET_END_HOUR: { label: "整理低峰窗口终点", unit: " 时" },
MEMORY_TIDY_RECALL_THRESHOLD: { label: "候选召回门(余弦)" },
MEMORY_TIDY_SCAN_LIMIT: { label: "每轮扫描上限" },
MEMORY_TIDY_CANDIDATE_LIMIT: { label: "每轮候选上限" },
QUIET_START_HOUR: { label: "安静时段起点(本地钟点)", unit: " 时" },
QUIET_END_HOUR: { label: "安静时段终点(本地钟点)", unit: " 时" },
};
/** factRows 把后端事实列表按给定键序映射成显示行;缺的键如实标「未报告」。 */
export function factRows(facts: AdminConfigFacts | null, keys: readonly string[]): FactRow[] {
const byKey = new Map<string, AdminConfigFact>();
for (const f of facts?.facts ?? []) byKey.set(f.key, f);
return keys.map((key) => {
const meta = FACT_META[key];
const got = byKey.get(key);
if (!got) {
return {
key,
label: meta ? meta.label : key,
value: FACT_MISSING_VALUE,
state: FACT_MISSING_VALUE,
};
}
return {
key,
label: meta ? meta.label : key,
value: `${got.value}${meta?.unit ?? ""}`,
state: got.isDefault ? "默认" : "已改",
};
});
}
/** 记忆整理事实行(7 条)。 */
export function memoryTidyRows(facts: AdminConfigFacts | null): FactRow[] {
return factRows(facts, MEMORY_TIDY_KEYS);
}
/** 安静时段事实行(2 条)。 */
export function quietHoursRows(facts: AdminConfigFacts | null): FactRow[] {
return factRows(facts, QUIET_HOURS_KEYS);
}
/** 安静时段窗口的一句话摘要(后端 label + 跨午夜 / 已关闭 / 「0/0 = 未填」的如实补充)。 */
export function quietHoursSummary(qh: AdminConfigQuietHours | null | undefined): string {
if (!qh) return FACT_MISSING_VALUE;
if (qh.disabled) return "已关闭(安静时段从不生效)";
const label = qh.label || `${qh.startHour}:00-${qh.endHour}:00`;
if (qh.fallbackToDefault) {
return `${label}(两端写 0 = 未填,运行时回落到默认窗口)`;
}
return qh.startHour > qh.endHour ? `${label}(跨午夜)` : label;
}
/**
* 安静时段的**用途文案**(主人裁决 10 的新语义)。
* ⚠️ 必须说清「只服务定时主动思考 → 主动搭话,不静默回复」——
* 写成「夜间全静默」是已废弃的旧实现语义,被这次改动明确否掉了。
*/
export function quietHoursPurpose(): string {
return "安静时段只服务「定时主动思考 → 主动搭话」:她在这个窗口里默认不主动找你。回复侧不受它约束——你任何时候说话,她都照常回,不会因为到了安静时段就被静默。群聊同样没有夜间静默。";
}