Files
openclaw-config/extensions/openclaw-lark/src/channel/abort-detect.js
T

217 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
/**
* Copyright (c) 2026 ByteDance Ltd. and/or its affiliates
* SPDX-License-Identifier: MIT
*
* Abort trigger detection for the Lark/Feishu channel plugin.
*
* Provides a fast-path check to determine whether an inbound message is
* an abort/stop command *before* it enters the per-chat serial queue.
*
* The trigger word list and normalisation logic are copied from the
* OpenClaw core (`src/auto-reply/reply/abort.ts`) so the plugin can
* make a lightweight decision without importing the full reply pipeline.
* The message still flows through `tryFastAbortFromMessage()` for
* authoritative handling.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.isAbortTrigger = isAbortTrigger;
exports.isLikelyAbortText = isLikelyAbortText;
exports.isConversationStopIntent = isConversationStopIntent;
exports.extractRawTextFromEvent = extractRawTextFromEvent;
// ---------------------------------------------------------------------------
// Trigger word list (synced with OpenClaw core abort.ts)
// ---------------------------------------------------------------------------
const ABORT_TRIGGERS = new Set([
'stop',
'esc',
'abort',
'wait',
'exit',
'interrupt',
'detente',
'deten',
'detén',
'arrete',
'arrête',
'停止',
'やめて',
'止めて',
'रुको',
'توقف',
'стоп',
'остановись',
'останови',
'остановить',
'прекрати',
'halt',
'anhalten',
'aufhören',
'hoer auf',
'stopp',
'pare',
'stop openclaw',
'openclaw stop',
'stop action',
'stop current action',
'stop run',
'stop current run',
'stop agent',
'stop the agent',
"stop don't do anything",
'stop dont do anything',
'stop do not do anything',
'stop doing anything',
'do not do that',
'please stop',
'stop please',
]);
// ---------------------------------------------------------------------------
// Normalisation helpers
// ---------------------------------------------------------------------------
const TRAILING_ABORT_PUNCTUATION_RE = /[.!?…,,。;:'"'")\]}]+$/u;
function normalizeAbortTriggerText(text) {
return text
.trim()
.toLowerCase()
.replace(/['`]/g, "'")
.replace(/\s+/g, ' ')
.replace(TRAILING_ABORT_PUNCTUATION_RE, '')
.trim();
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/** Exact trigger-word match (same logic as OpenClaw core `isAbortTrigger`). */
function isAbortTrigger(text) {
if (!text)
return false;
const normalized = normalizeAbortTriggerText(text);
return ABORT_TRIGGERS.has(normalized);
}
/**
* Extended abort detection: matches both bare trigger words and the
* `/stop` command form. Used by the monitor fast-path.
*/
function isLikelyAbortText(text) {
if (!text)
return false;
const trimmed = text.trim().toLowerCase();
if (trimmed === '/stop')
return true;
return isAbortTrigger(trimmed);
}
// ---------------------------------------------------------------------------
// Conversation stop-intent (broader than the exact abort triggers)
// ---------------------------------------------------------------------------
/**
* Conversational "please stop / interrupt this exchange" phrases.
*
* Deliberately SEPARATE from {@link ABORT_TRIGGERS} (which is synced word-for-
* word with OpenClaw core and matched by exact equality, e.g. `/stop`). These
* are matched by substring so natural phrasings like "中断对话" or "stop
* talking" are caught. The list is intentionally distinctive to avoid false
* positives — a false positive only means we skip the deterministic peer-@
* backstop for that turn (the model can still @ on its own), which is mild.
*/
const STOP_INTENT_PHRASES = [
// zh — stop / terminate / pause
'中断',
'中止',
'终止',
'停止',
'停下',
'停一下',
'暂停',
'打住',
'停手',
'收手',
// zh — "don't keep going / replying"
'别聊',
'别说了',
'别回复',
'别继续',
'别再聊',
'别再说',
'别吵',
'别争',
'不要回复',
'不要继续',
'不用回复',
'不用继续',
// zh — "wrap up / be quiet"
'结束对话',
'结束讨论',
'结束辩论',
'到此为止',
'闭嘴',
// en
'stop talking',
'stop chatting',
'stop debating',
'stop the debate',
'stop the conversation',
'stop this conversation',
'stop responding',
'stop replying',
'end the conversation',
'end conversation',
'end the debate',
'shut up',
'be quiet',
'cut it out',
'knock it off',
'wrap it up',
'stand down',
];
/**
* Whether an inbound message expresses intent to stop / interrupt the ongoing
* (bot-to-bot) exchange. Superset of {@link isLikelyAbortText} plus the
* conversational phrases above.
*
* Two consumers: (1) suppress the deterministic peer-@ backstop so a stop
* acknowledgement doesn't re-wake the peer bot; (2) mute an active bot loop so
* the in-flight ping-pong drains instead of being re-armed. Substring match —
* keep the list distinctive (no bare "停"/"stop") to limit false positives;
* the worst case is a missed forced-@ or a self-healing mute (any normal
* message lifts it).
*/
function isConversationStopIntent(text) {
if (!text)
return false;
// Drop bot mention placeholders so "@Bot 中断对话" → "中断对话".
const normalized = text.replace(/@_user_\d+/g, '').trim().toLowerCase();
if (!normalized)
return false;
if (isLikelyAbortText(normalized))
return true;
return STOP_INTENT_PHRASES.some((p) => normalized.includes(p));
}
/**
* Extract the raw text payload from a Feishu message event.
*
* Only handles `text` type messages. The `message.content` field is a
* JSON string like `{"text":"hello"}`. Returns `undefined` for
* non-text messages or parse failures.
*
* In group chats, bot mention placeholders (`@_user_N`) are stripped so
* a message like `@Bot stop` is detected as `stop`.
*/
function extractRawTextFromEvent(event) {
if (!event.message || event.message.message_type !== 'text') {
return undefined;
}
try {
const parsed = JSON.parse(event.message.content);
let text = parsed?.text;
if (typeof text !== 'string')
return undefined;
// Strip bot mention placeholders (@_user_1, @_user_2, etc.)
text = text.replace(/@_user_\d+/g, '').trim();
return text || undefined;
}
catch {
return undefined;
}
}