From 069d836dea7afeaae1efdac5fa5d14ddd0f2e9a5 Mon Sep 17 00:00:00 2001 From: Wxw-Gu <84499436+Wxw-Gu@users.noreply.github.com> Date: Mon, 21 Sep 2026 11:06:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=B2=BE=E7=A1=AE=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8C=96=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/wcdb/win32/x64/wcdb_api.dll | Bin 382976 -> 394240 bytes src/main/index.ts | 51 +++- src/main/services/automation-action-runner.ts | 69 ++++- .../automation-execution-log-service.ts | 14 +- src/main/services/automation-service.ts | 196 +++++++++++- src/main/services/message-listener-service.ts | 107 ++++--- .../services/personal-wechat-send-service.ts | 96 +++++- src/main/services/wechat-action-gateway.ts | 29 +- src/main/wcdb4-client.ts | 85 +++++- .../automation/AutomationWorkspace.tsx | 18 +- .../automation/ExecutionDetailDrawer.tsx | 3 +- .../features/automation/ExecutionLogPanel.tsx | 3 +- .../features/automation/RuleEditorPanel.tsx | 57 +++- .../src/features/automation/model/format.ts | 8 +- src/renderer/src/styles/automation.scss | 24 +- src/shared/automation.ts | 92 +++++- src/shared/wechat-action.ts | 12 + .../component/automation-rule-editor.test.tsx | 118 +++++++ .../automation-workspace-status.test.tsx | 44 +++ tests/integration/automation-chain.test.ts | 4 + tests/unit/automation-action-runner.test.ts | 132 +++++++- tests/unit/automation-reply-delay.test.ts | 84 +++++ tests/unit/automation-service.test.ts | 289 +++++++++++++++++- tests/unit/message-listener-service.test.ts | 164 ++++++++++ tests/unit/wechat-action-gateway.test.ts | 133 +++++++- 25 files changed, 1710 insertions(+), 122 deletions(-) create mode 100644 tests/component/automation-rule-editor.test.tsx create mode 100644 tests/component/automation-workspace-status.test.tsx create mode 100644 tests/unit/automation-reply-delay.test.ts diff --git a/resources/wcdb/win32/x64/wcdb_api.dll b/resources/wcdb/win32/x64/wcdb_api.dll index f1f98150e8277838a685cb4e27ce339da78a3db0..127dcc6f5f8559c52c814f244d8201e48bcf0b22 100644 GIT binary patch delta 136978 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

@Z~8v>=Vt~ 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 z zi*<+za5|eS6Bm1tAS^nOw&k$tF#|p8IaU!rjFEm#yq*`MovzcfXV|SyNu(=l8rzW; zCbFrqu5?2pTN>+O8&+bn%gv?9tJ(3`wzOp;Gt+u2Eng^Dpw=U}i$YP^QJc5kZy&}A zt0;{5AKev-n7nALfe-Kr(j0`DEPSqDW3(Qgz|V&dk`mcJvR`E1$Ugo1^*%2B{y|LQ z3HUr)v}c^3ILS`A@MW57^xf<=dX2p_dKAl#8>$TZOTpg7^`!d(S@+JjE$U>$k-2Bh zTE@4o*A&4z{2<}D|La(L0|*y5<15GArVm>e??bQlksbG+5Dfos$9f1<1rzi(O4{uUFU$ zhPW~=#!z0-7vATWo$7%tZ$L&j|8Otn-K9l9V?gtzV#_bw^ArL4(mnU;4fnhoJ=JUX z#V+oIy02u{7QTRJnrLbRR?O+OsL$gREvleYE=JeyiD-9t~-= z-t12I^R$OI+mo0}N7%9Oq-j*LWrvgc)5o67E4dG?W6Ls=r_#;U*+0q2G@&|6>d}ct zC9}0Xy1U$ZqJ*MDH|sw@f}uGyMySRT8)nzjQ}rV%4_L~uQfY(@3-1{~!@KPs)Uykr zJr=XQy^^V65mWU}qdrzFw|4+-(VcDY-Hd8%*@ND}RF}YN_W7GyxiV$n@ifE&TsX6N zeUoVbV^ZHPbVM~4-Y=L=?ZSrj3!#gx*^++ysZBEL*1tRbC5dguqkj@J^zW~{^;XFS zq_`@N;hBl&#<$7^Dc)>rJFNuCZA2^J* zjM+VSkdv9iT0KUmUnBx`m^&#akLkXNp%ZuPzWmKeLbw0K)(nrM{eIg0bhtB7u6U

NgU~E677S9)BJ1Qxjt;e}5?>|toKI43q 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

