Files
TencentDB-Agent-Memory/scripts/openclaw-after-tool-call-messages.patch.sh
T

318 lines
15 KiB
Bash
Raw Normal View History

2026-05-29 17:33:12 +08:00
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════════
# OpenClaw Patch: after_tool_call hook 注入 session messages
# ═══════════════════════════════════════════════════════════════════
# 用途:在 after_tool_call hookEvent 中注入 ctx.params.session?.messages
# 使 context-offload 等插件能在工具调用后访问完整历史消息列表。
#
# 兼容策略(按优先级尝试,首个成功即停止):
# 策略 1: AST-like — 搜索 hookEvent 对象中 durationMs 字段,在其后追加 messages
# 策略 2: 旧版 dispatch-*.js — 针对早期版本文件布局
# 策略 3: runAfterToolCall 锚点 — 在 hookEvent 关闭大括号前插入
# 策略 4: 通用 fallback — 基于 after_tool_call + durationMs 的宽松匹配
#
# 用法:
# bash openclaw-after-tool-call-messages.patch.sh
# bash openclaw-after-tool-call-messages.patch.sh /custom/path/to/openclaw
#
# 幂等:已 patch 过的文件会自动跳过,可安全重复执行。
# ═══════════════════════════════════════════════════════════════════
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
fail() { echo -e "${RED}[FAIL]${NC} $*"; exit 1; }
debug() { [[ "${DEBUG:-}" == "1" ]] && echo -e "${CYAN}[DEBUG]${NC} $*" || true; }
# ─── 定位 OpenClaw 安装目录 ──────────────────────────────────────
# Uses Node.js require.resolve to locate the package root — handles
# nvm, pnpm, npm, yarn, volta, and any other layout automatically.
_node_resolve_openclaw() {
node -e "
const {dirname, join} = require('path');
const {realpathSync, existsSync, readFileSync, statSync} = require('fs');
// Helper: walk up from a file/dir to find the openclaw package root
function walkUp(start) {
let dir = statSync(start).isDirectory() ? start : dirname(start);
for (let i = 0; i < 10; i++) {
const pj = join(dir, 'package.json');
if (existsSync(pj)) {
try {
const pkg = JSON.parse(readFileSync(pj, 'utf8'));
if (pkg.name === 'openclaw') return dir;
} catch {}
}
const parent = dirname(dir);
if (parent === dir) break;
dir = parent;
}
return null;
}
// Strategy 1: which openclaw → realpath → walk up
try {
const {execSync} = require('child_process');
const bin = execSync('which openclaw', {encoding:'utf8'}).trim();
const real = realpathSync(bin);
const found = walkUp(real);
if (found) { console.log(found); process.exit(0); }
// pnpm uses shell shims: the bin file is a script, not a symlink.
// Read the shim content to extract the real entry point path.
const content = readFileSync(bin, 'utf8');
// pnpm shim contains a line like: exec node \"/path/.../openclaw/dist/cli.js\"
// or: require(\"/path/.../openclaw/dist/cli.js\")
const m = content.match(/['\"]([^'\"]*openclaw[^'\"]*\\.(?:js|mjs))['\"]/) ||
content.match(/['\"]([^'\"]*openclaw[^'\"]*)['\"].*node/);
if (m) {
const shimTarget = realpathSync(m[1]);
const found2 = walkUp(shimTarget);
if (found2) { console.log(found2); process.exit(0); }
}
} catch {}
// Strategy 2: search common pnpm/npm global paths
const {execSync: exec2} = require('child_process');
const searchDirs = [
join(process.env.HOME || '/root', '.local/share/pnpm'),
join(process.env.HOME || '/root', '.local/node/lib/node_modules'),
'/usr/local/lib/node_modules',
'/usr/lib/node_modules',
];
for (const base of searchDirs) {
if (!existsSync(base)) continue;
try {
const out = exec2(
'find ' + JSON.stringify(base) + ' -maxdepth 8 -name package.json -path \"*/openclaw/package.json\" 2>/dev/null',
{encoding:'utf8', timeout: 5000}
).trim();
for (const line of out.split('\\n')) {
if (!line) continue;
try {
const pkg = JSON.parse(readFileSync(line, 'utf8'));
if (pkg.name === 'openclaw') { console.log(dirname(line)); process.exit(0); }
} catch {}
}
} catch {}
}
process.exit(1);
" 2>/dev/null
}
if [[ -n "${1:-}" ]]; then
OPENCLAW_DIR="$1"
elif OPENCLAW_DIR="$(_node_resolve_openclaw)"; then
debug "Node.js resolved openclaw → $OPENCLAW_DIR"
else
fail "找不到 OpenClaw 安装目录。请手动指定:\n bash $0 /path/to/openclaw"
fi
DIST_DIR="$OPENCLAW_DIR/dist"
if [[ ! -d "$DIST_DIR" ]]; then
fail "dist 目录不存在: $DIST_DIR"
fi
info "OpenClaw 目录: $OPENCLAW_DIR"
# ─── 检测 OpenClaw 版本 ──────────────────────────────────────────
VERSION=$(grep -oP '"version"\s*:\s*"\K[^"]+' "$OPENCLAW_DIR/package.json" 2>/dev/null || echo "unknown")
info "检测到 OpenClaw 版本: $VERSION"
# ─── 已 patch 检测 ───────────────────────────────────────────────
# 核心标记:hookEvent 内部 durationMs 之后紧跟 messages 注入
# 支持多种缩进格式(tab / 空格 / 混合)
INJECTION_CODE='messages: ctx.params.session?.messages'
INJECTION_CODE_ALT='messages:ctx.params.session?.messages'
is_already_patched() {
local f="$1"
# 方法 1: 精确检测 — durationMs 后面紧跟 messages 注入(允许任意空白)
if perl -0777 -ne 'exit(0) if /durationMs[,\s]*\n\s*messages\s*:\s*ctx\.params\.session\?\s*\.messages/; exit(1)' "$f" 2>/dev/null; then
return 0
fi
# 方法 2: 上下文检测 — after_tool_call hookEvent 对象内部(durationMs 附近)有 messages 注入
# 注意: 不能用全文件检测,因为 before_compaction 也有 messages: ctx.params.session.messages
if perl -0777 -ne 'exit(0) if /(?:hookEvent|hook_event)\s*=\s*\{[\s\S]{0,500}durationMs[\s\S]{0,100}messages\s*:\s*ctx\.params\.session/; exit(1)' "$f" 2>/dev/null; then
return 0
fi
return 1
}
verify_patch() {
local f="$1"
is_already_patched "$f"
}
# ─── 备份工具 ────────────────────────────────────────────────────
backup_file() {
local f="$1"
local bak="${f}.pre-offload-patch.bak"
if [[ ! -f "$bak" ]]; then
cp "$f" "$bak"
debug "备份: $bak"
fi
}
# ─── 查找所有候选文件 ────────────────────────────────────────────
# 收集所有包含 after_tool_call 的 JS 文件(不限子目录深度)
mapfile -t CANDIDATE_FILES < <(grep -rl 'after_tool_call' "$DIST_DIR" --include='*.js' 2>/dev/null || true)
if [[ ${#CANDIDATE_FILES[@]} -eq 0 ]]; then
warn "在 $DIST_DIR 下未找到包含 after_tool_call 的 JS 文件"
fi
info "找到 ${#CANDIDATE_FILES[@]} 个候选文件"
PATCHED=0
SKIPPED=0
FAILED=0
# ─── 对每个候选文件尝试多种策略 ──────────────────────────────────
for f in "${CANDIDATE_FILES[@]}"; do
fname="$(basename "$f")"
relpath="${f#$DIST_DIR/}"
# 已 patch → 跳过
if is_already_patched "$f"; then
warn "$relpath — 已经 patch 过,跳过"
((SKIPPED++)) || true
continue
fi
# 确认文件中有 durationMshookEvent 的标志字段)
if ! grep -q 'durationMs' "$f" 2>/dev/null; then
debug "$relpath — 不含 durationMs,非 patch 目标,跳过"
continue
fi
# 确认 durationMs 附近有 after_tool_call 上下文(避免误匹配 before_compaction 等)
if ! perl -0777 -ne 'exit(0) if /after_tool_call[\s\S]{0,2000}durationMs/; exit(1)' "$f" 2>/dev/null; then
debug "$relpath — durationMs 不在 after_tool_call 上下文中,跳过"
continue
fi
backup_file "$f"
applied=false
# ── 策略 1: hookEvent 对象中 durationMs 是最后一个字段 ────────
# 匹配: durationMs<换行><空白>};<换行><空白>hookRunnerAfter
# 或: durationMs<换行><空白>};<换行><空白>await ...hookRunner...afterToolCall
# 缩进用 \s+ 宽松匹配
if [[ "$applied" == "false" ]]; then
if perl -0777 -ne 'exit(0) if /durationMs\s*\n(\s*)\};\s*\n\s*(hookRunnerAfter|await\s+\S*hookRunner\S*\.runAfterToolCall|hookRunner\S*\.runAfterToolCall)/; exit(1)' "$f" 2>/dev/null; then
debug "$relpath — 命中策略1 (hookRunnerAfter 锚点)"
perl -0777 -i -pe 's/(durationMs)\s*\n(\s*\};\s*\n\s*(?:hookRunnerAfter|await\s+\S*hookRunner\S*\.runAfterToolCall|hookRunner\S*\.runAfterToolCall))/$1,\n\t\t\tmessages: ctx.params.session?.messages\n$2/' "$f"
if verify_patch "$f"; then
ok "[策略1] $relpath — patch 成功"
((PATCHED++)) || true
applied=true
fi
fi
fi
# ── 策略 2: 旧版 dispatch-*.js — durationMs 行末独占 ─────────
if [[ "$applied" == "false" ]]; then
if echo "$relpath" | grep -qP 'dispatch-.*\.js' 2>/dev/null; then
# 匹配行末独占的 durationMs(前面是空白)
if grep -qP '^\s+durationMs\s*$' "$f" 2>/dev/null; then
debug "$relpath — 命中策略2 (旧版 dispatch)"
sed -i -E 's/^(\s+)(durationMs)\s*$/\1\2,\n\1messages: ctx.params.session?.messages/' "$f"
if verify_patch "$f"; then
ok "[策略2] $relpath — patch 成功"
((PATCHED++)) || true
applied=true
fi
fi
fi
fi
# ── 策略 3: durationMs 后跟 }; 但无 hookRunnerAfter 锚点 ────
# 匹配: durationMs<换行><空白>}; hookEvent 闭合)
# 通过上下文(附近有 after_tool_call)确认是正确的对象
if [[ "$applied" == "false" ]]; then
# 找到包含 durationMs 且附近(±20行)有 after_tool_call 的代码区域
if perl -0777 -ne 'exit(0) if /after_tool_call[\s\S]{0,800}durationMs\s*\n(\s*)\};/; exit(1)' "$f" 2>/dev/null; then
debug "$relpath — 命中策略3 (durationMs→}; 邻近 after_tool_call)"
# 只替换 after_tool_call 上下文附近的 durationMs → };
perl -0777 -i -pe 's/(after_tool_call[\s\S]{0,800}durationMs)\s*\n(\s*\};)/$1,\n\t\t\tmessages: ctx.params.session?.messages\n$2/' "$f"
if verify_patch "$f"; then
ok "[策略3] $relpath — patch 成功"
((PATCHED++)) || true
applied=true
fi
fi
fi
# ── 策略 4: 通用 fallback — hookEvent 赋值中的 durationMs ────
# 匹配形如: const hookEvent = { ... durationMs ... }
# 或: hookEvent = { ... durationMs ... }
# 用 perl 找到包含 after_tool_call 和 durationMs 的对象字面量,在 durationMs 后插入
if [[ "$applied" == "false" ]]; then
# 极宽松: 在文件中找到 "durationMs" 后面最近的 "}" 或 "};" ,在中间插入
# 但限制在 after_tool_call 关键字附近 2000 字符内
if perl -0777 -ne 'exit(0) if /after_tool_call[\s\S]{0,2000}?(?:hookEvent|hook_event)[\s\S]{0,500}?durationMs/; exit(1)' "$f" 2>/dev/null; then
debug "$relpath — 命中策略4 (通用 fallback)"
# 在 durationMs 后追加 (仅首次匹配)
perl -0777 -i -pe '
my $done = 0;
s/(after_tool_call[\s\S]{0,2000}?(?:hookEvent|hook_event)[\s\S]{0,500}?durationMs)\s*\n(\s*)(\};)/
if (!$done) { $done = 1; "$1,\n$2\tmessages: ctx.params.session?.messages\n$2$3" }
else { "$1\n$2$3" }
/ge;
' "$f"
if verify_patch "$f"; then
ok "[策略4] $relpath — patch 成功"
((PATCHED++)) || true
applied=true
else
warn "[策略4] $relpath — patch 后验证失败,恢复备份"
cp "${f}.pre-offload-patch.bak" "$f"
fi
fi
fi
# ── 无策略命中 ───────────────────────────────────────────────
if [[ "$applied" == "false" ]]; then
debug "$relpath — 无策略命中"
((FAILED++)) || true
fi
done
# ─── 结果报告 ────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} Patch 完成 (OpenClaw $VERSION)${NC}"
echo -e "${GREEN}═══════════════════════════════════════════════════════${NC}"
echo ""
echo -e " 成功: ${GREEN}${PATCHED}${NC} 跳过: ${YELLOW}${SKIPPED}${NC} 失败: ${RED}${FAILED}${NC}"
echo ""
if [[ $PATCHED -gt 0 ]]; then
echo -e " ${CYAN}重启 OpenClaw 后生效。${NC}"
echo -e " ${CYAN}备份文件: *.pre-offload-patch.bak${NC}"
elif [[ $SKIPPED -gt 0 && $FAILED -eq 0 ]]; then
echo -e " ${YELLOW}所有目标文件已 patch,无需重复操作。${NC}"
elif [[ $FAILED -gt 0 ]]; then
echo -e " ${RED}部分文件未能 patch。可能需要手动检查或更新 patch 脚本。${NC}"
echo -e " ${RED}提示:设置 DEBUG=1 运行以查看详细匹配过程:${NC}"
echo -e " ${RED} DEBUG=1 bash $0 $OPENCLAW_DIR${NC}"
else
echo -e " ${RED}未找到匹配的目标文件,请确认 OpenClaw 版本。${NC}"
echo -e " ${RED}提示:设置 DEBUG=1 运行以查看详细匹配过程:${NC}"
echo -e " ${RED} DEBUG=1 bash $0 $OPENCLAW_DIR${NC}"
fi
echo ""
# ─── 退出码 ───────────────────────────────────────────────────────
# 0: 成功(至少有一个文件 patch 成功或已跳过)
# 1: 失败(无任何 patch 成功且无已跳过的文件)
if [[ $PATCHED -gt 0 || $SKIPPED -gt 0 ]]; then
exit 0
else
exit 1
fi