121 lines
6.1 KiB
Bash
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}"
|