feat: KOOK自动获取bot身份 + action/narration剥出气泡 + 渠道配置表单回填
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

agent-116 KOOK token即用+自动获取:
- kook/identity.go: Connect 调 GET /user/me 拿 bot 身份(bot_id/bot_username/bot_nickname), 失败容错
- 渠道列表 enrichAccountsWithBot 附 bot 身份; base_url空用默认/receive_mode默认ws(只填token可跑)

agent-117 action/narration 剥出气泡(主人需求: 动作/旁白不进气泡,按位置上/下):
- parseContent.splitBubble(content)->{body,head,tail}: body=纯文本(去标签), head=开头/中间action/narr(气泡上), tail=结尾(气泡下)
- renderContent 只渲染text(去内联灰斜体); chat.tsx BubbleNotes 泡外 *动作/旁白* 灰斜体小字(气泡外依附), SplitChatBubble 先剥splitBubble再splitParagraphs(body), <p>/\n\n/毛玻璃/SourceLabel不破坏
- 注: 非chat mode renderContent 现在丢弃action/narr(可接受)

Channels 配置表单回填(修save后输入框空):
- openConfig 从 data.accounts[0].config 回填 token(掩码提示)/base_url/平台字段/黑白名单/限流, 不再硬清空
全仓 build/vet/test绿+前端build绿
This commit is contained in:
Aska committed 2026-09-07 15:31:14 +08:00
1 parent bdd9d752e1
commit 65091f6497
15 files changed
+571 -83

No files matched your search

+53 -28
View File
@@ -34,12 +34,13 @@ interface PlatformField {
* are rendered for every platform separately; this map holds only what a given
* adapter additionally needs. Keys are the adapter Config json tags verbatim. */
const PLATFORM_FIELDS: Record<string, PlatformField[]> = {
kook: [
{ key: "receive_mode", label: "receive_mode(收包模式 ws/webhook)", placeholder: "ws" },
{ key: "webhook_path", label: "webhook_path(webhook 挂载路由)", placeholder: "/webhook/kook" },
{ key: "webhook_verify_token", label: "webhook_verify_token(校验 token)", secret: true },
{ key: "webhook_encrypt_key", label: "webhook_encrypt_key(AES 加密密钥)", secret: true },
],
// KOOK is "token 即用、其余自动获取": only the token is required. base_url
// defaults to the official root, receive_mode defaults to "ws" and webhook_path
// to "/webhook/kook", so none of them need a form field. Advanced values
// (receive_mode="webhook", webhook_path, webhook_verify_token, webhook_encrypt_key)
// are set through the "额外 config" JSON instead. We keep the (empty) entry so
// the panel still renders the KOOK-specific hint below.
kook: [],
"qq-obv11": [
{ key: "mode", label: "mode(client/server)", placeholder: "server" },
{ key: "listen_addr", label: "listen_addr(server 监听)", placeholder: "0.0.0.0:6700" },
@@ -132,18 +133,29 @@ export function ChannelsModule(): React.ReactNode {
const openConfig = (name: string) => {
setErr(null);
setNote(null);
setCfgPlatform(platformOf(name));
setCfgId("");
setToken("");
setBaseURL("");
setPlatVals({});
setAllowGroups("");
setDenyGroups("");
setAllowUsers("");
setDenyUsers("");
setGroupMsgPerMin("");
setMsgPerMin("");
setMaxMessages("");
const platform = platformOf(name);
setCfgPlatform(platform);
// Prefill from the backend-provided account (if already configured) so a
// saved config is visible again instead of the form opening blank. token is
// masked by the backend (never raw), so we only echo it as a hint.
const acct = (data ?? []).find((c) => platformOf(c.name) === platform)?.accounts?.[0];
setCfgId(acct?.id ?? "");
const cfg = acct?.config ?? {};
setToken(typeof cfg.bot_token === "string" && cfg.bot_token ? "(已配置,仅掩码显示)" : "");
setBaseURL(typeof cfg.base_url === "string" ? cfg.base_url : "");
const plat: Record<string, string> = {};
for (const f of PLATFORM_FIELDS[platform] ?? []) {
const v = cfg[f.key];
plat[f.key] = v == null ? "" : String(v);
}
setPlatVals(plat);
setAllowGroups((acct?.allowGroups ?? []).join(","));
setDenyGroups((acct?.denyGroups ?? []).join(","));
setAllowUsers((acct?.allowUsers ?? []).join(","));
setDenyUsers(acct?.denyUsers ? (Array.isArray(acct.denyUsers) ? acct.denyUsers.join(",") : "") : "");
setGroupMsgPerMin(cfg.groupMsgPerMin == null ? "" : String(cfg.groupMsgPerMin));
setMsgPerMin(cfg.msgPerMin == null ? "" : String(cfg.msgPerMin));
setMaxMessages(cfg.maxMessages == null ? "" : String(cfg.maxMessages));
setExtraConfig("");
};
@@ -253,16 +265,21 @@ export function ChannelsModule(): React.ReactNode {
render: (c) =>
c.accounts && c.accounts.length ? (
<span className="flex flex-wrap items-center gap-1">
{c.accounts.map((a) => (
<span key={a.id || "default"} className="flex items-center gap-1">
<Tag tone="neutral">{a.id || "default"}</Tag>
{a.id ? (
<Button size="sm" variant="ghost" disabled={busy !== null} onClick={() => del(platformOf(c.name), a.id)}>
删除
</Button>
) : null}
</span>
))}
{c.accounts.map((a) => {
// Show the connected bot's resolved identity (kook user/me) when the
// adapter fetched it, falling back to the account id.
const label = a.botNickname || (a.botUsername ? `@${a.botUsername}` : a.id || "default");
return (
<span key={a.id || "default"} className="flex items-center gap-1">
<Tag tone="neutral">{label}</Tag>
{a.id ? (
<Button size="sm" variant="ghost" disabled={busy !== null} onClick={() => del(platformOf(c.name), a.id)}>
删除
</Button>
) : null}
</span>
);
})}
</span>
) : (
<span className="text-xs text-[var(--mu-text-lo)]">—</span>
@@ -316,6 +333,14 @@ export function ChannelsModule(): React.ReactNode {
{cfgPlatform ? (
<Surface className="space-y-3">
<h3 className="text-sm font-semibold">配置账号 → {cfgPlatform}</h3>
{cfgPlatform === "kook" ? (
<p className="text-xs text-[var(--mu-text-lo)]">
KOOK 为「token 即用、其余自动获取」:仅需填 token(必填);base_url 留空即用官方默认;receive_mode 默认 ws、webhook_path 默认 /webhook/kook,均无需填写。若改用 webhook 或配校验/加密,在下方「额外 config」JSON 里设置,如
{" "}
{"{"}"receive_mode":"webhook","webhook_path":"/webhook/kook","webhook_verify_token":"...","webhook_encrypt_key":"..."{"}"}
(verify_token / encrypt_key 仅在 webhook 模式需要)。连接后渠道列表会显示该 KOOK bot 身份。
</p>
) : null}
<p className="text-xs text-[var(--mu-text-lo)]">
保存后写入 channels.json(本地、已 gitignore)并热更新 {cfgPlatform} 的渠道 adapter,无需重启进程。token 只在写文件时出现,列表/响应不回显裸值。
</p>