D=%+5fL5tU8R`zZoty?cTc3N|< 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

u9AMBCvd1|~;*WtI?L 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 C49PeE diff --git a/src/main/index.ts b/src/main/index.ts index b3d2ece..7530508 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -118,7 +118,7 @@ import { automationExecutionLogService } from './services/automation-execution-l import { initAutomationService, getAutomationService } from './services/automation-service' import { GroupStatsService } from './services/group-stats-service' import { wechatActionLogService } from './services/wechat-action-log-service' -import { wechatActionGateway } from './services/wechat-action-gateway' +import { toPersonalWechatSendResult, wechatActionGateway } from './services/wechat-action-gateway' import { personalWechatSendService } from './services/personal-wechat-send-service' import { getPersonalWechatSendCapability } from './services/personal-wechat-capability-service' import { scheduledReportService } from './services/scheduled-report-service' @@ -1056,14 +1056,17 @@ app.whenReady().then(async () => { voiceRecognition?.connect(voiceService, resolvedRoot) stickerService = new StickerService(wcdb4Client) videoAssetService = new VideoAssetService(wcdb4Client) - const monitoring = await wcdb4Client.startMonitor((type, json) => { + const monitoring = await wcdb4Client.startMonitor((event) => { wcdb4Client.invalidateSessionCache() // 正式 MessageListener:只做 coalesce + 有界回读 + dedup + 投递,不触发业务。 - messageListener.handleNativeChange() - groupExitMonitorService.notifyDatabaseChanged(json) - recallArchiveMonitor?.handleDatabaseChange(json) + // v2 事件带 sessionId ⇒ 回读不再依赖「最近活跃会话」。 + messageListener.handleNativeChange(event) + groupExitMonitorService.notifyDatabaseChanged(event.raw) + recallArchiveMonitor?.handleDatabaseChange(event.raw) for (const window of BrowserWindow.getAllWindows()) { - if (!window.isDestroyed()) window.webContents.send('wcdb-change', { type, json }) + if (!window.isDestroyed()) { + window.webContents.send('wcdb-change', { type: event.type, json: event.raw }) + } } }) automationListening = monitoring === true @@ -1208,7 +1211,33 @@ app.whenReady().then(async () => { ) // 日报等系统动作仍复用现有发送服务;普通聊天不再暴露这个入口。 ipcMain.handle('wechat-personal:send', async (_, request: PersonalWechatSendRequest) => { - if (request.type !== 'voice' || String(request.fromId || '').trim()) { + // 项目规则:**所有发送都必须经过 WechatActionGateway**(审计 + 幂等 + 同一个 Send Log)。 + // 手动发送在改造前从这里直连 `PersonalWechatSendService`,于是完全不留痕 —— + // 排查「消息到底发没发出去」时恰好缺的就是那份证据。 + // `triggerType: 'user'` 不会命中 automation 专用的 purpose allowlist 与 3s 节流, + // 所以行为与改造前一致,只是多一条审计记录(「发送日志」里能看到)。 + if (request.type === 'text' || request.type === 'image') { + const to = String(request.to || '').trim() + const action = await wechatActionGateway.execute({ + origin: 'user_manual', + purpose: request.type === 'text' ? 'manual_text' : 'manual_image', + triggerType: 'user', + recipient: { + type: request.isGroup || to.endsWith('@chatroom') ? 'group' : 'contact', + id: to + }, + content: + request.type === 'text' + ? { type: 'text', text: String(request.text || '') } + : { type: 'image', path: String(request.filePath || '') } + }) + // 返回契约保持 `PersonalWechatSendResult`,界面判读不用改。 + return toPersonalWechatSendResult(action, await personalWechatSendService.getStatus()) + } + + // 语音仍走既有分支:它有自己的网关入口 `wechat-personal:sendGeneratedTtsVoice`, + // 且需要先解析当前账号 wxid 才能编码。 + if (String(request.fromId || '').trim()) { return personalWechatSendService.send(request) } let fromId = '' @@ -2368,11 +2397,13 @@ app.whenReady().then(async () => { if (client) { voiceService = new VoiceService(client, client.getAccountRoot()) voiceRecognition?.connect(voiceService, client.getAccountRoot()) - const monitoring = await client.startMonitor((type, json) => { + const monitoring = await client.startMonitor((event) => { client.invalidateSessionCache() - groupExitMonitorService.notifyDatabaseChanged(json) + groupExitMonitorService.notifyDatabaseChanged(event.raw) for (const window of BrowserWindow.getAllWindows()) { - if (!window.isDestroyed()) window.webContents.send('wcdb-change', { type, json }) + if (!window.isDestroyed()) { + window.webContents.send('wcdb-change', { type: event.type, json: event.raw }) + } } }) void groupExitMonitorService.start(monitoring) diff --git a/src/main/services/automation-action-runner.ts b/src/main/services/automation-action-runner.ts index f01c1a4..524a8a4 100644 --- a/src/main/services/automation-action-runner.ts +++ b/src/main/services/automation-action-runner.ts @@ -4,6 +4,7 @@ import { AUTOMATION_SEND_PURPOSE, DEFAULT_REPLY_TEXT, automationIdempotencyKey, + normalizeReplyDelaySeconds, type AutomationAction, type AutomationRule, type AutomationStep, @@ -31,6 +32,22 @@ import { wechatActionGateway } from './wechat-action-gateway' /** 一步执行完毕后,后续步骤的处置方式。 */ const STEP_ORDER: AutomationStepKey[] = ['received', 'matched', 'reply', 'report', 'send'] +/** + * 前置步骤失败时,后续步骤的 `skipReason`。 + * + * 必须写清「是因为前面那步没成」,否则用户看到一连串「已跳过」会以为是规则没配好。 + */ +const SKIPPED_AFTER_REPLY_FAILURE = '前置步骤失败(回复确认未成功),本次不再继续。' +const SKIPPED_AFTER_REPORT_FAILURE = '前置步骤失败(日报未生成),没有图片可发送。' + +/** + * 规则启用了「发送日报图片」,但手上没有图片文件。 + * + * 这不是「正常跳过」,也不允许退而求其次去发空路径 / 上一张旧图 / 不存在的文件 —— + * 发错东西比不发更糟,所以直接判失败。 + */ +const SEND_WITHOUT_IMAGE_ERROR = '日报图片未生成,无法发送。' + export interface AutomationRunInput { executionId: string rule: AutomationRule @@ -52,6 +69,8 @@ export interface AutomationActionRunnerDependencies { generateReport?: (request: AgentGroupReportRequest) => Promise executeAction?: (request: WechatActionRequest) => Promise now?: () => number + /** 延迟实现。默认真 sleep;单测注入即时 resolve 的假实现,避免真的等 2 秒。 */ + delay?: (ms: number) => Promise } /** 策略层的错误码 → 用户可读短句。UI 直接展示这些文案,不做二次翻译。 */ @@ -86,8 +105,9 @@ function markFailed(step: AutomationStep, at: number, error: string): void { step.error = error } -function markSkipped(step: AutomationStep): void { +function markSkipped(step: AutomationStep, skipReason?: string): void { step.status = 'skipped' + if (skipReason) step.skipReason = skipReason } function actionErrorMessage(code: string | undefined, fallback: string | undefined): string { @@ -102,11 +122,15 @@ export class AutomationActionRunner { private readonly generateReport: (request: AgentGroupReportRequest) => Promise private readonly executeAction: (request: WechatActionRequest) => Promise private readonly now: () => number + private readonly delay: (ms: number) => Promise constructor(dependencies: AutomationActionRunnerDependencies = {}) { this.generateReport = dependencies.generateReport ?? generateAgentGroupReport this.executeAction = dependencies.executeAction ?? ((request) => wechatActionGateway.execute(request)) this.now = dependencies.now ?? (() => Date.now()) + this.delay = + dependencies.delay ?? + ((ms) => new Promise((resolve) => setTimeout(resolve, ms))) } async run(input: AutomationRunInput): Promise { @@ -122,6 +146,22 @@ export class AutomationActionRunner { const sendAction = findAction(input.rule, 'sendReportImage') // ---- 步骤 3:回复确认 ---- + // + // 回复等待:规则一命中就秒回,看起来就是个机器人(消息刚到、回复就到)。 + // 等待时长是**规则自己的一项执行参数**(`rule.replyDelaySeconds`,在 + // 「编辑自动化 → 3 · 触发后执行」里配),所以不同规则可以不一样。 + // + // 等待刻意放在 `reply` 步骤计时**之外** —— `reply.durationMs` 只应该反映发送本身, + // 否则用户看到「回复确认 2000ms」会误以为是发送慢。 + // 已经命中就不再回头重判规则:等待窗口里规则被停用/删掉也不中断本次执行, + // 与 cooldown、同消息幂等的口径一致(都是「命中那一刻」的快照)。 + if (replyAction) { + // 用共享的归一化函数,而不是 `Number(...) || 0`:旧版 rules.json 里没有这个字段, + // 那应该按**默认 2 秒**处理(否则「默认 2 秒」要等用户手动进编辑页才会生效)。 + const replyDelayMs = normalizeReplyDelaySeconds(input.rule.replyDelaySeconds) * 1_000 + if (replyDelayMs > 0) await this.delay(replyDelayMs) + } + if (!replyAction) { markSkipped(stepAt('reply')) } else { @@ -134,7 +174,7 @@ export class AutomationActionRunner { }) if (!sent.ok) { markFailed(replyStep, this.now(), sent.error || '回复确认失败') - markRemainingSkipped(steps, 'reply') + markRemainingSkipped(steps, 'reply', SKIPPED_AFTER_REPLY_FAILURE) return { steps, status: 'failed', errorSummary: replyStep.error } } markSuccess(replyStep, this.now()) @@ -159,7 +199,7 @@ export class AutomationActionRunner { } if (!result.success || !result.pngPath) { markFailed(reportStep, this.now(), result.error || '日报生成失败') - markRemainingSkipped(steps, 'report') + markRemainingSkipped(steps, 'report', SKIPPED_AFTER_REPORT_FAILURE) return { steps, status: 'failed', errorSummary: reportStep.error } } pngPath = result.pngPath @@ -167,12 +207,23 @@ export class AutomationActionRunner { } // ---- 步骤 5:发送日报图片 ---- - // 日报被跳过(或没产出图片)时,这一条也必须 skipped —— 不能凭空发图。 - if (!sendAction || !pngPath) { + // + // 三种情况必须分开判断 —— 合并成 `!sendAction || !pngPath` 会把 + // 「规则要求发图、但图根本没生成」当成正常跳过,execution 还记成 success: + // + // A. 规则**本来就没有启用**这个动作 → skipped,这是正常的,不影响整体结果; + // B. 启用了,但要发的东西不存在 → **failed**,不能假装成功, + // 更不能退而求其次去发空路径 / 上一次的旧图 / 不存在的文件; + // C. 前置(生成日报)已经失败 → 上面就 return 了,走不到这里。 + if (!sendAction) { markSkipped(stepAt('send')) return { steps, status: 'success', ...(pngPath ? { pngPath } : {}) } } const sendStep = stepAt('send') + if (!pngPath) { + markFailed(sendStep, this.now(), SEND_WITHOUT_IMAGE_ERROR) + return { steps, status: 'failed', errorSummary: sendStep.error } + } sendStep.status = 'running' sendStep.startedAt = this.now() const sent = await this.sendThroughGateway(input, 'report', { type: 'image', path: pngPath }) @@ -226,11 +277,15 @@ function findAction(rule: AutomationRule, type: AutomationAction['type']): Autom } /** 把 `after` 之后的步骤全部标成 `skipped`(前一步挂了,后面的不许再动)。 */ -function markRemainingSkipped(steps: AutomationStep[], after: AutomationStepKey): void { +function markRemainingSkipped( + steps: AutomationStep[], + after: AutomationStepKey, + skipReason?: string +): void { const from = STEP_ORDER.indexOf(after) + 1 for (const key of STEP_ORDER.slice(from)) { const step = steps.find((item) => item.key === key) - if (step) markSkipped(step) + if (step) markSkipped(step, skipReason) } } diff --git a/src/main/services/automation-execution-log-service.ts b/src/main/services/automation-execution-log-service.ts index 37c904c..2ee8ae0 100644 --- a/src/main/services/automation-execution-log-service.ts +++ b/src/main/services/automation-execution-log-service.ts @@ -22,6 +22,8 @@ export interface AutomationExecutionLogDependencies { userDataPath?: () => string } +const EXECUTION_STATUSES: AutomationExecution['status'][] = ['running', 'success', 'failed'] + function normalizeExecution(value: unknown): AutomationExecution | null { if (!value || typeof value !== 'object') return null const record = value as Partial @@ -33,7 +35,10 @@ function normalizeExecution(value: unknown): AutomationExecution | null { ruleName: String(record.ruleName || ''), triggerTime: Number(record.triggerTime) || 0, sourceDisplayName: String(record.sourceDisplayName || ''), - status: record.status === 'success' || record.status === 'failed' ? record.status : 'running', + // 不认识的 status(含历史遗留值)一律降级成 `running`,绝不凭空造出成功/失败。 + status: EXECUTION_STATUSES.includes(record.status as AutomationExecution['status']) + ? (record.status as AutomationExecution['status']) + : 'running', durationMs: Number(record.durationMs) || 0, steps: Array.isArray(record.steps) ? record.steps : [], ...(record.errorSummary ? { errorSummary: String(record.errorSummary) } : {}) @@ -78,7 +83,12 @@ export class AutomationExecutionLogService { return true } - /** 统计 `sinceMs` 之后(含)的记录数与成功数。用于顶部「今日执行」。 */ + /** + * 统计 `sinceMs` 之后(含)的记录数与成功数。用于顶部「今日执行」。 + * + * 每条记录都对应一次**真正跑过**的执行(gate 拦下的消息不会产生记录), + * 所以这里直接计数即可。 + */ countSince(sinceMs: number): { total: number; success: number } { this.ensureLoaded() const from = Number(sinceMs) || 0 diff --git a/src/main/services/automation-service.ts b/src/main/services/automation-service.ts index 955a0cf..9f45bb5 100644 --- a/src/main/services/automation-service.ts +++ b/src/main/services/automation-service.ts @@ -52,6 +52,28 @@ import { * 少了它就会自己触发自己,形成死循环。 */ +/** + * gate 表的容量上限。超了才清理「已解锁」的条目 —— 不是为了省内存, + * 而是防止长期运行下 ruleId × conversationId 无界增长。 + */ +const GATE_MAX_ENTRIES = 5_000 + +/** + * 一条规则在一个会话里的 gate 状态。 + * + * ``` + * blocked = inFlight || now < triggeredAt + cooldown + * ``` + */ +interface ConversationRuleGate { + /** **第一次**触发的时间(cooldown 起点)。执行完成时**不重置**。 */ + triggeredAt: number + /** 当前是否有这条规则在这个会话里的执行还在跑。 */ + inFlight: boolean + /** 本次执行期间被 gate 挡下的消息数(仅诊断日志用)。 */ + blockedSinceTrigger: number +} + /** 幂等登记表的存活时间与容量上限(与 MessageListener 的 dedup 同思路)。 */ const CLAIM_TTL_MS = 10 * 60 * 1000 const CLAIM_MAX_ENTRIES = 5_000 @@ -66,6 +88,8 @@ export interface AutomationServiceDependencies { /** 由主进程注入:MessageListener 是否在运行。 */ isListening?: () => boolean now?: () => number + /** gate 表容量上限。默认 `GATE_MAX_ENTRIES`;单测用小值来触发清理路径。 */ + gateMaxEntries?: number } interface ClaimEntry { @@ -82,8 +106,18 @@ export class AutomationService { /** `ruleId:sessionId:localId` → 登记时间。 */ private readonly claims = new Map() - /** `${ruleId}:${conversationId}` → 上次真正开始执行的毫秒时间戳。 */ - private readonly cooldowns = new Map() + /** + * Automation rule ↔ conversation gate。 + * + * 粒度是 **`ruleId + conversationId`**(不是 sender,也不是整个会话): + * 群 A 的「@我生成日报」被触发后,群 B 的同一条规则、或同群里的**另一条规则**都不受影响。 + */ + private readonly gates = new Map() + + /** 仅用于诊断统计(`[Automation] blockedMessages=N`),**不进用户执行日志**。 */ + private blockedMessageCount = 0 + + private readonly gateMaxEntries: number private selfUsernames: string[] = [] private selfUsernamesAt = 0 @@ -98,6 +132,7 @@ export class AutomationService { this.getCapability = dependencies.getCapability ?? getPersonalWechatSendCapability this.isListening = dependencies.isListening ?? (() => true) this.now = dependencies.now ?? (() => Date.now()) + this.gateMaxEntries = dependencies.gateMaxEntries ?? GATE_MAX_ENTRIES } /** @@ -125,6 +160,30 @@ export class AutomationService { } for (const rule of rules) { + // ① Gate 检查放在 TriggerMatcher **之前**(纯读,无副作用)。 + // + // Automation rule conversation gate: + // Once a rule is triggered for a conversation, ignore subsequent messages for + // this rule while the current execution is still running OR until the configured + // cooldown has elapsed from the original trigger time. + // + // Messages arriving during this blocked window must not be matched, replied to, + // executed, or written to the user-facing execution log. + // + // The rule becomes eligible again only after BOTH: + // 1. the previous execution has finished; and + // 2. the cooldown since the original trigger has expired. + // + // Important: cooldown starts at the original trigger time, not when execution finishes. + // + // 也就是说:第一个任务执行期间,以及配置的触发间隔内,后续消息全部静默忽略。 + if (this.isGated(rule, message.sessionId)) { + this.blockedMessageCount += 1 + this.noteBlocked(rule, message.sessionId) + continue + } + + // ② 匹配(同步)。放在 gate 之后 —— 阻塞窗口内的消息不该被匹配。 let matched: boolean try { matched = matchAutomationRule(rule, input, selfUsernames).matched @@ -134,13 +193,28 @@ export class AutomationService { } if (!matched) continue - // 三道闸全部是**同步**的,必须在任何 await 之前完成登记, - // 否则同一批并发消息会同时穿过检查。 - if (this.isCoolingDown(rule, message.sessionId)) continue - if (!this.claim(rule, message)) continue + // ③ Gate claim:check → 写入。**中间不能有 await**,否则两条几乎同时到达的消息 + // 会一起穿过检查。上面 ① 到这里的唯一代码是同步的 matcher,所以这里仍是原子的。 + if (!this.claimGate(rule, message.sessionId)) { + this.blockedMessageCount += 1 + this.noteBlocked(rule, message.sessionId) + continue + } + + // ④ 同一条消息只处理一次(native 事件重复 / 多窗口回读都可能重复投递)。 + if (!this.claim(rule, message)) { + this.leaveGate(rule, message.sessionId) + continue + } const sourceDisplayName = this.resolveDisplayName(message) - await this.execute(rule, message, sourceDisplayName) + try { + await this.execute(rule, message, sourceDisplayName) + } finally { + // 无论 success / failed / 抛异常都要解除 in-flight。 + // 漏掉这一步会让这条规则在这个会话里**永久锁死**。 + this.leaveGate(rule, message.sessionId) + } } } @@ -223,7 +297,8 @@ export class AutomationService { return } - this.cooldowns.set(`${rule.id}:${message.sessionId}`, this.now()) + // cooldown 起点已经在 `claimGate()` 记过了(= 第一次触发时间)。 + // 这里**不再**写任何时间戳 —— 否则「执行真正开始」的时间会把 cooldown 起点往后推。 let result: Awaited> try { @@ -322,15 +397,108 @@ export class AutomationService { } } - private isCoolingDown(rule: AutomationRule, conversationId: string): boolean { + private gateKey(rule: AutomationRule, conversationId: string): string { + // 粒度:规则 × 会话。**不含 senderId** —— 产品语义是「这个群里这条规则刚被触发过」, + // 不是「张三 60 秒内不能再触发」。 + return `${rule.id}:${conversationId}` + } + + private cooldownMs(rule: AutomationRule): number { const seconds = Number(rule.cooldownSeconds) - if (!Number.isFinite(seconds) || seconds <= 0) return false - const last = this.cooldowns.get(`${rule.id}:${conversationId}`) - if (last === undefined) return false - return this.now() - last < seconds * 1000 + if (!Number.isFinite(seconds) || seconds <= 0) return 0 + return seconds * 1000 + } + + /** + * 是否处于阻塞窗口。**纯读**,所以可以放在 TriggerMatcher 之前。 + * + * ``` + * blocked = inFlight || now < triggeredAt + cooldown + * ``` + * + * `triggeredAt` 是**第一次触发**的时间,不是执行完成时间: + * 任务跑得比 cooldown 久时,真正的解锁时间是 + * `max(执行完成, triggeredAt + cooldown)`,而不是「完成之后再等一个 cooldown」。 + */ + private isGated(rule: AutomationRule, conversationId: string): boolean { + const gate = this.gates.get(this.gateKey(rule, conversationId)) + if (!gate) return false + if (gate.inFlight) return true + const cooldownMs = this.cooldownMs(rule) + if (cooldownMs <= 0) return false + // 用「当前时间 vs triggeredAt」现算,**不依赖 setTimeout** —— + // 事件循环卡顿 / app suspend / 定时器漂移都不会把冷却算错。 + return this.now() < gate.triggeredAt + cooldownMs + } + + /** + * 原子地占用 gate(check + 写入)。 + * + * 返回 `false` 表示这一刻已经被阻塞(调用方按「忽略」处理,不匹配、不执行、不写日志)。 + * 调用方必须保证**从 `isGated()` 到这里之间没有 await**。 + */ + private claimGate(rule: AutomationRule, conversationId: string): boolean { + if (this.isGated(rule, conversationId)) return false + this.gates.set(this.gateKey(rule, conversationId), { + triggeredAt: this.now(), + inFlight: true, + blockedSinceTrigger: 0 + }) + this.evictGatesIfNeeded() + return true + } + + /** + * 解除 in-flight。**保留 `triggeredAt`** —— cooldown 仍以第一次触发时间为起点。 + * + * 无论 success / failed / 抛异常都必须走这里,否则这条规则在这个会话里会永久锁死。 + */ + private leaveGate(rule: AutomationRule, conversationId: string): void { + const gate = this.gates.get(this.gateKey(rule, conversationId)) + if (!gate) return + gate.inFlight = false + // 本次执行期间被挡下的消息数,汇总成**一行**诊断日志。 + // 只允许出现数量与 ruleId(不含群名 / wxid / 昵称 / 正文)。 + if (gate.blockedSinceTrigger > 0) { + this.info(`blockedMessages=${gate.blockedSinceTrigger} ruleId=${rule.id}`) + gate.blockedSinceTrigger = 0 + } + } + + /** 记一次「被 gate 拦下的消息」。**只计数**,绝不写入用户执行日志。 */ + private noteBlocked(rule: AutomationRule, conversationId: string): void { + const gate = this.gates.get(this.gateKey(rule, conversationId)) + if (gate) gate.blockedSinceTrigger += 1 + } + + /** 仅供诊断:累计被 gate 拦下的消息数。不接 IPC、不进 UI。 */ + getBlockedMessageCount(): number { + return this.blockedMessageCount + } + + /** + * gate 表有界:**只清理已经解锁的条目**。 + * + * 硬不变量:**正在执行(`inFlight === true`)的 gate 永不被淘汰** —— + * 淘汰它就等于把一条正在跑的规则提前放开,会立刻产生第二次并发执行。 + * 所以这里的条件是 `!inFlight && 已过 cooldown`,两个都要满足; + * 清理不掉就一直留着(宁可让表大一点,也不能让正在跑的规则失去保护)。 + */ + private evictGatesIfNeeded(): void { + if (this.gates.size <= this.gateMaxEntries) return + const now = this.now() + const rules = this.ruleStore.listRules() + for (const [key, gate] of this.gates) { + if (this.gates.size <= this.gateMaxEntries) break + if (gate.inFlight) continue + const separator = key.lastIndexOf(':') + const ruleId = separator < 0 ? key : key.slice(0, separator) + const rule = rules.find((item) => item.id === ruleId) + const cooldownMs = rule ? this.cooldownMs(rule) : 0 + if (now >= gate.triggeredAt + cooldownMs) this.gates.delete(key) + } } - /** 登记「这条消息已经被这条规则处理过」。返回 false 表示重复,应当跳过。 */ private claim(rule: AutomationRule, message: NormalizedIncomingMessage): boolean { const key = `${rule.id}:${message.sessionId}:${message.localId}` const now = this.now() diff --git a/src/main/services/message-listener-service.ts b/src/main/services/message-listener-service.ts index 7eeaa37..007b196 100644 --- a/src/main/services/message-listener-service.ts +++ b/src/main/services/message-listener-service.ts @@ -1,5 +1,5 @@ import { decompress as zstdDecompress } from 'fzstd' -import type { Wcdb4Client, Wcdb4Message } from '../wcdb4-client' +import type { Wcdb4Client, Wcdb4Message, Wcdb4MonitorEvent } from '../wcdb4-client' /** * MessageListener —— 实时消息回读底座。 @@ -21,9 +21,8 @@ import type { Wcdb4Client, Wcdb4Message } from '../wcdb4-client' * **不负责**(这些属于下一层,本模块不许碰):关键词匹配、@我业务判断、日报、 * 自动回复、AI 调用、Agent 调度、发送消息。 * - * 设计约束全部来自 2026-09-20 的真实环境 Spike(见 - * `.ai-local/reports/2026-09-20-realtime-message-spike-result.md`): - * 一条真实消息平均触发 **≈17 个** native event,所以**绝不能**一个事件触发一次业务动作。 + * 设计约束:一次写入会触发**一连串** native event(十几条), + * 所以**绝不能**一个事件触发一次业务动作。 */ /** zstd 帧魔数(微信 `source` 列是 zstd 压缩)。 */ @@ -185,6 +184,13 @@ export class MessageListenerService { * 必须有界 —— Spike 里用的是无上限 `Set`,长时间运行会持续吃内存。 */ private readonly seen = new Map() + /** + * 本批 coalesce 窗口内**出现过精确会话**的事件收集到的 session 集合。 + * + * v2 事件(Native Monitor Event v2)会带上发生变化的 sessionId,这里用 **Set** 收集 —— + * 120ms 内收到 `A B A C B` 必须回读 A/B/C 三个,绝不能「最后一个 wins」。 + */ + private readonly pendingSessions = new Set() private coalesceTimer: ReturnType | null = null private readbackInFlight = false private disposed = false @@ -243,11 +249,17 @@ export class MessageListenerService { /** * 接 native change event。 * - * 只做计数与 coalesce —— **绝不**在这里回读,更不触发任何业务。 + * 只做计数、收集会话与 coalesce —— **绝不**在这里回读,更不触发任何业务。 + * + * `event.protocol === 2` 时把 `sessionId` 收进本批的集合;legacy 事件(v1,不带会话) + * 走到回读时仍然只能退化成「最近活跃会话」。 */ - handleNativeChange(): void { + handleNativeChange(event?: Wcdb4MonitorEvent): void { if (this.disposed) return this.counters.nativeEvents += 1 + if (event?.protocol === 2 && event.sessionId) { + this.pendingSessions.add(event.sessionId) + } if (this.coalesceTimer) { // 已经在同一个窗口里:直接合并掉,这就是 17:1 那个比值的收敛点。 this.counters.coalescedEvents += 1 @@ -262,47 +274,44 @@ export class MessageListenerService { /** * 有界回读 → normalize → dedup → 投递。 * - * 回读范围严格受限:**单会话** + **小时间窗口** + **条数上限**。 - * 不遍历会话、不扫全库。 + * 回读范围严格受限:**按会话** + **小时间窗口** + **条数上限**。不遍历会话、不扫全库。 + * + * 目标会话的来源有两种: + * - **precise(protocol v2)**:本批事件收集到的 `sessionId` 集合,逐个回读 + * (多个会话同时来消息时,A/B/C 都会被读到); + * - **legacy(protocol v1,旧 runtime)**:只能退回「最近活跃会话」`getSessions()[0]`。 */ private async readback(): Promise { if (this.disposed || this.readbackInFlight) return this.readbackInFlight = true const startedAt = Date.now() + + const preciseSessions = Array.from(this.pendingSessions) + this.pendingSessions.clear() + try { - const nowSec = Math.floor(startedAt / 1000) - const sessions = this.client.getSessions() - const session = sessions[0] - if (!session?.username) return - - this.counters.readbacks += 1 - const messages = await this.client.getMessagesAsync( - session.username, - nowSec - this.lookbackSec, - nowSec + this.lookaheadSec, - { limit: this.readLimit } - ) - if (this.disposed) return - let delivered = 0 - for (const message of messages) { - const normalized = this.normalize(session.username, message) - if (!normalized) continue - // dedup:同一条消息可能在多个读回窗口中反复出现。 - if (!this.markSeen(normalized)) { - this.counters.deduped += 1 - continue + let sessions = 0 + + if (preciseSessions.length > 0) { + for (const sessionId of preciseSessions) { + if (this.disposed) return + delivered += await this.readbackSession(sessionId) + sessions += 1 } - this.counters.delivered += 1 - delivered += 1 - this.deliver(normalized) + } else { + // legacy:旧 runtime 的 payload 不带会话,只能回读「最近活跃会话」。 + const session = this.client.getSessions()[0] + if (!session?.username) return + delivered = await this.readbackSession(session.username) + sessions = 1 } if (delivered > 0) { - // 日志只记录数量与耗时,**不含** wxid / 群名 / 昵称 / 正文 / source。 + // 日志只记录协议版本、数量与耗时 —— **不含** wxid / 群名 / 昵称 / 正文 / source / sessionId。 console.log( - `[MessageListener] delivered=${delivered} readbackMs=${Date.now() - startedAt}` + - ` sessionType=${session.username.endsWith('@chatroom') ? 'group' : 'direct'}` + + `[MessageListener] protocol=${preciseSessions.length > 0 ? 'v2' : 'v1'}` + + ` sessions=${sessions} delivered=${delivered} readbackMs=${Date.now() - startedAt}` + ` events=${this.counters.nativeEvents} coalesced=${this.counters.coalescedEvents}` ) } @@ -315,6 +324,34 @@ export class MessageListenerService { } } + /** 回读**单个**会话并投递。返回本次真正投递出去的条数。 */ + private async readbackSession(sessionId: string): Promise { + const nowSec = Math.floor(Date.now() / 1000) + this.counters.readbacks += 1 + const messages = await this.client.getMessagesAsync( + sessionId, + nowSec - this.lookbackSec, + nowSec + this.lookaheadSec, + { limit: this.readLimit } + ) + if (this.disposed) return 0 + + let delivered = 0 + for (const message of messages) { + const normalized = this.normalize(sessionId, message) + if (!normalized) continue + // dedup:同一条消息可能在多个读回窗口中反复出现(native 事件本身也可能重复)。 + if (!this.markSeen(normalized)) { + this.counters.deduped += 1 + continue + } + this.counters.delivered += 1 + delivered += 1 + this.deliver(normalized) + } + return delivered + } + private deliver(message: NormalizedIncomingMessage): void { for (const listener of this.listeners) { try { diff --git a/src/main/services/personal-wechat-send-service.ts b/src/main/services/personal-wechat-send-service.ts index 3f03874..3ad4ecf 100644 --- a/src/main/services/personal-wechat-send-service.ts +++ b/src/main/services/personal-wechat-send-service.ts @@ -931,6 +931,44 @@ function windowsStatusBase(host = windowsHookHost() || ''): PersonalWechatSender } } +/** 文件字节数;读不到时返回 `-1`(**不是** 0 —— 0 会被误读成「空文件」)。 */ +function safeFileSize(target: string): number { + try { + return statSync(target).size + } catch { + return -1 + } +} + +/** + * 发送链路诊断日志(排查「hook 报成功、群里却没有消息」时用)。 + * + * 只有在设置里打开「调试模式」时才输出,所以正常使用不会刷屏。 + * + * **隐私约束**:只允许输出类型、字节数、HTTP 状态码、hook 的 `ret` 这类结构性字段。 + * 文件名、完整路径、wxid、群名、消息正文不进日志。 + */ +function logSendDiagnostic(stage: string, detail: Record): void { + try { + if (!loadSettings().debugEnabled) return + } catch { + return + } + console.log(`[SendDiag] ${stage} ${JSON.stringify(detail)}`) +} + +/** + * 发送完成后**保留**图片临时文件的时长(毫秒)。默认 `0` = 立刻删除(现行行为)。 + * + * 为什么留这个开关:`finally` 里的删除发生在 **hook 返回之后**,这隐含假设 + * 「hook 返回时发送已经完成」。如果 hook 其实是「先回 200、后台再读文件」, + * 文件就可能在它读到之前已经被删掉 —— 现象正好是「hook 报成功、微信里没有消息」。 + * + * 置为非 0 时可分辨两类失败:保留期间图片到了 ⇒ 删除时机问题(修法与 hook 约定文件生命周期); + * 仍然没到 ⇒ 与文件生命周期无关。 + */ +const WINDOWS_TEMP_IMAGE_GRACE_MS = 0 + async function requestWindowsHook( endpoint: string, body: Record, @@ -939,15 +977,41 @@ async function requestWindowsHook( host = windowsHookHost() ): Promise { if (!host) throw new Error('尚未配置微信发送能力端口') + const startedAt = Date.now() const init: RequestInit = { method, headers: { 'Content-Type': 'application/json' } } if (method !== 'GET') init.body = JSON.stringify(body) + logSendDiagnostic('hook-request', { + endpoint, + method, + // 只报 payload 里有没有内容,不报内容本身。 + hasPayload: method !== 'GET' ? init.body !== undefined : false + }) const response = await requestWithTimeout(`http://${host}${endpoint}`, init, timeoutMs) const responseText = await response.text() - if (!response.ok) throw new WindowsHookHttpError(response.status, responseText) - return parseWindowsHookResponse(responseText, method === 'POST') + if (!response.ok) { + // HTTP 层失败:状态码不是隐私,正文可能是(hook 有时会把请求原样回显)。 + logSendDiagnostic('hook-response', { + endpoint, + httpStatus: response.status, + ok: false, + bodyLength: responseText.length, + elapsedMs: Date.now() - startedAt + }) + throw new WindowsHookHttpError(response.status, responseText) + } + const parsed = parseWindowsHookResponse(responseText, method === 'POST') + logSendDiagnostic('hook-response', { + endpoint, + httpStatus: response.status, + ok: true, + ret: typeof parsed.ret === 'number' ? parsed.ret : null, + retMessageLength: String(parsed.retmsg ?? parsed.msg ?? '').length, + elapsedMs: Date.now() - startedAt + }) + return parsed } export class PersonalWechatSendService { @@ -1465,6 +1529,14 @@ export class PersonalWechatSendService { } else if (request.type === 'image') { const preparedImage = prepareWindowsImageFile(request.filePath) temporaryImagePath = preparedImage.temporary ? preparedImage.filePath : undefined + // 关键证据:源文件有多大、是否走了临时副本、副本拷出来多大。 + // `copiedBytes < sourceBytes` ⇒ 截断的拷贝:拷贝中途失败不会抛错, + // 只看「有没有文件」会漏掉这种情况。 + logSendDiagnostic('image-prepared', { + sourceBytes: safeFileSize(request.filePath), + usedTempCopy: preparedImage.temporary, + copiedBytes: safeFileSize(preparedImage.filePath) + }) const windowsRequest = buildWindowsWechatRequest({ ...request, filePath: preparedImage.filePath @@ -1503,10 +1575,22 @@ export class PersonalWechatSendService { } } finally { if (temporaryImagePath) { - try { - unlinkSync(temporaryImagePath) - } catch { - // Temporary files are best-effort cleanup only. + if (WINDOWS_TEMP_IMAGE_GRACE_MS > 0) { + // 诊断模式:先不删,等一段时间再删(见常量注释)。 + const pending = temporaryImagePath + setTimeout(() => { + try { + unlinkSync(pending) + } catch { + // best effort + } + }, WINDOWS_TEMP_IMAGE_GRACE_MS).unref?.() + } else { + try { + unlinkSync(temporaryImagePath) + } catch { + // Temporary files are best-effort cleanup only. + } } } if (temporaryVoicePath) { diff --git a/src/main/services/wechat-action-gateway.ts b/src/main/services/wechat-action-gateway.ts index bff8161..adef5e5 100644 --- a/src/main/services/wechat-action-gateway.ts +++ b/src/main/services/wechat-action-gateway.ts @@ -5,7 +5,8 @@ import path from 'path' import type { PersonalWechatSendCapability, PersonalWechatSendRequest, - PersonalWechatSendResult + PersonalWechatSendResult, + PersonalWechatSenderStatus } from '../../shared/personal-wechat' import type { PolicyDecision, @@ -608,6 +609,32 @@ function toPersonalWechatSendRequest(request: WechatActionRequest): PersonalWech } } +/** + * 把网关结果还原成既有调用方期望的 `PersonalWechatSendResult`。 + * + * 为什么需要:手动发送的 IPC(`wechat-personal:send`)返回契约是 + * `PersonalWechatSendResult`,界面上靠 `response.success` / `response.error` 判读。 + * 改成走网关之后必须把这个契约**原样**还回去,否则「统一发送路径」会把 UI 一起改坏。 + * + * 网关成功时会把底层 `sendResult` 原样挂在 `action.sendResult` 上,优先用它 + * (它带着真实的 `status`);拿不到时按 `action.status` 合成一个。 + */ +export function toPersonalWechatSendResult( + action: WechatActionResult, + fallbackStatus: PersonalWechatSenderStatus +): PersonalWechatSendResult { + const raw = action.sendResult + if (raw && typeof raw === 'object' && 'success' in (raw as Record)) { + return raw as PersonalWechatSendResult + } + if (action.status === 'sent') return { success: true, status: fallbackStatus } + return { + success: false, + status: fallbackStatus, + error: action.reason || action.errorCode || '微信发送失败' + } +} + function contentAudit( content: WechatActionContent ): Pick { diff --git a/src/main/wcdb4-client.ts b/src/main/wcdb4-client.ts index d111151..4a1c848 100644 --- a/src/main/wcdb4-client.ts +++ b/src/main/wcdb4-client.ts @@ -384,6 +384,79 @@ export function resolveWindowsNativeAccountRoot( } } +/** + * native monitor 事件(pipe / socket 上的一行 JSON)。 + * + * ## 两个协议版本 + * + * **v1(历史)** —— 变更信号来自**文件系统通知**(Windows `ReadDirectoryChangesW` / + * macOS `kqueue`),native 只知道「哪个文件被写了」: + * + * ```json + * {"db":"message_0.db","table":"message","action":"update"} + * ``` + * + * 它**不携带会话**,所以上层只能回读「最近活跃会话」。`protocol` 会标成 1。 + * + * **v2(Native Monitor Event v2)** —— native 侧在收到文件变化后做一次 + * SessionTable watermark diff,把**每一个真正变化的会话**各发一条: + * + * ```json + * {"version":2,"kind":"message_change","session_id":"xxx@chatroom", + * "db":"message_0.db","table":"message","action":"update","observed_at_ms":1} + * ``` + * + * `protocol = 2` 且带 `sessionId` 时,上层可以**直接按会话回读**,不再依赖 + * `getSessions()[0]`。 + * + * **降级规则**:只要 `version`/`kind`/`session_id` 任一不符合 v2 契约, + * 就按 v1 处理(`protocol = 1`)—— 宁可不精确,也不能拿一个不可信的会话 id 去读。 + */ +export interface Wcdb4MonitorEvent { + /** pipe 上的原始一行(原样转发给需要它的消费者,例如退群监控)。 */ + raw: string + /** 语义化的动作类型。当事件语义出现时用它,比如 `'user_change'`。 */ + type: string + action: string + /** `2` = 精确会话事件;`1` = legacy(不含会话)。 */ + protocol: 1 | 2 + /** 粗表分类:`database` / `message` / `Session` / `contact`。 */ + table: string + /** `protocol === 2` 时才有:发生变化的会话(`xxx@chatroom` 或 wxid)。 */ + sessionId?: string + /** native 侧观测时刻(epoch ms),仅用于诊断。 */ + observedAtMs?: number +} + +/** 把 pipe 上的一行 payload 解析成事件。纯函数,便于单测。 */ +export function parseMonitorEvent(rawPayload: string): Wcdb4MonitorEvent { + const raw = rawPayload.trim() + let parsed: Record = {} + try { + const value = JSON.parse(raw) as unknown + if (value && typeof value === 'object' && !Array.isArray(value)) { + parsed = value as Record + } + } catch { + // 不是 JSON:当作 legacy 事件处理,保留原始 payload。 + } + + const action = typeof parsed.action === 'string' && parsed.action ? parsed.action : 'update' + const sessionId = typeof parsed.session_id === 'string' ? parsed.session_id.trim() : '' + // 三个条件缺一不可:版本、语义、以及**非空**的会话 id。 + const precise = parsed.version === 2 && parsed.kind === 'message_change' && Boolean(sessionId) + + return { + raw, + type: action, + action, + protocol: precise ? 2 : 1, + table: typeof parsed.table === 'string' ? parsed.table : '', + ...(precise ? { sessionId } : {}), + ...(typeof parsed.observed_at_ms === 'number' ? { observedAtMs: parsed.observed_at_ms } : {}) + } +} + export class Wcdb4Client { static readonly defaultRoot = Wcdb4Client.findExistingDefaultRoot() @@ -495,7 +568,7 @@ export class Wcdb4Client { private wcdbStopMonitorPipe: (() => void) | null = null private wcdbGetMonitorPipeName: ((outName: WcdbVoidOut) => number) | null = null private monitorPipeClient: Socket | null = null - private monitorCallback: ((type: string, json: string) => void) | null = null + private monitorCallback: ((event: Wcdb4MonitorEvent) => void) | null = null private monitorConnectTimer: ReturnType | null = null private monitorReconnectTimer: ReturnType | null = null private monitorPipePath = '' @@ -773,7 +846,7 @@ export class Wcdb4Client { }) } - async startMonitor(callback: (type: string, json: string) => void): Promise { + async startMonitor(callback: (event: Wcdb4MonitorEvent) => void): Promise { if (this.closing || !this.wcdbStartMonitorPipe || !this.wcdbGetMonitorPipeName || !this.koffi) { return false } @@ -911,13 +984,7 @@ export class Wcdb4Client { private emitMonitorPayload(rawPayload: string): void { const payload = rawPayload.trim() if (!payload || !this.monitorCallback) return - - try { - const parsed = JSON.parse(payload) as { action?: string } - this.monitorCallback(parsed.action || 'update', payload) - } catch { - this.monitorCallback('update', payload) - } + this.monitorCallback(parseMonitorEvent(payload)) } private scheduleMonitorReconnect(): void { diff --git a/src/renderer/src/features/automation/AutomationWorkspace.tsx b/src/renderer/src/features/automation/AutomationWorkspace.tsx index 4081fc0..d94b2f3 100644 --- a/src/renderer/src/features/automation/AutomationWorkspace.tsx +++ b/src/renderer/src/features/automation/AutomationWorkspace.tsx @@ -9,9 +9,6 @@ import { Button, SegmentedControl, SegmentedControlItem, - Tooltip, - TooltipContent, - TooltipTrigger, useToast } from '../../components/ui' import { RuleListPanel } from './RuleListPanel' @@ -188,21 +185,8 @@ export function AutomationWorkspace({

