Files
reply-end-controls/scripts/apply-openclaw-poc-patch.sh

121 lines
6.1 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
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"
CONFIG_PATH="$(dirname "$0")/../config/reply-end-controls.json"
PATCH_CONTRACT_PATH="$(dirname "$0")/../generated/openclaw-telegram-patch-contract.json"
mkdir -p "$(dirname "${PATCH_CONTRACT_PATH}")"
npx -p tsx@4.20.6 -p typescript@5.9.3 tsx "$(dirname "$0")/render-openclaw-patch-contract.mjs" > "${PATCH_CONTRACT_PATH}"
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}" "${PATCH_CONTRACT_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])
patch_contract_path = Path(sys.argv[4])
cfg = json.loads(config_path.read_text(encoding='utf-8'))
contract = json.loads(patch_contract_path.read_text(encoding='utf-8'))
cb_continue = cfg['callbacks']['continue']
cb_stop = cfg['callbacks']['stop']
label_continue = contract['defaultButtons'][0][0]['text']
label_stop = contract['defaultButtons'][0][1]['text']
resolved_continue = contract['resolved']['continue']['buttons'][0][0]['text']
resolved_stop = contract['resolved']['stop']['buttons'][0][1]['text']
ack_continue = contract['resolved']['continue']['acknowledgement']
ack_stop = contract['resolved']['stop']['acknowledgement']
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 || "<empty>"}\n`); } catch {}
try { fsSync.appendFileSync(replyEndDebugPath, `[raw] ${new Date().toISOString()} data=${data || "<empty>"}\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 ?? "<media:document>";'
new_stop = f'''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{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}"