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