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

318 lines
15 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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