feat: implement apply and rollback patch scripts / 實作 apply 與 rollback patch 腳本

This commit is contained in:
Alice (OpenClaw)
2026-05-13 12:41:44 +08:00
parent ae096bcbc2
commit e610f5f930
2 changed files with 115 additions and 16 deletions

View File

@@ -1,11 +1,99 @@
#!/usr/bin/env bash
set -euo pipefail
echo "reply-end-controls PoC patch applicator"
echo
echo "This is a skeleton script."
echo "Its intended future role is to apply the currently documented OpenClaw runtime patch set to a target test environment."
echo
echo "Before implementing this script fully, consult:"
echo "- reports/openclaw-patch-inventory.md"
echo "- telegram-openclaw-poc-plan.md"
if [[ $# -lt 1 ]]; then
echo "usage: $0 <openclaw-dist-dir>" >&2
exit 1
fi
DIST_DIR="$1"
SEND_JS="${DIST_DIR}/send-sxDwUGaO.js"
BOT_JS="${DIST_DIR}/bot-Ce301bOE.js"
if [[ ! -f "${SEND_JS}" || ! -f "${BOT_JS}" ]]; then
echo "expected runtime files not found under: ${DIST_DIR}" >&2
exit 1
fi
cp "${SEND_JS}" "${SEND_JS}.reply-end-controls.bak"
cp "${BOT_JS}" "${BOT_JS}.reply-end-controls.bak"
python3 - <<'PY' "${SEND_JS}" "${BOT_JS}"
from pathlib import Path
import sys
send_js = Path(sys.argv[1])
bot_js = Path(sys.argv[2])
send_text = send_js.read_text(encoding='utf-8')
send_old = 'const replyMarkup = buildInlineKeyboard(opts.buttons);'
send_new = 'const replyMarkup = buildInlineKeyboard(opts.buttons ?? [[{ text: "A. 繼續", callback_data: "rec:continue" }, { text: "B. 就這樣吧,不需要額外處理", callback_data: "rec:stop" }]]);'
if send_old in send_text and send_new not in send_text:
send_text = send_text.replace(send_old, send_new, 1)
send_js.write_text(send_text, encoding='utf-8')
bot_text = bot_js.read_text(encoding='utf-8')
if 'import fsSync from "node:fs";' not in bot_text:
bot_text = 'import fsSync from "node:fs";\n' + bot_text
old_raw = 'const data = (callback.data ?? "").trim();'
new_raw = '''const data = (callback.data ?? "").trim();
const replyEndDebugPath = path.join(process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || "", ".openclaw"), "reply-end-debug.log");
try { fsSync.appendFileSync(replyEndDebugPath, `[raw] ${new Date().toISOString()} data=${data || "<empty>"}\n`); } catch {}'''
if old_raw in bot_text and 'reply-end-debug.log' not in bot_text:
bot_text = bot_text.replace(old_raw, new_raw, 1)
old_click = '''const chatId = callbackMessage.chat.id;
const isGroup = callbackMessage.chat.type === "group" || callbackMessage.chat.type === "supergroup";'''
new_click = '''const chatId = callbackMessage.chat.id;
const replyEndMatch = /^(rec):(continue|stop)$/.exec(data);
if (replyEndMatch) {
const choice = replyEndMatch[2];
const state = {
lastChoice: choice,
lastChoiceAt: new Date().toISOString(),
sourceMessageId: String(callbackMessage.message_id),
sourceCallbackId: String(callback.id),
active: true
};
globalThis.__replyEndControlsState ??= new Map();
globalThis.__replyEndControlsState.set(String(chatId), state);
try {
const replyEndPath = path.join(process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || "", ".openclaw"), "reply-end-controls.json");
fsSync.writeFileSync(replyEndPath, JSON.stringify(Object.fromEntries(globalThis.__replyEndControlsState), null, 2));
fsSync.appendFileSync(replyEndDebugPath, `[state] ${new Date().toISOString()} choice=${choice} stateWriteOk=true\n`);
} catch {}
const resolvedButtons = choice === "continue" ? [[{ text: "✅ A. 繼續", callback_data: "rec:continue" }, { text: "B. 就這樣吧,不需要額外處理", callback_data: "rec:stop" }]] : [[{ text: "A. 繼續", callback_data: "rec:continue" }, { text: "⏹ B. 就這樣吧,不需要額外處理", callback_data: "rec:stop" }]];
try {
await editCallbackButtons(resolvedButtons);
fsSync.appendFileSync(replyEndDebugPath, `[edit] ${new Date().toISOString()} choice=${choice} editOk=true\n`);
} catch {}
try {
await replyToCallbackChat(choice === "continue" ? "reply-end-controls: continue received" : "reply-end-controls: stop received");
fsSync.appendFileSync(replyEndDebugPath, `[ack] ${new Date().toISOString()} choice=${choice} ackOk=true\n`);
} catch {}
return;
}
const isGroup = callbackMessage.chat.type === "group" || callbackMessage.chat.type === "supergroup";'''
if old_click in bot_text and 'reply-end-controls: continue received' not in bot_text:
bot_text = bot_text.replace(old_click, new_click, 1)
old_stop = 'else bodyText = savedMediaPlaceholder ?? "<media:document>";'
new_stop = '''else bodyText = savedMediaPlaceholder ?? "<media:document>";
try {
const replyEndPath = path.join(process.env.OPENCLAW_STATE_DIR || path.join(process.env.HOME || "", ".openclaw"), "reply-end-controls.json");
if (fsSync.existsSync(replyEndPath)) {
const rawState = JSON.parse(fsSync.readFileSync(replyEndPath, "utf-8"));
const replyEndState = rawState?.[String(chatId)];
if (replyEndState?.lastChoice === "stop" && replyEndState?.active === true) {
bodyText = `${bodyText}\n\n[System: The user previously pressed reply-end-controls Stop for this conversation. Treat the previous thread as closed. Unless the user's new message contains a clear new instruction or explicit request for more work, you must NOT proactively continue, must NOT add extra checks, must NOT propose next steps, must NOT extend the task, and should answer briefly and close cleanly.]`.trim();
}
}
} catch {}'''
if old_stop in bot_text and 'Treat the previous thread as closed' not in bot_text:
bot_text = bot_text.replace(old_stop, new_stop, 1)
bot_js.write_text(bot_text, encoding='utf-8')
PY
echo "reply-end-controls PoC patch applied to ${DIST_DIR}"