mirror of
https://github.com/TencentCloud/TencentDB-Agent-Memory
synced 2026-07-10 20:34:30 +00:00
318 lines
15 KiB
Bash
318 lines
15 KiB
Bash
#!/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
|
||
|
||
# 确认文件中有 durationMs(hookEvent 的标志字段)
|
||
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
|