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,64 @@
import type { AdminChannelRecord } from "../../../lib/types";
import { PLATFORM_FIELDS } from "../channelFields";
import type { ChannelConfigInitial } from "../channelConfigForm";
/** Platform part of an adapter name ("kook:conf0" → "kook"). */
export function platformOf(name: string): string {
return name.split(":")[0];
}
/** Adapter (platform:account) name for one account config. The bridge registers a
* channels.json account as "kook:conf0" (env single-account = bare platform), so a
* config id of "default"/"" maps to the bare platform name. */
export function adapterNameOf(platform: string, accountId?: string): string {
const id = accountId?.trim();
return id && id !== "default" ? `${platform}:${id}` : platform;
}
/** Open-config target: the platform + the prefill bundle for its form. */
export interface CfgTarget {
platform: string;
initial: ChannelConfigInitial;
}
/** Build the config-form prefill from a platform name and the channel records.
* Do NOT put the masked placeholder into `token`: it would be written back as
* the real bot_token (overwriting the stored secret with a demo string). Keep
* token empty (save omits it → keeps the existing secret) and show a hint. */
export function buildCfgTarget(name: string, records: AdminChannelRecord[]): CfgTarget {
const plat = platformOf(name);
const acct = records.find((c) => platformOf(c.name) === plat)?.accounts?.[0];
const acctCfg = acct?.config ?? {};
const hasToken = typeof acctCfg.bot_token === "string" && acctCfg.bot_token && !acctCfg.bot_token.includes("(已配置");
const platVals: Record<string, string> = {};
for (const f of PLATFORM_FIELDS[plat] ?? []) {
const v = acctCfg[f.key];
platVals[f.key] = v == null ? "" : String(v);
}
return {
platform: plat,
initial: {
accountId: acct?.id ?? "",
tokenHint: hasToken ? "已配置(仅掩码显示),留空则不改动;如需更换请重新输入。" : "",
baseURL: typeof acctCfg.base_url === "string" ? acctCfg.base_url : "",
platVals,
allowGroups: (acct?.allowGroups ?? []).join(","),
denyGroups: (acct?.denyGroups ?? []).join(","),
allowUsers: (acct?.allowUsers ?? []).join(","),
denyUsers: acct?.denyUsers ? (Array.isArray(acct.denyUsers) ? acct.denyUsers.join(",") : "") : "",
groupMsgPerMin: acctCfg.groupMsgPerMin == null ? "" : String(acctCfg.groupMsgPerMin),
msgPerMin: acctCfg.msgPerMin == null ? "" : String(acctCfg.msgPerMin),
maxMessages: acctCfg.maxMessages == null ? "" : String(acctCfg.maxMessages),
},
};
}
/** The set of platforms that have at least one configured account. */
export function configuredPlatforms(records: AdminChannelRecord[]): AdminChannelRecord[] {
return records.filter((c) => (c.accounts?.length ?? 0) > 0);
}
/** Total configured accounts across all platforms. */
export function configuredAccountCount(records: AdminChannelRecord[]): number {
return configuredPlatforms(records).reduce((n, c) => n + (c.accounts?.length ?? 0), 0);
}