#!/usr/bin/env bash set -euo pipefail if [[ $# -lt 1 ]]; then echo "usage: $0 " >&2 exit 1 fi DIST_DIR="$1" SEND_JS="${DIST_DIR}/send-sxDwUGaO.js" BOT_JS="${DIST_DIR}/bot-Ce301bOE.js" CONFIG_PATH="$(dirname "$0")/../config/reply-end-controls.json" 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}" "${CONFIG_PATH}" from pathlib import Path import json import sys send_js = Path(sys.argv[1]) bot_js = Path(sys.argv[2]) config_path = Path(sys.argv[3]) cfg = json.loads(config_path.read_text(encoding='utf-8')) cb_continue = cfg['callbacks']['continue'] cb_stop = cfg['callbacks']['stop'] label_continue = cfg['labels']['continue'] label_stop = cfg['labels']['stop'] resolved_continue = cfg['resolvedLabels']['continue'] resolved_stop = cfg['resolvedLabels']['stop'] ack_continue = cfg['ack']['continue'] ack_stop = cfg['ack']['stop'] stop_policy = cfg['stopPolicyText'] send_text = send_js.read_text(encoding='utf-8') send_old = 'const replyMarkup = buildInlineKeyboard(opts.buttons);' send_new = f'const replyMarkup = buildInlineKeyboard(opts.buttons ?? [[{{ text: {json.dumps(label_continue, ensure_ascii=False)}, callback_data: {json.dumps(cb_continue)} }}, {{ text: {json.dumps(label_stop, ensure_ascii=False)}, callback_data: {json.dumps(cb_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, `[pre-answer] ${new Date().toISOString()} data=${data || ""}\n`); } catch {} try { fsSync.appendFileSync(replyEndDebugPath, `[raw] ${new Date().toISOString()} data=${data || ""}\n`); } catch {}''' if old_raw in bot_text and '[pre-answer]' 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 = f'''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: {json.dumps(resolved_continue, ensure_ascii=False)}, callback_data: {json.dumps(cb_continue)} }} , {{ text: {json.dumps(label_stop, ensure_ascii=False)}, callback_data: {json.dumps(cb_stop)} }}]] : [[{{ text: {json.dumps(label_continue, ensure_ascii=False)}, callback_data: {json.dumps(cb_continue)} }}, {{ text: {json.dumps(resolved_stop, ensure_ascii=False)}, callback_data: {json.dumps(cb_stop)} }}]]; try {{ await editCallbackButtons(resolvedButtons); fsSync.appendFileSync(replyEndDebugPath, `[edit] ${{new Date().toISOString()}} choice=${{choice}} editOk=true\n`); }} catch {{}} try {{ await replyToCallbackChat(choice === "continue" ? {json.dumps(ack_continue, ensure_ascii=False)} : {json.dumps(ack_stop, ensure_ascii=False)}); 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 ?? "";' new_stop = f'''else bodyText = savedMediaPlaceholder ?? ""; 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{stop_policy}`.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}"