feat(backthink,browser): 自排工具面 C1-3 + 浏览器两处偏离收口(X2+X3 同文件一并落)
X2 C1-3(自排工具面与提示词): - request_think(thinktool.go,照 transcode/tool.go 范式)+ reason 必填/urgency 闭集 + 双守门 (安全闸 + owner 判定:群聊/陌生人一律拒) - **成对**进两张唤醒态允许集(quiet + awake;CAPACITY_TOOL_SETS 是整体替换) - 提示词抽共用段落 + 自排前缀优先(不枚举工具清单);面板加 self_wake 标签与筛选轴 - 4 个变异自检(拿掉 owner 判定/只加一张表/放宽 urgency/前端标签恒等) X3 收口(与 X2 共用 wire_services.go ⇒ 一并落): - 删掉第三份 SSRF 实现(guard.go),改由 io.ValidateRequestURL 唯一判定 + 薄门面(保两个错误身份) - 浏览器会话挂进 a.close(真机:真 Chrome 被关、无残留、生产未扰动) - a.wireBrowser() 归位到 wire_runtime.go,删掉 ioToolOptions 的临时挂点 - 5 个变异自检 顺带补提交 C1-2 漏掉的 internal/config/runtimeconfig/backthink_test.go(ed50053 漏 add)。
This commit is contained in:
1 parent
2bf3b536dd
commit
2d211c2d16
20 files changed
+1568
-176
No files matched your search
@@ -23,6 +23,42 @@ const PUSH_ROUTE_LABEL: Record<string, string> = {
|
||||
none: "无投递目标",
|
||||
};
|
||||
|
||||
/** 触发源的中文标签(后端 `Record.Trigger` / `Trigger.Source` 的取值域,design/12 §C.6)。
|
||||
* `self_wake` 是她自己排起的一轮(`request_think`);其余是周期性轮与事件主题。 */
|
||||
export const TRIGGER_LABEL: Record<string, string> = {
|
||||
heartbeat: "例行心跳",
|
||||
startup: "启动问候",
|
||||
self_wake: "自我唤醒",
|
||||
};
|
||||
|
||||
/** 触发源的显示文本:**未知取值原样返回**,与 `PUSH_ROUTE_LABEL` 同口径 —— 绝不猜一个
|
||||
* 更好听的词,也绝不把不认识的取值藏起来(事件主题 `iot.device_update` 等就走这一条)。
|
||||
* 抽成函数是为了让「未知原样显示」这条口径可被直接钉住,而不是靠读 JSX 里的 `??`。 */
|
||||
export function triggerLabel(trigger: string): string {
|
||||
return TRIGGER_LABEL[trigger] ?? trigger;
|
||||
}
|
||||
|
||||
/** 触发源筛选轴。它与「打扰」筛选轴**取交集**,两轴语义互斥、不挤进同一个 enum。 */
|
||||
export type TriggerFilter = "all" | "self_wake" | "heartbeat";
|
||||
|
||||
/** 打扰筛选轴的取值。 */
|
||||
export type DisturbFilter = "all" | "notify" | "silent";
|
||||
|
||||
/** 一行是否通过**两轴**筛选(纯函数:组件只做接线,判定可以直接钉住)。
|
||||
*
|
||||
* 两轴取**交集**而不是并集,也不是把四个语义挤进一个 enum —— 「静默 + 自我唤醒」
|
||||
* 这类组合因此是可表达的。筛选作用在**客户端已拉取的那一页**上(见下方 Notice)。 */
|
||||
export function backthinkVisible(
|
||||
item: BackthinkItem,
|
||||
filter: DisturbFilter,
|
||||
triggerFilter: TriggerFilter
|
||||
): boolean {
|
||||
const notifyOK =
|
||||
filter === "all" ||
|
||||
(filter === "notify" ? item.shouldNotify === true : item.shouldNotify !== true);
|
||||
return notifyOK && (triggerFilter === "all" || item.trigger === triggerFilter);
|
||||
}
|
||||
|
||||
/** 推送路由的色调:真的送出去了才用 ok,被限频拦住是 warn。 */
|
||||
function pushRouteTone(route: string): "ok" | "warn" | "neutral" {
|
||||
if (route === "active_client" || route === "im_bridge") return "ok";
|
||||
@@ -30,7 +66,13 @@ function pushRouteTone(route: string): "ok" | "warn" | "neutral" {
|
||||
return "neutral";
|
||||
}
|
||||
|
||||
function BackthinkRow({
|
||||
/** 单行:时间 / 触发源标签 / 打扰与推送结果 / 紧急度标记 / 结论 / 链路导图入口。
|
||||
*
|
||||
* 导出是为了让「触发源标签」这条口径能被**真实渲染**直接钉住(design/12 §C.6 要求
|
||||
* `self_wake` 显示成「自我唤醒」、未知取值原样显示)—— 只喂 `item` prop 即可渲染;
|
||||
* 它从 store 读的三处(`flowGraphs` / `wsStatus` / `markBackthinkPushed`)取初始值即可
|
||||
* 工作,不依赖任何运行期数据。 */
|
||||
export function BackthinkRow({
|
||||
item,
|
||||
graphSource,
|
||||
}: {
|
||||
@@ -49,7 +91,7 @@ function BackthinkRow({
|
||||
<Surface className="space-y-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Tag tone="neutral">{new Date(item.at).toLocaleTimeString()}</Tag>
|
||||
<span className="font-medium">{item.trigger}</span>
|
||||
<span className="font-medium">{triggerLabel(item.trigger)}</span>
|
||||
<Tag tone={item.shouldNotify ? "warn" : "neutral"}>{item.shouldNotify ? "需要打扰" : "静默"}</Tag>
|
||||
<Tag tone={item.pushed ? "ok" : "neutral"}>{item.pushed ? "已推送" : "未推送"}</Tag>
|
||||
{item.route ? <Tag tone={pushRouteTone(item.route)}>{PUSH_ROUTE_LABEL[item.route] ?? item.route}</Tag> : null}
|
||||
@@ -130,7 +172,10 @@ export function BackthinkModule(): React.ReactNode {
|
||||
// 先只读拉一次 sess-backthink 的**最近一轮**图(否则每一行都只显示「尚未产生链路图」)。
|
||||
// 一次拉取在模块级做(不是每行一个 hook)—— 行数多时不会打出 N 个重复请求。
|
||||
const graphSource = useFlowSnapshot(BACKTHINK_SESSION_ID);
|
||||
const [filter, setFilter] = useState<"all" | "notify" | "silent">("all");
|
||||
const [filter, setFilter] = useState<DisturbFilter>("all");
|
||||
// 触发源是**独立的第二筛选轴**(design/12 §C.6):与「打扰」轴取交集,而不是挤进
|
||||
// 同一个 enum(那样「静默 + 自我唤醒」这类组合就不可表达了)。
|
||||
const [triggerFilter, setTriggerFilter] = useState<TriggerFilter>("all");
|
||||
const [interveneHint, setInterveneHint] = useState(false);
|
||||
// Cold-load persisted history once (design/12 §4); real-time updates come from the
|
||||
// WS `backthink` events already feeding useAppStore.backthink (head-inserted), so we
|
||||
@@ -140,13 +185,10 @@ export function BackthinkModule(): React.ReactNode {
|
||||
const byId = new Map<string, BackthinkItem>();
|
||||
for (const b of backthink) if (b.id) byId.set(b.id, b);
|
||||
for (const b of rec.data ?? []) if (b.id) byId.set(b.id, b);
|
||||
const items = Array.from(byId.values())
|
||||
const allItems = Array.from(byId.values());
|
||||
const items = allItems
|
||||
.sort((a, b) => (b.at ?? 0) - (a.at ?? 0))
|
||||
.filter((b) => {
|
||||
if (filter === "notify") return b.shouldNotify === true;
|
||||
if (filter === "silent") return b.shouldNotify !== true;
|
||||
return true;
|
||||
});
|
||||
.filter((b) => backthinkVisible(b, filter, triggerFilter));
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@@ -172,14 +214,34 @@ export function BackthinkModule(): React.ReactNode {
|
||||
|
||||
<QuietHoursFactsCard />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<FilterPill active={filter === "all"} onClick={() => setFilter("all")}>全部</FilterPill>
|
||||
<FilterPill active={filter === "notify"} onClick={() => setFilter("notify")}>需打扰</FilterPill>
|
||||
<FilterPill active={filter === "silent"} onClick={() => setFilter("silent")}>静默</FilterPill>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-12 shrink-0 text-xs text-[var(--mu-text-lo)]">打扰</span>
|
||||
<FilterPill active={filter === "all"} onClick={() => setFilter("all")}>全部</FilterPill>
|
||||
<FilterPill active={filter === "notify"} onClick={() => setFilter("notify")}>需打扰</FilterPill>
|
||||
<FilterPill active={filter === "silent"} onClick={() => setFilter("silent")}>静默</FilterPill>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-12 shrink-0 text-xs text-[var(--mu-text-lo)]">触发源</span>
|
||||
<FilterPill active={triggerFilter === "all"} onClick={() => setTriggerFilter("all")}>全部</FilterPill>
|
||||
<FilterPill active={triggerFilter === "self_wake"} onClick={() => setTriggerFilter("self_wake")}>自我唤醒</FilterPill>
|
||||
<FilterPill active={triggerFilter === "heartbeat"} onClick={() => setTriggerFilter("heartbeat")}>例行心跳</FilterPill>
|
||||
<span className="text-xs text-[var(--mu-text-lo)]">
|
||||
显示 {items.length} / {allItems.length} 条 · 两个筛选轴取交集,且只筛当前已拉取的这一页(不是全部历史)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<EmptyState icon={<Icon name="night" size={32} />} title="暂无后台思考" hint="后台任务完成时按 WS 事件实时呈现(backthink / thinking)。" />
|
||||
<EmptyState
|
||||
icon={<Icon name="night" size={32} />}
|
||||
title={allItems.length === 0 ? "暂无后台思考" : "当前筛选下没有匹配的记录"}
|
||||
hint={
|
||||
allItems.length === 0
|
||||
? "后台任务完成时按 WS 事件实时呈现(backthink / thinking)。"
|
||||
: `本页共 ${allItems.length} 条记录,没有一条落在当前筛选(打扰 / 触发源)里 —— 这不等于「这件事没发生」,换个筛选或看「全部」即可。`
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{items.map((b) => (
|
||||
@@ -189,6 +251,8 @@ export function BackthinkModule(): React.ReactNode {
|
||||
)}
|
||||
|
||||
<Notice>数据源:/ws/chat 下行 backthink / thinking 事件(实时,主驱动)+ 打开时一次拉取 GET /api/v1/admin/backthink 历史,两者按 id 去重、按时间倒序,新记录增量插入(不整页重渲)。「推送结果」的 route / detail / bypassed 两条路径同名同义:历史拉取取 AdminBackthinkItem 的同名键,实时帧取后端原始 PushResult 的 Route / Detail / Bypassed(解析器已读,见 lib/wsParse.ts#parseBackthinkItem,兼容 camelCase 变体),因此本次会话新到的行同样显示推送路由与推送说明,不再只显示「已推送/未推送」。链路导图取自后台思考会话(sess-backthink)的 graph_update 快照 —— 后端完整链路图始终发布,与管理端 Flow 页的「当前会话」图相互独立(裁决 16.4.2)。干预为后端 M10+ 提供,当前为只读展示。
|
||||
<br />
|
||||
触发源标签(契约 §C.6,形状不变、仅值域扩展):`self_wake` 显示为「自我唤醒」= 她**自己**用 `request_think` 给自己排起的一轮(不是心跳、也不是外部事件),`heartbeat` / `startup` 分别显示为「例行心跳」/「启动问候」;其余取值(`iot.device_update` / `reminder.trigger` / `system.event` 等事件主题)与将来新增的未知取值**一律原样显示**,绝不猜一个更好听的词。⚠️ 「触发源」筛选是**客户端**筛选,只作用于下表**已拉取的这一页**(历史最多 100 条 + 本次会话实时帧),因此「筛不到」不等于「没发生」;服务端 `?trigger=` 筛选与面板写路径属 C2,尚未提供。同理,「例行心跳」这一档只匹配 `heartbeat` 本身,`startup`(启动问候)只在「全部」下可见。
|
||||
<br />
|
||||
新增的「穿透安静时段 / 紧急度」标记(契约 v0.70):穿透 = 这一轮落在安静时段窗口内、仍然被放行发出(只有 high 及以上档位做得到);紧急度**三态画得不一样** —— 把下限当精确值、把未知当「例常」都是把猜当事实:① **精确档位**(「例常」/「紧急(高)」/「紧急·安全域」)= 该轮实际生效的档位,随记录落库;② **≥ 下限**(带 `urgencyLowerBound` 的旧行:该行没有存档档位,此值由安静时段窗口规则推导 —— 「≥ 高」只表示至少 high、可能更高);③ **档位未知** = 存档里没有这个事实,**不是**「例常」。⚠️ 实时帧带上记录里的精确档位(`backthink.Record` 自 X2a 起带 `urgency`),但**不带** `urgencyLowerBound` / `quietHoursBypassed`(后两者由网关按安静时段窗口推导)⇒ 本次会话新到的行先显示精确档位与推送结果,穿透标记与旧行的下限标记要等重开页面由历史拉取补齐。</Notice>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user