fix(channelbridge): IM 发送过滤 action/narration + <p>拆多气泡 + 0.5-2s 间隔
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

把回复转发给 IM 适配器(sendReply)时剥掉 <action>/<narration> 动作/内心独白(Web 端
才渲染灰斜体,IM 只能收纯文本),并按 <p> 段落拆成多条气泡分别发送;多气泡发送间隔
随机 0.5~2s(仅 IM,Web 前端不延迟)。新增 sanitize.go stripNarration/splitBubbles/
bubbleGap + 测试。
This commit is contained in:
Aska committed 2026-09-07 17:50:56 +08:00
1 parent dcb0b33954
commit 5a0c1e2fd1
11 files changed
+790 -12

No files matched your search

@@ -0,0 +1,80 @@
import { useState } from "react";
import { adminChannelTest } from "../../../lib/api";
import type { ChannelTestResult } from "../../../lib/types";
import { useAuditStore } from "../../audit";
import { Button, Surface, Tag } from "../../../components/primitives";
/** Structural identity test (design/08: actor metadata × owner whitelist — never a
* prompt/LLM judgment). Self-contained so the list page and any adapter detail
* page can render it; `defaultPlatform` prefills when known. */
export function ChannelIdentityTest({
defaultPlatform = "",
disabled = false,
}: {
defaultPlatform?: string;
disabled?: boolean;
}): React.ReactNode {
const log = useAuditStore((s) => s.log);
const [platform, setPlatform] = useState(defaultPlatform);
const [ownerId, setOwnerId] = useState("");
const [group, setGroup] = useState(false);
const [verdict, setVerdict] = useState<ChannelTestResult | null>(null);
const [testing, setTesting] = useState(false);
const [err, setErr] = useState<string | null>(null);
const test = () => {
if (!ownerId.trim()) {
setErr("邮箱/账号 id 不能为空");
return;
}
setTesting(true);
setErr(null);
adminChannelTest({ platform: platform.trim(), accountId: ownerId.trim(), isGroup: group })
.then((v) => {
log("channel.identity-test", "ok");
setVerdict(v);
})
.catch((e: unknown) => {
log("channel.identity-test", "error");
setErr(e instanceof Error ? e.message : "身份测试失败");
})
.finally(() => setTesting(false));
};
return (
<Surface gap="sm" className="space-y-3">
<h3 className="text-sm font-semibold">身份识别测试(结构性判定 · 绝非提示词)</h3>
<p className="text-xs text-[var(--mu-text-lo)]">
输入渠道确认的发送者账号 id,后端用 channel 层 actor 元数据 × 主人白名单(configs/users/owner.yaml)判定;判定绝不依赖提示词 / LLM 自我判断。
</p>
<div className="flex flex-wrap items-center gap-2">
<input
value={platform}
onChange={(e) => setPlatform(e.target.value)}
placeholder="platform(如 kook,可留空)"
className="min-w-0 flex-1 rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-[var(--mu-accent)]"
/>
<input
value={ownerId}
onChange={(e) => setOwnerId(e.target.value)}
placeholder="渠道账号 id(发送者 id)"
className="min-w-0 flex-1 rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-[var(--mu-accent)]"
/>
<label className="flex items-center gap-1.5 text-xs text-[var(--mu-text-mid)]">
<input type="checkbox" checked={group} onChange={(e) => setGroup(e.target.checked)} className="accent-[var(--mu-accent)]" />
群聊
</label>
<Button variant="primary" disabled={disabled || testing} onClick={test}>
测试
</Button>
</div>
{err ? <Tag tone="err">{err}</Tag> : null}
{verdict ? (
<div className="space-y-1 rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-xs text-[var(--mu-text-mid)]">
<div>角色:{verdict.role} | 主人:{verdict.owner ? "是" : "否"} | 群聊:{verdict.isGroup ? "是" : "否"} | 环境:{verdict.env}</div>
<div>判定:{verdict.verdict}</div>
</div>
) : null}
</Surface>
);
}