diff --git a/src/renderer/src/features/automation/ExecutionDetailDrawer.tsx b/src/renderer/src/features/automation/ExecutionDetailDrawer.tsx index 2585ef7..139ed97 100644 --- a/src/renderer/src/features/automation/ExecutionDetailDrawer.tsx +++ b/src/renderer/src/features/automation/ExecutionDetailDrawer.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { AUTOMATION_EXECUTION_STATUS_LABELS } from '../../../../shared/automation' import type { AutomationExecution } from '../../../../shared/automation' import { Button } from '../../components/ui' import { @@ -75,7 +76,7 @@ export function ExecutionDetailDrawer({
结果
- {execution.status === 'success' ? '成功' : '失败'} + {AUTOMATION_EXECUTION_STATUS_LABELS[execution.status]}
diff --git a/src/renderer/src/features/automation/ExecutionLogPanel.tsx b/src/renderer/src/features/automation/ExecutionLogPanel.tsx index 2cd5bf5..a5267d4 100644 --- a/src/renderer/src/features/automation/ExecutionLogPanel.tsx +++ b/src/renderer/src/features/automation/ExecutionLogPanel.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { AUTOMATION_EXECUTION_STATUS_LABELS } from '../../../../shared/automation' import type { AutomationExecution } from '../../../../shared/automation' import { Button, Spinner } from '../../components/ui' import { STEP_STATUS_LABELS, formatDuration, formatTriggerTime, stepStatusTone } from './model/format' @@ -100,7 +101,7 @@ export function ExecutionLogPanel({ - {execution.status === 'success' ? '成功' : '失败'} + {AUTOMATION_EXECUTION_STATUS_LABELS[execution.status]} diff --git a/src/renderer/src/features/automation/RuleEditorPanel.tsx b/src/renderer/src/features/automation/RuleEditorPanel.tsx index 5e66126..ff0722b 100644 --- a/src/renderer/src/features/automation/RuleEditorPanel.tsx +++ b/src/renderer/src/features/automation/RuleEditorPanel.tsx @@ -1,8 +1,10 @@ import * as React from 'react' import { + AUTOMATION_REPLY_DELAY_MAX_SECONDS, DEFAULT_REPLY_TEXT, KEYWORD_MATCH_MODE_LABELS, createDefaultDailyReportRule, + normalizeReplyDelaySeconds, normalizeRuleDraft, type AutomationActionType, type AutomationRule, @@ -54,7 +56,10 @@ function draftFromRule(rule: AutomationRule | null): AutomationRuleDraft { scope: base.scope, conditions: base.conditions, actions: base.actions, - cooldownSeconds: base.cooldownSeconds + cooldownSeconds: base.cooldownSeconds, + // 漏掉一个字段就会被 normalizeRuleDraft 的默认值悄悄覆盖 —— + // 比如把用户设成 0 的「回复前等待」重置回 2 秒。 + replyDelaySeconds: base.replyDelaySeconds }) } @@ -122,6 +127,15 @@ export function RuleEditorPanel({ ) }, [groups, groupFilter]) + /** + * 生效范围的实际状态。 + * + * 现在的交互是「勾选群 = 指定群聊;一个都不勾 = 所有群聊」,所以没有单独的 + * 「所有群聊 / 指定群聊」单选控件 —— 不去动第 2 节的结构,只把这个状态显式说出来, + * 好让「所有群聊」那条例外提示挂在对的上下文里。 + */ + const hasSelectedGroups = draft.conditions.conversationIds.length > 0 + const handleSave = (): void => { if (!draft.name.trim()) { setValidationError('请填写自动化名称') @@ -228,9 +242,9 @@ export function RuleEditorPanel({

2 · 在哪些聊天生效

- {draft.conditions.conversationIds.length + {hasSelectedGroups ? `已选 ${draft.conditions.conversationIds.length} 个群` - : '未选择时对所有群聊生效'} + : '所有群聊'}
+ {hasSelectedGroups ? null : ( + // 只在「所有群聊」这个上下文里说明真实边界。 + // 不解释底层原因(表事件 / 会话回读),也不写「100% 覆盖」这种绝对承诺。 +

+ + 当前版本在多个会话同时收到消息时,极少数自动化触发可能遗漏。 +

+ )}
@@ -268,6 +290,30 @@ export function RuleEditorPanel({

3 · 触发后执行

按下列顺序执行 + {/* 第一步不是动作,而是「等多久」—— 所以放在动作列表之前,不混进 ACTION_META。 */} +
+
+ 回复前等待(秒) + 命中后先等这么久再回复,避免秒回显得像机器人;0 = 立刻回复 +
+ { + const next = Number(event.target.value) + setDraft((current) => ({ + ...current, + replyDelaySeconds: normalizeReplyDelaySeconds( + Number.isFinite(next) ? next : 0 + ) + })) + }} + className="automation-number-input" + aria-label="回复前等待秒数" + /> +
{ACTION_META.map((meta) => { const action = actionOf(meta.type) return ( @@ -304,7 +350,10 @@ export function RuleEditorPanel({
触发间隔(秒) - 同一个群里,两次触发之间至少间隔这么久,避免刷屏 + {/* 这句就是本产品的门语义定义,不要写 debounce / mutex / in-flight 这类技术词。 */} + + 触发后,在当前任务执行期间及设定间隔内,不再处理本规则在该会话中的新消息。 +
item.key === step.key) const blockedByFailure = steps .slice(0, index < 0 ? 0 : index) @@ -60,6 +64,6 @@ export function describeSkippedStep(step: AutomationStep, steps: AutomationStep[ if (blockedByFailure) return '上一步失败,已跳过' if (step.key === 'reply') return '规则未启用「回复确认」' if (step.key === 'report') return '规则未启用「生成日报」' - if (step.key === 'send') return '没有可发送的日报图片' + if (step.key === 'send') return '规则未启用「发送日报图片」' return '未执行' } diff --git a/src/renderer/src/styles/automation.scss b/src/renderer/src/styles/automation.scss index 6cfc450..b8f4336 100644 --- a/src/renderer/src/styles/automation.scss +++ b/src/renderer/src/styles/automation.scss @@ -147,6 +147,21 @@ font-size: 12px; } +// 轻量边界说明(例如「所有群聊」时的遗漏提示)。 +// 只陈述用户能理解的结果,不解释底层机制,也不做「100% 覆盖」这类承诺。 +.automation-section-hint { + display: flex; + gap: 6px; + margin: 10px 0 0; + padding: 8px 10px; + border-radius: var(--wxex-radius-sm); + border: 1px solid var(--wxex-border); + background: color-mix(in srgb, var(--wxex-text-muted) 6%, transparent); + color: var(--wxex-text-secondary); + font-size: 12px; + line-height: 18px; +} + .automation-loading { display: flex; align-items: center; @@ -855,11 +870,16 @@ color: var(--wxex-success); } - &.failed, - &.running { + &.failed { background: color-mix(in srgb, var(--wxex-danger) 16%, transparent); color: var(--wxex-danger); } + + // 「执行中」用强调色,不是红色。 + &.running { + background: var(--wxex-brand-soft); + color: var(--wxex-brand); + } } .automation-trail { diff --git a/src/shared/automation.ts b/src/shared/automation.ts index dbcf5cc..3dafce8 100644 --- a/src/shared/automation.ts +++ b/src/shared/automation.ts @@ -50,6 +50,17 @@ export interface AutomationRule { actions: AutomationAction[] /** 同一规则在同一会话内的最小触发间隔(秒)。防刷屏。 */ cooldownSeconds: number + /** + * 命中后**等多久再发「回复确认」**(秒)。0 = 立刻回复。 + * + * 与 `cooldownSeconds` 同口径(秒):界面上是秒、落盘也是秒, + * 只有 `AutomationActionRunner` 在等待前换算成毫秒。别在这里存毫秒, + * 否则「触发间隔」和「回复等待」两个字段一个秒一个毫秒,迟早有人读错。 + * + * **旧版 rules.json 里没有这个字段**:读取方一律按 + * `normalizeReplyDelaySeconds()` 处理 ⇒ 缺省即默认 2 秒,不是「不等待」。 + */ + replyDelaySeconds: number createdAt: number updatedAt: number } @@ -57,6 +68,7 @@ export interface AutomationRule { /** 执行步骤状态。`skipped` 用于「上一步失败所以这一步不做」。 */ export type AutomationStepStatus = 'pending' | 'running' | 'success' | 'failed' | 'skipped' +/** 执行步骤键。只有**真正进入执行**的规则才会有步骤。 */ export type AutomationStepKey = 'received' | 'matched' | 'reply' | 'report' | 'send' export interface AutomationStep { @@ -69,8 +81,37 @@ export interface AutomationStep { durationMs?: number /** 用户可读的错误信息。**禁止**含 wxid / 文件路径 / 原始 payload。 */ error?: string + /** + * 被跳过时的原因(给用户看的一句话)。 + * + * 与 `error` 分开:`skipped` 不是错误,硬塞进 `error` 会让界面把它当失败渲染。 + * **禁止**含 localId / serverId / dedup key 这类工程术语。 + */ + skipReason?: string } +/** + * 一次 execution 的最终状态。 + * + * **规则(不要改口径)** + * + * | 情况 | status | + * | --- | --- | + * | 所有**启用的必需 action** 成功 | `success` | + * | 某个必需 action `failed` | `failed` | + * | 后续 action 因前置失败未执行(step 记 `skipped`) | `failed`(整个规则没跑完整) | + * | 规则本来就没启用某个 action | 该 step `skipped`,**不影响**整体结果 | + * | 消息**没有命中**规则 | **不创建 execution** | + * | 落在 rule conversation gate 的阻塞窗口内 | **不创建 execution** | + * + * ⚠️ **阻塞窗口内的消息不留任何痕迹**:不匹配、不执行、不回复、**不写 execution**。 + * 从用户产品视角,那段时间这条规则就是「没处理这些消息」—— 不是「跳过」。 + * 门语义见 `automation-service.ts` 的 `ConversationRuleGate`。 + * + * ⇒ 所以 execution 级**没有** `skipped`:一次 execution 只要被创建出来,就说明它真的跑过, + * 结果非成功即失败。**步骤级**的 `skipped`(`AutomationStepStatus`)是另一回事,仍然保留 —— + * 那是「这次执行里某一步没做(没配 / 前置失败)」。 + */ export type AutomationExecutionStatus = 'running' | 'success' | 'failed' export interface AutomationExecution { @@ -108,6 +149,39 @@ export const AUTOMATION_SEND_PURPOSE = { /** 审计日志里的来源标识(`WechatActionOrigin`)。 */ export const AUTOMATION_SEND_ORIGIN = 'automation' +/** + * 「回复确认」前的默认等待(秒)。 + * + * 规则一命中就立刻回复,看起来就是个机器人 —— 消息刚到、回复就到了。 + * 这个等待把回复推到一个更像人的时间点上,所以它是**规则自己的一项执行参数** + * (和 `cooldownSeconds` 同口径:秒),在「编辑自动化 → 3 · 触发后执行」里配。 + * + * **只作用于「回复确认」这一步**:日报生成 + 图片发送本来就以秒计, + * 不再叠加等待,否则整条链路会慢得让人以为卡住了。 + */ +export const AUTOMATION_REPLY_DELAY_DEFAULT_SECONDS = 2 + +/** 等待上限。再长用户会以为功能坏了,所以封顶而不是无限放开。 */ +export const AUTOMATION_REPLY_DELAY_MAX_SECONDS = 60 + +/** + * 把任意输入收敛成合法的等待秒数。 + * + * 口径(三条都是刻意的): + * - **空值 / 非法值 → 默认值**,而不是 0 —— 填错了不该静默变成「不等待」; + * - **0 是合法值**(显式表示立刻回复),负数与它同归 0; + * - 超过上限按上限截断,避免有人填 10 分钟。 + */ +export function normalizeReplyDelaySeconds(value: unknown): number { + if (value === null || value === undefined || value === '') { + return AUTOMATION_REPLY_DELAY_DEFAULT_SECONDS + } + const parsed = typeof value === 'number' ? value : Number(String(value).trim()) + if (!Number.isFinite(parsed)) return AUTOMATION_REPLY_DELAY_DEFAULT_SECONDS + if (parsed <= 0) return 0 + return Math.min(AUTOMATION_REPLY_DELAY_MAX_SECONDS, Math.round(parsed)) +} + /** 所有自动化行为的 `sourceId` / `executionId` 口径:一次执行一个 id。 */ export function automationIdempotencyKey( kind: keyof typeof AUTOMATION_SEND_PURPOSE, @@ -125,6 +199,8 @@ export interface AutomationRuleDraft { conditions: AutomationConditions actions: AutomationAction[] cooldownSeconds: number + /** 命中后等多久再回复确认(秒)。0 = 立刻回复。 */ + replyDelaySeconds: number } /** 顶部状态条数据(全部来自 main 侧真实能力,UI 不做平台判断)。 */ @@ -197,6 +273,18 @@ export const AUTOMATION_STEP_LABELS: Record = { send: '发送日报图片' } +/** + * 执行结果的展示文案。 + * + * `skipped` 是**中性**状态(命中了但被规则自身的冷却 / 去重拦下), + * 不是失败 —— 界面不能用红色渲染它。 + */ +export const AUTOMATION_EXECUTION_STATUS_LABELS: Record = { + running: '执行中', + success: '成功', + failed: '失败' +} + export const KEYWORD_MATCH_MODE_LABELS: Record = { contains: '包含', exact: '完全匹配', @@ -281,6 +369,7 @@ export function createDefaultDailyReportRule(now: number): AutomationRule { { type: 'sendReportImage', enabled: true } ], cooldownSeconds: 60, + replyDelaySeconds: AUTOMATION_REPLY_DELAY_DEFAULT_SECONDS, createdAt: now, updatedAt: now } @@ -323,6 +412,7 @@ export function normalizeRuleDraft(input: unknown, fallbackName = '未命名自 ignoreSelf: conditions.ignoreSelf !== false }, actions: normalizedActions, - cooldownSeconds: Number.isFinite(cooldown) ? Math.max(0, Math.floor(cooldown)) : 60 + cooldownSeconds: Number.isFinite(cooldown) ? Math.max(0, Math.floor(cooldown)) : 60, + replyDelaySeconds: normalizeReplyDelaySeconds(raw.replyDelaySeconds) } } diff --git a/src/shared/wechat-action.ts b/src/shared/wechat-action.ts index 423ba0b..4908c0d 100644 --- a/src/shared/wechat-action.ts +++ b/src/shared/wechat-action.ts @@ -2,6 +2,14 @@ export type WechatActionOrigin = | 'member_monitor' | 'scheduled_report' | 'user_tts' + /** + * 用户在界面上**手动**触发的发送(发送日报图片、给群发统计文本等)。 + * + * 立项规则:**所有发送都必须经过 `WechatActionGateway`**。手动发送在改造前是 + * 从 IPC 直连 `PersonalWechatSendService` 的,因此没有审计记录、没有幂等、 + * 也不进 Send Log —— 排查「消息到底发没发出去」时会缺一份证据。 + */ + | 'user_manual' /** Automation v1(@我生成日报)。与 `shared/automation.ts` 的 AUTOMATION_SEND_ORIGIN 一致。 */ | 'automation' | 'unknown' @@ -11,6 +19,10 @@ export type WechatActionPurpose = | 'member_left_notification' | 'scheduled_report' | 'tts_voice' + /** 用户手动发送图片(日报图片 / 群成员统计图)。 */ + | 'manual_image' + /** 用户手动发送文本。 */ + | 'manual_text' /** * Automation v1 的两个用途。 * diff --git a/tests/component/automation-rule-editor.test.tsx b/tests/component/automation-rule-editor.test.tsx new file mode 100644 index 0000000..949fdf4 --- /dev/null +++ b/tests/component/automation-rule-editor.test.tsx @@ -0,0 +1,118 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { RuleEditorPanel } from '../../src/renderer/src/features/automation/RuleEditorPanel' +import { + createDefaultDailyReportRule, + type AutomationRule, + type AutomationRuleDraft +} from '../../src/shared/automation' + +/** + * 「回复前等待」是**规则自己的一项执行参数**,配置位置就是 + * 「编辑自动化 → 3 · 触发后执行(命中条件后,TraceMemo 会按下面的顺序依次执行)」。 + * + * 这一层要锁的是**接线**:初值必须来自规则本身、保存必须带回去。 + * 因为 `draftFromRule` 是逐字段搬运的,漏一个字段就会被 `normalizeRuleDraft` + * 的默认值悄悄覆盖(把用户设成 0 的等待重置回 2 秒),这种 bug 只有真渲染才发现。 + */ +const GROUPS = [{ id: '12345678@chatroom', name: '测试群' }] + +function renderEditor( + rule: AutomationRule, + onSave: (draft: AutomationRuleDraft) => void = () => {} +): void { + render( + {}} + onSave={onSave} + /> + ) +} + +const delayField = (): HTMLInputElement => + screen.getByLabelText('回复前等待秒数') as HTMLInputElement + +describe('RuleEditorPanel · 回复前等待', () => { + it('它就在「3 · 触发后执行」里,和动作顺序在同一段', () => { + renderEditor(createDefaultDailyReportRule(0)) + + expect(screen.getByText('命中条件后,TraceMemo 会按下面的顺序依次执行。')).toBeInTheDocument() + expect(delayField()).toBeInTheDocument() + expect(screen.getByText('回复前等待(秒)')).toBeInTheDocument() + }) + + it('初值来自规则本身,不是硬编码的默认值', () => { + const rule = createDefaultDailyReportRule(0) + rule.replyDelaySeconds = 7 + renderEditor(rule) + + expect(delayField()).toHaveValue(7) + }) + + it('保存时把改动带回去', () => { + const saved: AutomationRuleDraft[] = [] + renderEditor(createDefaultDailyReportRule(0), (draft) => saved.push(draft)) + + fireEvent.change(delayField(), { target: { value: '0' } }) + fireEvent.click(screen.getByRole('button', { name: '保存' })) + + expect(saved).toHaveLength(1) + expect(saved[0].replyDelaySeconds).toBe(0) + }) + + it('用户设成 0(立刻回复)时,保存不会把它顶回默认 2 秒', () => { + const saved: AutomationRuleDraft[] = [] + const rule = createDefaultDailyReportRule(0) + rule.replyDelaySeconds = 0 + renderEditor(rule, (draft) => saved.push(draft)) + + // 什么都不改,直接保存:0 必须原样保留(而不是被默认的 2 顶掉)。 + fireEvent.click(screen.getByRole('button', { name: '保存' })) + + expect(saved[0].replyDelaySeconds).toBe(0) + }) +}) + +/** + * 「所有群聊」的边界提示。 + * + * 真实能力是:同一时刻多个会话同时来消息时可能漏掉少量触发。 + * 这条限制**只挂在「所有群聊」这个上下文里** —— 首页不挂、指定群聊时不挂, + * 而且只讲用户能理解的结果,不解释表事件 / 会话回读这类实现细节。 + */ +const LIMITATION_TEXT = '当前版本在多个会话同时收到消息时,极少数自动化触发可能遗漏。' + +function renderScopeEditor(conversationIds: string[]): void { + const rule = createDefaultDailyReportRule(0) + rule.conditions.conversationIds = conversationIds + renderEditor(rule) +} + +describe('RuleEditorPanel · 生效范围的边界提示', () => { + it('一个群都没选(所有群聊)时显示提示', () => { + renderScopeEditor([]) + + // 编辑页与右侧效果预览都会显示范围描述,所以这里用 getAllByText。 + expect(screen.getAllByText('所有群聊').length).toBeGreaterThan(0) + expect(screen.getAllByText(LIMITATION_TEXT)).toHaveLength(1) + }) + + it('选了具体群聊时不显示提示', () => { + renderScopeEditor([GROUPS[0].id]) + + expect(screen.getAllByText('已选 1 个群').length).toBeGreaterThan(0) + expect(screen.queryAllByText(LIMITATION_TEXT)).toHaveLength(0) + }) + + it('提示里不出现底层实现词,也不做绝对承诺', () => { + renderScopeEditor([]) + + expect(LIMITATION_TEXT).not.toMatch(/WCDB|SessionTable|表变化|native|回读/) + expect(document.body.textContent).not.toMatch(/覆盖所有群聊|100%|不会遗漏/) + }) +}) diff --git a/tests/component/automation-workspace-status.test.tsx b/tests/component/automation-workspace-status.test.tsx new file mode 100644 index 0000000..21ae7f9 --- /dev/null +++ b/tests/component/automation-workspace-status.test.tsx @@ -0,0 +1,44 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' + +import { ToastProvider } from '../../src/renderer/src/components/ui' +import { AutomationWorkspace } from '../../src/renderer/src/features/automation/AutomationWorkspace' + +/** + * 首页状态卡只讲用户能理解的状态。 + * + * 以前这里挂着一句「当前仅能捕获最近活跃的会话」,还用 Tooltip 解释了 + * 「微信底层只会上报有表发生了变化」——那是开发报告的内容,不该出现在普通用户界面。 + * 真实边界改挂在「规则编辑页 → 所有群聊」那个具体上下文里。 + */ +describe('Automation 首页状态卡', () => { + function renderWorkspace(): void { + render( + + + + ) + } + + it('仍然如实展示「消息监听」与发送能力两项', () => { + renderWorkspace() + + expect(screen.getByText('消息监听')).toBeInTheDocument() + expect(screen.getByText('发送能力')).toBeInTheDocument() + }) + + it('不再出现「最近活跃的会话」这类实现说明', () => { + renderWorkspace() + + expect(screen.queryByText(/最近活跃的会话/)).toBeNull() + expect(screen.queryByText(/有表发生了变化/)).toBeNull() + }) + + it('首页不出现底层实现词', () => { + renderWorkspace() + + expect(document.body.textContent).not.toMatch( + /WCDB|SessionTable|表变化|native event|回读机制|coalesce/ + ) + }) +}) diff --git a/tests/integration/automation-chain.test.ts b/tests/integration/automation-chain.test.ts index 4af976c..14b2528 100644 --- a/tests/integration/automation-chain.test.ts +++ b/tests/integration/automation-chain.test.ts @@ -131,6 +131,10 @@ function buildChain(): Chain { const store = new AutomationRuleStore({ userDataPath: () => root }) const log = new AutomationExecutionLogService({ userDataPath: () => root }) + // 回复等待归零:端到端链路要跑得快且可复现。 + // 真实默认值是 2 秒(编辑自动化 → 3 · 触发后执行),等待语义本身由 runner 单测覆盖。 + const seeded = store.listRules()[0] + if (seeded) store.updateRule(seeded.id, { ...seeded, replyDelaySeconds: 0 }) const runner = new AutomationActionRunner({ executeAction: (request) => gateway.execute(request), generateReport: async (request) => { diff --git a/tests/unit/automation-action-runner.test.ts b/tests/unit/automation-action-runner.test.ts index cbfb4c7..d334f8e 100644 --- a/tests/unit/automation-action-runner.test.ts +++ b/tests/unit/automation-action-runner.test.ts @@ -53,6 +53,14 @@ function fakeClock(step = 5): () => number { } } +/** + * 默认不真的等。 + * + * 内置规则的 `replyDelaySeconds` 默认是 2 秒,如果每条例都真 sleep, + * 这个文件会平白多出几十秒。**等待语义**由「回复前等待」那组用例显式覆盖。 + */ +const noDelay = async (): Promise => {} + function sentResult(): WechatActionResult { return { actionId: 'action-1', @@ -78,6 +86,7 @@ function failedResult(code: string, reason: string): WechatActionResult { function buildRunner(options: { executeAction?: (request: WechatActionRequest) => Promise generateReport?: () => Promise + delay?: (ms: number) => Promise }): { runner: AutomationActionRunner calls: WechatActionRequest[] @@ -87,6 +96,7 @@ function buildRunner(options: { const reports: Array<{ group: string; range?: string }> = [] const runner = new AutomationActionRunner({ now: fakeClock(), + delay: options.delay ?? noDelay, executeAction: async (request) => { calls.push(request) return options.executeAction ? options.executeAction(request) : sentResult() @@ -184,6 +194,8 @@ describe('AutomationActionRunner', () => { expect(statusOf(result, 'report')).toBe('failed') expect(statusOf(result, 'send')).toBe('skipped') expect(result.errorSummary).toBe('所选时间范围没有可总结的消息') + // 跳过必须写清「是因为前置那步挂了」,否则用户看到「已跳过」会以为规则没配好。 + expect(result.steps.find((step) => step.key === 'send')?.skipReason).toContain('前置步骤失败') // 只发过那条文字回复,图片一次都没发。 expect(calls).toHaveLength(1) expect(calls[0].content.type).toBe('text') @@ -245,16 +257,38 @@ describe('AutomationActionRunner', () => { expect(calls[0].content.type).toBe('image') }) - it('未启用生成日报时,发送图片也必须 skipped(不能凭空发图)', async () => { + it('规则没启用「发送日报图片」时,该步 skipped 且整体仍算成功(情况 A)', async () => { const { runner, calls, reports } = buildRunner({}) + const rule = makeRule((item) => { + item.actions = item.actions.filter((action) => action.type !== 'sendReportImage') + }) + const result = await run(runner, rule) + + expect(result.status).toBe('success') + expect(statusOf(result, 'reply')).toBe('success') + expect(statusOf(result, 'report')).toBe('success') + expect(statusOf(result, 'send')).toBe('skipped') + // 没配发送就只发了一条回复,不该有多余的图片请求。 + expect(calls).toHaveLength(1) + expect(calls[0].content.type).toBe('text') + expect(reports).toHaveLength(1) + }) + + it('规则要求发送图片、但手上没有图片时判失败,不允许静默跳过(情况 B)', async () => { + const { runner, calls } = buildRunner({}) + // 只留 reply + send:没有 generateReport ⇒ 永远拿不到 pngPath。 const rule = makeRule((item) => { item.actions = item.actions.filter((action) => action.type !== 'generateReport') }) const result = await run(runner, rule) expect(statusOf(result, 'report')).toBe('skipped') - expect(statusOf(result, 'send')).toBe('skipped') - expect(reports).toHaveLength(0) + expect(statusOf(result, 'send')).toBe('failed') + // 关键:整次执行必须是 failed(合并分支会把它错记成 success)。 + expect(result.status).toBe('failed') + const sendStep = result.steps.find((step) => step.key === 'send') + expect(sendStep?.error).toContain('日报图片未生成') + // 而且**绝不**退而求其次去发别的东西:只有那条文字回复出去过。 expect(calls).toHaveLength(1) expect(calls[0].content.type).toBe('text') }) @@ -287,6 +321,7 @@ describe('AutomationActionRunner', () => { const executeAction = vi.fn(async (_request: WechatActionRequest) => sentResult()) const runner = new AutomationActionRunner({ now: fakeClock(), + delay: noDelay, executeAction, generateReport: async () => ({ success: true, pngPath: '/tmp/report.png' }) }) @@ -304,4 +339,95 @@ describe('AutomationActionRunner', () => { id: 'wxid_friend' }) }) + + /** + * 回复前等待:规则一命中就秒回看起来像机器人,所以它是**规则自己的一项执行参数** + * (`rule.replyDelaySeconds`,在「编辑自动化 → 3 · 触发后执行」里配), + * 而不是全局设置 —— 不同规则可以不一样。 + * + * 这一组锁三件事:等待真的发生在回复之前、等待不计入回复步骤耗时、 + * 以及「不该等的时候一步都不等」。 + */ + it('回复等待发生在发送回复之前,顺序为 等待 → 回复 → 日报 → 图片', async () => { + const order: string[] = [] + const delay = vi.fn(async (ms: number) => { + order.push(`delay:${ms}`) + }) + const runner = new AutomationActionRunner({ + now: fakeClock(), + delay, + executeAction: async (request) => { + order.push(`send:${request.content.type}`) + return sentResult() + }, + generateReport: async () => { + order.push('report') + return { success: true, pngPath: '/tmp/report.png' } + } + }) + + await run( + runner, + makeRule((rule) => { + rule.replyDelaySeconds = 2 + }) + ) + + expect(delay).toHaveBeenCalledTimes(1) + expect(delay).toHaveBeenCalledWith(2_000) + expect(order).toEqual(['delay:2000', 'send:text', 'report', 'send:image']) + }) + + it('等待不计入「回复确认」这一步的耗时', async () => { + const { runner } = buildRunner({ delay: noDelay }) + const result = await run( + runner, + makeRule((rule) => { + rule.replyDelaySeconds = 2 + }) + ) + + // fakeClock 每调用一次进 5ms:reply 从 startedAt 到 finishedAt 只跨一次 now()。 + const replyStep = result.steps.find((step) => step.key === 'reply') + expect(replyStep?.durationMs).toBe(5) + }) + + it('等待为 0 时完全不等待;字段缺失(旧版 rules.json)按默认 2 秒', async () => { + const delay = vi.fn(noDelay) + const { runner } = buildRunner({ delay }) + + await run( + runner, + makeRule((rule) => { + rule.replyDelaySeconds = 0 + }) + ) + expect(delay).not.toHaveBeenCalled() + + // 历史规则里没有这个字段:按默认 2 秒处理,而不是凭空变成「不等待」。 + await run( + runner, + makeRule((rule) => { + delete (rule as { replyDelaySeconds?: number }).replyDelaySeconds + }) + ) + expect(delay).toHaveBeenCalledWith(2_000) + }) + + it('回复动作被关掉时不再空等', async () => { + const delay = vi.fn(noDelay) + const { runner } = buildRunner({ delay }) + + await run( + runner, + makeRule((rule) => { + rule.replyDelaySeconds = 2 + for (const action of rule.actions) { + if (action.type === 'replyText') action.enabled = false + } + }) + ) + + expect(delay).not.toHaveBeenCalled() + }) }) diff --git a/tests/unit/automation-reply-delay.test.ts b/tests/unit/automation-reply-delay.test.ts new file mode 100644 index 0000000..a796696 --- /dev/null +++ b/tests/unit/automation-reply-delay.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest' + +import { + AUTOMATION_REPLY_DELAY_DEFAULT_SECONDS, + AUTOMATION_REPLY_DELAY_MAX_SECONDS, + createDefaultDailyReportRule, + normalizeReplyDelaySeconds, + normalizeRuleDraft +} from '../../src/shared/automation' + +/** + * 「回复前等待」的口径:它是**规则自己的一项执行参数**(`replyDelaySeconds`,秒), + * 与 `cooldownSeconds` 同口径 —— 界面上是秒、落盘也是秒,只有 runner 换算成毫秒。 + * + * 核心契约:**填错了不该静默变成「不等待」**。默认 2 秒、0 是合法值、 + * 垃圾输入回落默认、超限截断。 + */ +describe('normalizeReplyDelaySeconds', () => { + it('缺省 / 空串 / null 都回落到默认 2 秒', () => { + for (const value of [undefined, null, '']) { + expect(normalizeReplyDelaySeconds(value)).toBe(AUTOMATION_REPLY_DELAY_DEFAULT_SECONDS) + } + expect(AUTOMATION_REPLY_DELAY_DEFAULT_SECONDS).toBe(2) + }) + + it('非法输入回落到默认值,而不是 0', () => { + for (const value of ['abc', NaN, Infinity, -Infinity, '两秒', {}]) { + expect(normalizeReplyDelaySeconds(value)).toBe(AUTOMATION_REPLY_DELAY_DEFAULT_SECONDS) + } + }) + + it('0 与负数都收敛成「不等待」,但两者语义不同 —— 0 是显式合法值', () => { + expect(normalizeReplyDelaySeconds(0)).toBe(0) + expect(normalizeReplyDelaySeconds('0')).toBe(0) + expect(normalizeReplyDelaySeconds(-1)).toBe(0) + }) + + it('正常值取整保留,超过上限按上限截断', () => { + expect(normalizeReplyDelaySeconds(5)).toBe(5) + expect(normalizeReplyDelaySeconds('30')).toBe(30) + expect(normalizeReplyDelaySeconds(4.6)).toBe(5) + expect(normalizeReplyDelaySeconds(AUTOMATION_REPLY_DELAY_MAX_SECONDS + 1)).toBe( + AUTOMATION_REPLY_DELAY_MAX_SECONDS + ) + expect(normalizeReplyDelaySeconds(99_999)).toBe(AUTOMATION_REPLY_DELAY_MAX_SECONDS) + }) +}) + +describe('规则里的回复前等待', () => { + it('内置「@我生成日报」默认 2 秒', () => { + expect(createDefaultDailyReportRule(0).replyDelaySeconds).toBe( + AUTOMATION_REPLY_DELAY_DEFAULT_SECONDS + ) + }) + + it('草稿归一化会带上这个字段(否则编辑页拿不到值)', () => { + const draft = normalizeRuleDraft({ + name: '测试', + replyDelaySeconds: 7, + actions: [{ type: 'replyText', enabled: true, text: '收到' }] + }) + expect(draft.replyDelaySeconds).toBe(7) + }) + + it('旧版 rules.json 里的规则没有这个字段 → 读出来是默认 2 秒,不是 undefined', () => { + const draft = normalizeRuleDraft({ + name: '旧规则', + actions: [{ type: 'replyText', enabled: true, text: '收到' }] + }) + expect(draft.replyDelaySeconds).toBe(AUTOMATION_REPLY_DELAY_DEFAULT_SECONDS) + }) + + it('草稿里的脏值同样被收敛', () => { + expect( + normalizeRuleDraft({ name: 'x', replyDelaySeconds: -3, actions: [] }).replyDelaySeconds + ).toBe(0) + expect( + normalizeRuleDraft({ name: 'x', replyDelaySeconds: 'abc', actions: [] }).replyDelaySeconds + ).toBe(AUTOMATION_REPLY_DELAY_DEFAULT_SECONDS) + expect( + normalizeRuleDraft({ name: 'x', replyDelaySeconds: 1e9, actions: [] }).replyDelaySeconds + ).toBe(AUTOMATION_REPLY_DELAY_MAX_SECONDS) + }) +}) diff --git a/tests/unit/automation-service.test.ts b/tests/unit/automation-service.test.ts index 1dbb27c..8c2be30 100644 --- a/tests/unit/automation-service.test.ts +++ b/tests/unit/automation-service.test.ts @@ -100,7 +100,18 @@ interface Harness { setRunResult: (value: AutomationRunResult) => void } -function buildHarness(options: { now?: () => number; capability?: PersonalWechatSendCapability } = {}): Harness { +function buildHarness( + options: { + now?: () => number + capability?: PersonalWechatSendCapability + /** 执行期间的挂起点:resolve 之前 `runner.run` 不会返回(用来模拟长任务)。 */ + holdExecution?: () => Promise + /** 每次真正进入 runner 时通知测试,便于确定时序。 */ + onRunStart?: () => void + /** gate 表容量上限,用来在单测里触发清理路径。 */ + gateMaxEntries?: number + } = {} +): Harness { const store = new AutomationRuleStore() const log = new AutomationExecutionLogService() const runs: AutomationRunInput[] = [] @@ -120,6 +131,8 @@ function buildHarness(options: { now?: () => number; capability?: PersonalWechat const spyRunner = { run: async (input: AutomationRunInput): Promise => { runs.push(input) + options.onRunStart?.() + await options.holdExecution?.() return runResult } } as unknown as AutomationActionRunner @@ -135,7 +148,8 @@ function buildHarness(options: { now?: () => number; capability?: PersonalWechat runner: spyRunner, getCapability: async () => currentCapability, isListening: () => true, - ...(options.now ? { now: options.now } : {}) + ...(options.now ? { now: options.now } : {}), + ...(options.gateMaxEntries !== undefined ? { gateMaxEntries: options.gateMaxEntries } : {}) }) return { @@ -187,13 +201,17 @@ describe('AutomationService', () => { expect(records[0].executionId).toBe(harness.runs[0].executionId) }) - it('同一条消息被重复投递时只执行一次', async () => { + it('同一条消息被重复投递时只执行一次,且**不产生任何额外记录**', async () => { const harness = buildHarness() + // 关掉 cooldown,单独验证「同消息幂等」这条闸 —— 否则第二次会先被 gate 拦下。 + const seeded = harness.store.listRules()[0] + harness.store.updateRule(seeded.id, { ...seeded, cooldownSeconds: 0 }) + await harness.service.handleMessage(message()) await harness.service.handleMessage(message()) - await harness.service.handleMessage(message({ localId: '100' })) expect(harness.runs).toHaveLength(1) + // 重复投递是「这条消息已经处理过」,不是「跳过了一次执行」——用户日志里不该出现。 expect(harness.log.list()).toHaveLength(1) }) @@ -205,7 +223,7 @@ describe('AutomationService', () => { expect(harness.runs).toHaveLength(2) }) - it('cooldown 内不重复触发', async () => { + it('cooldown 内不重复触发,且对用户完全静默', async () => { let now = 1_700_000_000_000 const harness = buildHarness({ now: () => now }) @@ -213,7 +231,12 @@ describe('AutomationService', () => { now += 30_000 await harness.service.handleMessage(message({ localId: '2' })) + // 只执行一次 —— 这条是硬要求。 expect(harness.runs).toHaveLength(1) + // 但第二次**不留任何痕迹**:没有 execution、没有「已跳过」、没有「冷却中」。 + expect(harness.log.list()).toHaveLength(1) + // 只进诊断计数(不上 UI)。 + expect(harness.service.getBlockedMessageCount()).toBe(1) }) it('cooldown 过后可以再次触发', async () => { @@ -370,6 +393,16 @@ describe('AutomationService', () => { }) }) + it('把命中时的规则整条交给 Runner(含回复前等待)', async () => { + const harness = buildHarness() + await harness.service.handleMessage(message()) + + expect(harness.runs).toHaveLength(1) + // 「回复前等待」是规则自己的字段,编排层只负责把规则原样传下去 —— + // 不该再有一份全局设置参与,否则同一字段有两个来源就必然对不上。 + expect(harness.runs[0].rule.replyDelaySeconds).toBe(2) + }) + it('handleMessage 内部异常不会向外抛', async () => { const harness = buildHarness() const brokenStore = { @@ -385,3 +418,249 @@ describe('AutomationService', () => { void harness }) }) + +/** + * Automation rule conversation gate —— 规则 × 会话维度的阻塞窗口。 + * + * ``` + * blocked = inFlight || now < triggeredAt + cooldown // 粒度:ruleId + conversationId + * ``` + * + * 窗口内的消息:不匹配、不执行、不回复、**不写用户执行日志**,只进诊断计数。 + * `triggeredAt` 是**第一次触发**的时间;执行完成不重置它。 + */ +describe('AutomationService · rule conversation gate', () => { + const T0 = 1_700_000_000_000 + + // 这个 describe 在文件后段,**不会**继承上一个 describe 的 beforeEach; + // 而规则是写盘的(同一个 userData root),不清理就会互相污染 + // (Test 6 建的第二条规则会漏给 Test 7/8)。 + beforeEach(() => { + fs.removeSync(path.join(root, 'automation')) + }) + + it('Test 1 · 0s 触发,2/20/59s 全忽略,61s 重新触发(executions = 2 而不是 5)', async () => { + let now = T0 + const harness = buildHarness({ now: () => now }) + + await harness.service.handleMessage(message({ localId: '1' })) + for (const offset of [2_000, 20_000, 59_000]) { + now = T0 + offset + await harness.service.handleMessage(message({ localId: `t${offset}` })) + } + now = T0 + 61_000 + await harness.service.handleMessage(message({ localId: 'e' })) + + expect(harness.runs).toHaveLength(2) + expect(harness.log.list()).toHaveLength(2) + expect(harness.service.getBlockedMessageCount()).toBe(3) + }) + + it('Test 2 · 短任务(8s 就完成)仍然阻塞到 cooldown 到期', async () => { + let now = T0 + const harness = buildHarness({ now: () => now }) + + await harness.service.handleMessage(message({ localId: '1' })) // 0s 触发,立即完成 + now = T0 + 8_000 + now = T0 + 30_000 // 任务早完成了,但 cooldown 还没到 + await harness.service.handleMessage(message({ localId: '2' })) + expect(harness.runs).toHaveLength(1) + + now = T0 + 61_000 + await harness.service.handleMessage(message({ localId: '3' })) + expect(harness.runs).toHaveLength(2) + }) + + it('Test 3 · 长任务(超过 cooldown)一直阻塞到执行结束:解锁 = max(完成, 触发+cooldown)', async () => { + let now = T0 + let release!: () => void + const held = new Promise((resolve) => { + release = resolve + }) + let markStarted!: () => void + const started = new Promise((resolve) => { + markStarted = resolve + }) + const harness = buildHarness({ + now: () => now, + holdExecution: () => held, + onRunStart: markStarted + }) + + const firstRun = harness.service.handleMessage(message({ localId: '1' })) + await started + + // 61s:cooldown 早就过期了,但第一条执行还在跑 ⇒ 仍然忽略 + now = T0 + 61_000 + await harness.service.handleMessage(message({ localId: '2' })) + expect(harness.runs).toHaveLength(1) + + // 75s:第一条终于结束 + now = T0 + 75_000 + release() + await firstRun + + // 76s:这才重新允许 —— **不是**「完成后再等 60 秒」 + now = T0 + 76_000 + await harness.service.handleMessage(message({ localId: '3' })) + expect(harness.runs).toHaveLength(2) + }) + + it('Test 4 · 同群不同发送者也忽略(gate 不是 sender 级)', async () => { + let now = T0 + const harness = buildHarness({ now: () => now }) + + await harness.service.handleMessage(message({ localId: '1' })) + now = T0 + 20_000 + await harness.service.handleMessage( + message({ localId: '2', senderId: 'wxid_other', senderNickname: '李四' }) + ) + + expect(harness.runs).toHaveLength(1) + }) + + it('Test 5 · 同一条规则在另一个群正常执行(gate 是 ruleId + conversationId)', async () => { + let now = T0 + const harness = buildHarness({ now: () => now }) + + await harness.service.handleMessage(message({ localId: '1' })) + now = T0 + 10_000 + await harness.service.handleMessage(message({ localId: '2', sessionId: OTHER_GROUP })) + + expect(harness.runs).toHaveLength(2) + }) + + it('Test 6 · 同一群里的另一条规则不受影响(不是会话全局锁)', async () => { + let now = T0 + const harness = buildHarness({ now: () => now }) + const seeded = harness.store.listRules()[0] + // 第二条规则同样命中这条消息,但**没有冷却** —— 用它证明 gate 按 ruleId 隔离。 + harness.store.createRule({ + name: '帮助', + enabled: true, + scope: 'group', + conditions: { + requireMentionMe: true, + keyword: '日报', + keywordMatchMode: 'contains', + conversationIds: [], + ignoreSelf: true + }, + actions: [{ type: 'replyText', enabled: true, text: '好的' }], + cooldownSeconds: 0, + replyDelaySeconds: 0 + }) + + await harness.service.handleMessage(message({ localId: '1' })) + expect(harness.runs).toHaveLength(2) + + now = T0 + 10_000 + await harness.service.handleMessage(message({ localId: '2' })) + // 规则 A 被 gate 挡下,规则 B 照常执行。 + expect(harness.runs).toHaveLength(3) + expect(harness.runs[2].rule.id).not.toBe(seeded.id) + }) + + it('Test 7 · 第一条失败后仍然走完整 cooldown(避免故障期疯狂重试)', async () => { + let now = T0 + const harness = buildHarness({ now: () => now }) + harness.setRunResult({ steps: [], status: 'failed', errorSummary: 'AI 服务不可达' }) + + await harness.service.handleMessage(message({ localId: '1' })) + expect(harness.runs).toHaveLength(1) + + now = T0 + 5_000 + await harness.service.handleMessage(message({ localId: '2' })) + expect(harness.runs).toHaveLength(1) + + now = T0 + 61_000 + await harness.service.handleMessage(message({ localId: '3' })) + expect(harness.runs).toHaveLength(2) + }) + + it('Test 8 · 60 秒内 10 条符合条件的消息,用户执行日志只留 1 条', async () => { + let now = T0 + const harness = buildHarness({ now: () => now }) + + await harness.service.handleMessage(message({ localId: '1' })) + for (let index = 2; index <= 11; index += 1) { + now = T0 + index * 5_000 // 5s..55s,全部落在 60s 窗口内 + await harness.service.handleMessage(message({ localId: String(index) })) + } + + expect(harness.runs).toHaveLength(1) + const records = harness.log.list() + expect(records).toHaveLength(1) + expect(records[0].status).toBe('success') + // 不许出现「冷却中 / 剩余 N 秒」这类记录。 + expect(JSON.stringify(records)).not.toContain('冷却') + expect(harness.service.getBlockedMessageCount()).toBe(10) + }) +}) + +/** + * gate 表清理的硬不变量:**正在执行(`inFlight === true`)的 gate 永不被淘汰**。 + * + * 淘汰它 = 把一条正在跑的规则提前放开,立刻会产生第二次并发执行。 + * 这里用极小的上限(1)逼出清理路径。 + */ +describe('AutomationService · gate 缓存清理', () => { + const T0 = 1_700_000_000_000 + const GROUP_A = 'aaa@chatroom' + const GROUP_B = 'bbb@chatroom' + const GROUP_C = 'ccc@chatroom' + + beforeEach(() => { + fs.removeSync(path.join(root, 'automation')) + }) + + it('清理只删「inFlight=false 且 cooldown 已过期」的条目,正在执行的一条必须留着', async () => { + let now = T0 + let release!: () => void + const held = new Promise((resolve) => { + release = resolve + }) + let markStarted!: () => void + const started = new Promise((resolve) => { + markStarted = resolve + }) + // 只把**第一次**执行挂住(也就是 A 群那条);B/C 的执行立即完成。 + let runCount = 0 + const harness = buildHarness({ + now: () => now, + gateMaxEntries: 1, + holdExecution: () => { + runCount += 1 + return runCount === 1 ? held : Promise.resolve() + }, + onRunStart: markStarted + }) + // cooldown = 0:这样「执行完成」的 gate 立刻就算「已解锁」,可以被清理。 + const seeded = harness.store.listRules()[0] + harness.store.updateRule(seeded.id, { ...seeded, cooldownSeconds: 0 }) + + // A 群:开始执行并**挂住**(gate A 处于 inFlight) + const runningA = harness.service.handleMessage( + message({ localId: 'a1', sessionId: GROUP_A }) + ) + await started + expect(harness.runs).toHaveLength(1) + + // B 群:执行一次并正常结束(gate B 变成「已解锁」,是本次清理的目标) + now = T0 + 1_000 + await harness.service.handleMessage(message({ localId: 'b1', sessionId: GROUP_B })) + expect(harness.runs).toHaveLength(2) + + // C 群:claim 一条新 gate ⇒ 表 size 超过上限 ⇒ 触发 evictGatesIfNeeded + now = T0 + 2_000 + await harness.service.handleMessage(message({ localId: 'c1', sessionId: GROUP_C })) + + // 关键不变量:A 的执行还在跑,往 A 发消息必须**仍然被挡住**。 + now = T0 + 3_000 + await harness.service.handleMessage(message({ localId: 'a2', sessionId: GROUP_A })) + expect(harness.runs.filter((run) => run.conversationId === GROUP_A)).toHaveLength(1) + + release() + await runningA + }) +}) diff --git a/tests/unit/message-listener-service.test.ts b/tests/unit/message-listener-service.test.ts index bfd71ed..dcb7044 100644 --- a/tests/unit/message-listener-service.test.ts +++ b/tests/unit/message-listener-service.test.ts @@ -6,6 +6,7 @@ import { extractMentionTargets, type NormalizedIncomingMessage } from '../../src/main/services/message-listener-service' +import { parseMonitorEvent } from '../../src/main/wcdb4-client' import type { Wcdb4Client, Wcdb4Message } from '../../src/main/wcdb4-client' /** @@ -344,3 +345,166 @@ describe('Observation Mode 约束', () => { expect(received).toHaveLength(0) }) }) + +/** + * Native Monitor Event v2:pipe payload 里带上**发生变化的会话**。 + * + * 这一组锁的是「不再依赖 `getSessions()[0]`」这件事,以及两个必须保留的行为: + * legacy(v1 runtime)fallback 和同消息 dedup。 + */ +describe('parseMonitorEvent(协议解析)', () => { + it('v2 + message_change + session_id ⇒ precise', () => { + const event = parseMonitorEvent( + JSON.stringify({ + version: 2, + kind: 'message_change', + session_id: 'group_A@chatroom', + db: 'message_0.db', + table: 'message', + action: 'update', + observed_at_ms: 1_700_000_000_000 + }) + ) + expect(event.protocol).toBe(2) + expect(event.sessionId).toBe('group_A@chatroom') + expect(event.table).toBe('message') + expect(event.observedAtMs).toBe(1_700_000_000_000) + }) + + it('v1 payload 仍然解析成 legacy(不带会话)', () => { + const event = parseMonitorEvent( + JSON.stringify({ db: 'session.db', table: 'Session', action: 'update' }) + ) + expect(event.protocol).toBe(1) + expect(event.sessionId).toBeUndefined() + expect(event.action).toBe('update') + expect(event.raw).toContain('session.db') + }) + + it('v2 但缺 session_id / kind 不对 ⇒ 一律降级成 legacy,绝不猜会话', () => { + for (const payload of [ + { version: 2, kind: 'message_change' }, + { version: 2, kind: 'message_change', session_id: ' ' }, + { version: 2, session_id: 'group_A@chatroom' }, + { version: 1, kind: 'message_change', session_id: 'group_A@chatroom' } + ]) { + expect(parseMonitorEvent(JSON.stringify(payload)).protocol).toBe(1) + expect(parseMonitorEvent(JSON.stringify(payload)).sessionId).toBeUndefined() + } + }) + + it('不是 JSON 也不崩', () => { + const event = parseMonitorEvent('not-json') + expect(event.protocol).toBe(1) + expect(event.action).toBe('update') + }) +}) + +describe('MessageListener · precise session path', () => { + const v2Event = (sessionId: string) => + parseMonitorEvent( + JSON.stringify({ version: 2, kind: 'message_change', session_id: sessionId, table: 'message' }) + ) + const v1Event = () => + parseMonitorEvent(JSON.stringify({ db: 'session.db', table: 'Session', action: 'update' })) + + /** 按会话返回消息,并记录每个会话被回读了几次。 */ + function multiSessionClient(bySession: Record): { + client: Wcdb4Client + calls: string[] + } { + const calls: string[] = [] + const client = { + // legacy fallback 用的「最近活跃会话」,故意与 precise 的会话不同。 + getSessions: vi.fn(() => [{ username: 'recent@chatroom' }]), + getMessagesAsync: vi.fn(async (sessionId: string) => { + calls.push(sessionId) + return bySession[sessionId] ?? [] + }) + } as unknown as Wcdb4Client + return { client, calls } + } + + it('v2 事件直接按 sessionId 回读,不再碰 getSessions()[0]', async () => { + const { client, calls } = multiSessionClient({ + 'A@chatroom': [makeMessage({ mesLocalID: '1' })] + }) + const listener = new MessageListenerService(client) + collect(listener) + + listener.handleNativeChange(v2Event('A@chatroom')) + await flush() + + expect(calls).toEqual(['A@chatroom']) + expect(received.map((message) => message.sessionId)).toEqual(['A@chatroom']) + }) + + it('120ms 内 A/B/A/C 会回读 A、B、C 各一次(不是「最后一个 wins」)', async () => { + const { client, calls } = multiSessionClient({ + 'A@chatroom': [makeMessage({ mesLocalID: '1' })], + 'B@chatroom': [makeMessage({ mesLocalID: '2' })], + 'C@chatroom': [makeMessage({ mesLocalID: '3' })] + }) + const listener = new MessageListenerService(client) + collect(listener) + + listener.handleNativeChange(v2Event('A@chatroom')) + listener.handleNativeChange(v2Event('B@chatroom')) + listener.handleNativeChange(v2Event('A@chatroom')) + listener.handleNativeChange(v2Event('C@chatroom')) + await flush() + + expect(calls.sort()).toEqual(['A@chatroom', 'B@chatroom', 'C@chatroom']) + expect(received.map((message) => message.sessionId).sort()).toEqual([ + 'A@chatroom', + 'B@chatroom', + 'C@chatroom' + ]) + }) + + it('一批里既有 v2 又有 v1 时,仍然按 v2 的精确会话回读', async () => { + const { client, calls } = multiSessionClient({ + 'A@chatroom': [makeMessage({ mesLocalID: '1' })], + 'recent@chatroom': [makeMessage({ mesLocalID: '9' })] + }) + const listener = new MessageListenerService(client) + collect(listener) + + listener.handleNativeChange(v2Event('A@chatroom')) + listener.handleNativeChange(v1Event()) + await flush() + + expect(calls).toEqual(['A@chatroom']) + }) + + it('legacy(v1 runtime)仍然退回最近活跃会话,行为不变', async () => { + const { client, calls } = multiSessionClient({ + 'recent@chatroom': [makeMessage({ mesLocalID: '1' })] + }) + const listener = new MessageListenerService(client) + collect(listener) + + listener.handleNativeChange(v1Event()) + await flush() + + expect(calls).toEqual(['recent@chatroom']) + expect(received).toHaveLength(1) + }) + + it('native 重复发同一条消息的 v2 事件,只会投递一次', async () => { + const { client, calls } = multiSessionClient({ + 'A@chatroom': [makeMessage({ mesLocalID: '42' })] + }) + const listener = new MessageListenerService(client) + collect(listener) + + listener.handleNativeChange(v2Event('A@chatroom')) + await flush() + listener.handleNativeChange(v2Event('A@chatroom')) + await flush() + + // 回读了两次(两次事件各一个窗口),但同一条消息只投递一次。 + expect(calls).toEqual(['A@chatroom', 'A@chatroom']) + expect(received).toHaveLength(1) + }) +}) diff --git a/tests/unit/wechat-action-gateway.test.ts b/tests/unit/wechat-action-gateway.test.ts index 2089eb4..7a56f2c 100644 --- a/tests/unit/wechat-action-gateway.test.ts +++ b/tests/unit/wechat-action-gateway.test.ts @@ -18,10 +18,11 @@ vi.mock('../../src/main/services/personal-wechat-send-service', () => ({ import { AUTOMATION_SEND_INTERVAL_MS, - WechatActionGateway + WechatActionGateway, + toPersonalWechatSendResult } from '../../src/main/services/wechat-action-gateway' import type { PersonalWechatSendCapability } from '../../src/shared/personal-wechat' -import type { WechatActionResult } from '../../src/shared/wechat-action' +import type { WechatActionRequest, WechatActionResult } from '../../src/shared/wechat-action' const readyCapability: PersonalWechatSendCapability = { supported: true, @@ -375,4 +376,132 @@ describe('WechatActionGateway', () => { expect(Date.now()).toBe(startedAt) vi.useRealTimers() }) + + /** + * 项目规则:**所有发送都走 WechatActionGateway**。 + * + * 手动发送(`wechat-personal:send`)改造前是直连 `PersonalWechatSendService` 的, + * 于是没有审计、没有幂等、也不进 Send Log。这一组锁住改造后的语义。 + */ + describe('用户手动发送(triggerType: user)', () => { + function manualAction( + gateway: WechatActionGateway, + overrides: Partial = {} + ): Promise { + return gateway.execute({ + origin: 'user_manual', + purpose: 'manual_image', + triggerType: 'user', + recipient: { type: 'group', id: 'room@chatroom' }, + content: { type: 'image', path: '/tmp/report.png' }, + ...overrides + } as WechatActionRequest) + } + + it('放行、发送一次,并写入审计记录', async () => { + const userData = mkdtempSync(join(tmpdir(), 'tracememo-wechat-action-')) + directories.push(userData) + const gateway = new WechatActionGateway({ getUserDataPath: () => userData }) + + const result = await manualAction(gateway) + + expect(result).toMatchObject({ status: 'sent', decision: 'allow' }) + expect(mocks.sender.send).toHaveBeenCalledWith({ + type: 'image', + to: 'room@chatroom', + isGroup: true, + filePath: '/tmp/report.png' + }) + const audit = readJsonSync(join(userData, 'actions', 'wechat-actions.json')) + expect(audit).toEqual([ + expect.objectContaining({ + origin: 'user_manual', + purpose: 'manual_image', + triggerType: 'user', + recipientId: 'room@chatroom', + decision: 'allow', + sendStatus: 'sent' + }) + ]) + }) + + it('不受 automation 的 purpose allowlist 限制,也不吃 3s 节流', async () => { + vi.useFakeTimers() + vi.setSystemTime(new Date('2026-09-21T01:00:00.000Z')) + const gateway = createGateway() + + const first = await manualAction(gateway) + const second = await manualAction(gateway) + + expect(first.status).toBe('sent') + expect(second.status).toBe('sent') + // 节流只对 triggerType === 'automation' 生效:两次点击必须立刻都发出去。 + expect(mocks.sender.send).toHaveBeenCalledTimes(2) + vi.useRealTimers() + }) + + it('空文本由规范化拦下,不落到发送层', async () => { + const gateway = createGateway() + const result = await manualAction(gateway, { + purpose: 'manual_text', + content: { type: 'text', text: ' ' } + }) + + expect(result.status).toBe('blocked') + expect(result.errorCode).toBe('INVALID_REQUEST') + expect(mocks.sender.send).not.toHaveBeenCalled() + }) + }) + + describe('toPersonalWechatSendResult', () => { + const fallback = { canSend: true } as unknown as Parameters< + typeof toPersonalWechatSendResult + >[1] + + it('优先返回底层 sendResult(它带着真实的 status)', () => { + const underlying = { success: true, status: { canSend: true, marker: 'real' } } + expect( + toPersonalWechatSendResult( + { + actionId: 'a1', + status: 'sent', + decision: 'allow', + startedAt: '2026-09-21T01:00:00.000Z', + finishedAt: '2026-09-21T01:00:01.000Z', + sendResult: underlying + }, + fallback + ) + ).toBe(underlying) + }) + + it('拿不到 sendResult 时按 action.status 合成', () => { + const sent = toPersonalWechatSendResult( + { + actionId: 'a1', + status: 'sent', + decision: 'allow', + startedAt: '2026-09-21T01:00:00.000Z', + finishedAt: '2026-09-21T01:00:01.000Z' + }, + fallback + ) + expect(sent).toEqual({ success: true, status: fallback }) + + const blocked = toPersonalWechatSendResult( + { + actionId: 'a2', + status: 'blocked', + decision: 'block', + errorCode: 'ACTION_NOT_ALLOWED', + reason: '自动化动作不允许执行', + startedAt: '2026-09-21T01:00:00.000Z', + finishedAt: '2026-09-21T01:00:01.000Z' + }, + fallback + ) + expect(blocked.success).toBe(false) + expect(blocked.error).toBe('自动化动作不允许执行') + }) + }) })