fix(channelbridge): IM 发送过滤 action/narration + <p>拆多气泡 + 0.5-2s 间隔
把回复转发给 IM 适配器(sendReply)时剥掉 <action>/<narration> 动作/内心独白(Web 端 才渲染灰斜体,IM 只能收纯文本),并按 <p> 段落拆成多条气泡分别发送;多气泡发送间隔 随机 0.5~2s(仅 IM,Web 前端不延迟)。新增 sanitize.go stripNarration/splitBubbles/ bubbleGap + 测试。
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user