OpenViking ZCode 记忆插件:为 ZCode 接入长期记忆生命周期的薄适配层实战指南
【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenViking
本篇技术指南围绕 OpenViking 为 ZCode(基于 Claude Code 配置格式的 AI 编程 Agent)提供的官方长期记忆插件展开,讲解其如何通过复用memory-plugin-shared共享运行时,以四个 Hook 事件完成用户画像注入、记忆召回、viking://虚拟路径拦截与增量会话捕获。读完本文,你将掌握该插件的安装方式、事件调度与 rollout 文件机制、严格 JSON 输出契约以及回归测试方法,并能据此理解 ZCode 扩展面与 OpenViking 记忆服务之间的完整调用链。
插件定位:只做薄适配,不重复记忆逻辑
ZCode 记忆插件(examples/zcode-memory-plugin/README_CN.md)的核心理念是:复用memory-plugin-shared共享运行时,不重复任何记忆逻辑,仅新增一个 ZCode 薄适配层。
也就是说,召回(recall)、批量写入(batch send)、待处理队列(pending queue)、凭据解析(credentials)与 MCP 代理等记忆能力全部来自共享库examples/memory-plugin-shared/lib/,本插件只负责两件事:
- 把 ZCode 的 Hook 事件翻译成 OpenViking 记忆服务能理解的动作;
- 处理 ZCode 特有的确认(acknowledgement)与游标(cursor)状态转换。
插件的集成清单文件 openviking.integration.json 声明了其身份与能力边界:
{ "schemaVersion": 1, "id": "openviking-memory", "version": "0.1.2", "clients": ["zcode"], "capabilities": ["hooks", "mcp"] }功能概览:四个 Hook 事件完成全生命周期接入
插件围绕 ZCode 实际支持的 7 个 Hook 事件,选择了其中 4 个可用的子集进行接线,其功能对照如下:
| Hook 事件 | 触发时机 | 插件行为 |
|---|---|---|
SessionStart | 会话启动 | 注入用户画像与偏好/实体到上下文,并重放待处理队列(replay pending) |
UserPromptSubmit | 用户提交提示词 | 搜索 OpenViking 相关记忆并注入上下文,带去重防抖 |
PreToolUse(Read\|Glob\|Grep) | 工具调用前 | 拦截viking://虚拟路径的直接访问,引导 Agent 使用 MCP 工具 |
Stop | 回合结束 | 立即返回,在 detached worker 中捕获增量用户/助手对话并提交 OpenViking 会话 |
之所以只接 4 个事件,是因为 DESIGN.md 中记录的已验证事实显示:ZCode不支持PreCompact、SessionEnd、Notification、SubagentStart、SubagentStop这 5 个事件。因此插件通过Stop 时 commit来补足 compact/会话结束信号,这是整个捕获链路设计的出发点。
安装:一行命令接入 ZCode
安装使用共享安装脚本,指定目标 harness 为zcode:
bash examples/memory-plugin-shared/install.sh --harness zcode安装脚本(examples/memory-plugin-shared/install.sh)的 ZCode 分支完成以下工作:
- 检测 ZCode:通过
~/.zcode/目录或zcode二进制是否存在来判断(脚本内HAVE_ZCODE判定逻辑),并在未被显式指定时自动探测。 - 合并配置:将 hooks 配置与 MCP 配置合并写入
~/.zcode/cli/config.json。从源码看,安装会生成~/.zcode/hooks.json与 MCP 配置,再通过zcode_merge_config合并进用户级配置文件。 - 写入凭据:将 OpenViking 凭据写入
~/.openviking/ovcli.conf。 - 模板变量替换:源模板 hooks/hooks.json 中的
${ZCODE_PLUGIN_ROOT}在安装时被替换为绝对路径。这是关键设计:ZCode 的 config-file hooks不做模板展开,所以安装脚本必须在写入前完成路径渲染,规避该限制。 - 卸载清理:脚本同时提供卸载路径,会移除
~/.zcode/hooks.json、~/.zcode/mcp.json以及~/.openviking/agent-integrations/zcode目录。
安装完成后,可以查看~/.zcode/cli/config.json确认 hooks 与mcp.servers已就位。插件还支持在安装脚本中与其他 harness(claude,codex,cursor,trae,opencode,pi,dsh等)一起组合安装。
架构:Vendor 共享运行时 + 单入口调度器
插件的目录结构与职责划分如下:
examples/zcode-memory-plugin/ ├── hooks/hooks.json # Hook 配置模板(${ZCODE_PLUGIN_ROOT} 占位) ├── scripts/ │ ├── zcode-hook.mjs # 事件调度器(单一入口,按事件分支) │ ├── zcode-capture.mjs # ZCode 特有确认与游标状态转换 │ ├── zcode-turns.mjs # rollout 文件解析 / stdin 回退 │ ├── session-start.mjs # 三个轻量 shim + uri-guard 独立入口 │ ├── auto-recall.mjs │ ├── auto-capture.mjs │ ├── uri-guard.mjs # PreToolUse 独立入口 │ └── shared/ # vendor 进来的共享运行时(18 个 .mjs) └── servers/mcp-proxy.mjs # OpenViking MCP 代理关键设计一:Vendor 而非相对路径引用
与 TRAE/Cursor(通过跨目录相对路径 import 共享库)不同,ZCode 插件与 Claude Code、Codex 采用同一模式:通过sync.mjs将共享运行时 vendor 到scripts/shared/。这让插件完全自包含、可整体搬迁——因为 ZCode 的 config 驱动安装模型会把文件拷贝到~/.openviking/agent-integrations/,相对路径方案会在此场景下失效。
关键设计二:config-file hooks 而非 plugin-manifest hooks
插件把 hooks 与 MCP 配置写入~/.zcode/cli/config.json(config-file 作用域),而不是走插件市场注册。这与 Cursor/TRAE 的安装模式一致。需要特别注意的是:config-file hooks 要求hooks.enabled: true,合并脚本会自动设置该开关。
关键设计三:调度器按事件名分支
zcode-hook.mjs 是唯一的逻辑入口,它通过process.env.OPENVIKING_HOOK_EVENT(或第二个命令行参数)拿到事件名:
const eventName = process.env.OPENVIKING_HOOK_EVENT || process.argv[2] || ""; const cfg = loadAgentHookConfig("zcode"); const { log, logError } = createAgentLogger("zcode", eventName, cfg);三个 shim(session-start.mjs、auto-recall.mjs、auto-capture.mjs)都只有寥寥数行:设置OPENVIKING_HOOK_EVENT环境变量后动态 import 调度器。例如:
// scripts/auto-capture.mjs process.env.OPENVIKING_HOOK_EVENT = "stop"; await import("./zcode-hook.mjs");运行时还会做一次 session id 归一化——ZCode 可能以 camelCase 或 snake_case 传入sessionId,调度器会补齐input.session_id字段,避免同一目录下开两个窗口时因 cwd 回退导致的 session 冲突:
if (!input.session_id && input.sessionId) input.session_id = input.sessionId;严格 JSON 输出契约:只输出 ZCode 认可的键
这是本插件最值得注意的实现约束。ZCode 将 Hook 的 stdout 解析为严格 JSON——任何不被识别的多余键都会导致整个输出被静默丢弃。因此调度器绝不输出 Claude Code 风格的{ "decision": "approve" }字段,而是使用 ZCode 规范的两类输出:
上下文注入(SessionStart / UserPromptSubmit):
process.stdout.write( JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext, }, }) + "\n", );透传(无需输出时):不写 stdout、隐式 exit 0。
拦截拒绝(PreToolUse,见 uri-guard.mjs):
{ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: "…", } }这个契约被 DESIGN.md 标记为"第一大静默失败模式"(#1 silent-failure mode)——一旦混入多余键,功能看似正常实则完全不生效。
SessionStart:画像注入 + 待处理队列重放
调度器对session-start事件的处理包含 2000ms 节流(防止同一会话重复触发),并依次完成:
- 重放待处理队列(
replayAgentPending),把之前因网络失败排队未发送的消息补发出去; - 构建用户画像(
buildAgentProfile,从 OpenViking 读取用户偏好/实体); - 以
<openviking-context source="session-start">…</openviking-context>包裹注入上下文。
UserPromptSubmit:记忆召回 + 双重防抖
对user-prompt-submit事件,调度器先清洗 prompt 文本(剥离开插件注入的<openviking-context>、<relevant-memories>、<system-reminder>块),再做召回,并使用两种去重手段防止重复注入:
- 若 stdin 带
generation_id/request_id等事件 ID,直接比较事件 ID; - 否则对 prompt 做
stableHash并检查 500ms 内的重复提交。
召回结果同样缓存在 hook state 中(recallBlock),供 Stop 阶段回填pendingPrompt。
Stop 捕获机制:rollout 文件是权威增量对话源
ZCode 的 Stop hook stdin 载荷并未被完整文档化。根据 DESIGN.md 记录的反向工程结论(#3127),Stop 载荷中至少包含session_id、cwd、transcript_path(指向一个只含最后一条助手消息的临时文件)以及responseText/responsePreview。用户消息并不在 stdin 中。
因此 zcode-turns.mjs 采用双通道策略:
- rollout 文件优先(权威来源):ZCode 在
~/.zcode/cli/rollout/model-io-<sessionId>.jsonl存放完整对话,每行一条 JSON,结构为:{ "sessionId": "sess_…", "turnId": 123, "type": "model_io", "request": { "messages": [ { "role": "user", "content": "…" } ] }, "response": { "text": "…", "toolCalls": [], "finishReason": "…" } }解析器从
state.lastTurnId之后读取所有未见回合(extractUnseenRolloutTurns),首个捕获周期(无 lastTurnId)则读取全部条目,避免丢失历史。稳定的 hostturnId既用于去重,也让后续 Stop 能恢复漏掉的回合。 - stdin 回退:仅当 rollout 文件不可读时,才从
responseText/responsePreview/prompt等字段回退构造 user/assistant 回合,并结合pendingPrompt补全用户消息。
确认与游标:不丢消息的增量提交
zcode-capture.mjs 负责把解析出的回合转换为待发送载荷,并推进去重与游标状态:
export function zcodeTurnDedupKey(turn) { return turn.turnId ? `${turn.turnId}:${turn.role}` // 有 turnId 时:turnId + role 组合去重 : stableHash(turn.role, turn.content); }核心逻辑分三步:
- 过滤:用
shouldCaptureText判断每条回合是否值得捕获,生成{ dedupKey, turn, content }候选; - 去重:剔除已在
state.capturedTurnIds中的候选(该集合按确认结果滚动保留最近 1000 条); - 游标推进:只有某个 turnId 下所有回合都被确认(acknowledged)后,
lastTurnId才推进到该 turnId——这保证了即使部分消息发送失败,下次 Stop 也能从断点恢复,而不是跳号。
发送成功的判定基于响应中的sent + queued计数(applyZcodeCaptureResult中captured = min(toSend.length, sent + queued)),即消息已发出或已持久化排队才算确认。捕获到新消息后,调度器还会调用commitAgentSession提交会话,并把capturedSinceCommit归零。
Detached 写入:不阻塞 ZCode
Stop 事件在 zcode-hook.mjs 的入口处先尝试maybeDetach进入 detached worker:
if (eventName === "stop" && cfg.enabled && cfg.autoCapture) { const detached = await maybeDetach(cfg, { approve: () => {} }); if (detached) return; }这样网络写入在独立进程中执行,Hook 立即返回,ZCode 的会话流程不会被慢网络阻塞。任何未捕获异常都会走 pass-through 兜底(logError("uncaught", error)),绝不让 Hook 卡死会话。
URI Guard:拦截 viking:// 直接访问
uri-guard.mjs 独立处理PreToolUse事件,matcher 限定为Read|Glob|Grep(见 hooks/hooks.json):
"PreToolUse": [ { "matcher": "Read|Glob|Grep", "hooks": [ { "type": "command", "command": "node \"${ZCODE_PLUGIN_ROOT}/scripts/uri-guard.mjs\"", "timeout": 5 } ] } ]它从 stdin 中兼容读取tool_name/toolName/name/tool与tool_input/toolInput/input等字段(适配不同字段命名),交由共享运行时agent-uri-guard.mjs的evaluateAgentUriGuard判断是否为viking://URI。命中时返回上述 deny 输出,并附带引导使用 MCP 工具的原因说明;未命中则空输出透传。这样 Agent 不会绕过 MCP 直接以文件读写方式触碰viking://虚拟路径。
Hook 配置模板速查
源模板 hooks/hooks.json 完整定义了四个事件的接线,其中timeout单位为秒(command类型;若用process类型则对应timeoutMs毫秒):
| 事件 | 入口脚本 | timeout(秒) |
|---|---|---|
SessionStart | session-start.mjs | 30 |
UserPromptSubmit | auto-recall.mjs | 20 |
PreToolUse(Read|Glob|Grep) | uri-guard.mjs | 5 |
Stop | auto-capture.mjs | 30 |
MCP 侧,OpenViking 通过 servers/mcp-proxy.mjs 以用户作用域注册到~/.zcode/cli/config.json的mcp.servers,ZCode 会在会话启动时自动连接所有作用域的 MCP 服务器;工具名按plugin:<plugin>:<server>规则命名空间化。
测试:回归套件覆盖关键故障模式
插件自带聚焦回归测试,覆盖了 DESIGN.md 中对抗性评审(adversarial review)识别的全部高风险场景:
node --test scripts/*.test.mjs四个测试文件各司其职:
- zcode-hooks.test.mjs:事件调度与严格输出契约;
- zcode-turns.test.mjs:rollout 文件解析、首次捕获、断点恢复;
- zcode-capture.test.mjs:确认与游标状态转换、重复 Stop 投递去重;
- zcode-async.test.mjs:detached 慢写入不阻塞会话。
已验证的 ZCode 扩展面(事实清单)
下表是 DESIGN.md 基于真实 ZCode 安装(内置zcode-guide插件文档 + 实际~/.zcode/cli/config.json+ 真实安装的带 hooks 插件)验证的事实,可作为二次开发或排查问题的依据:
| 方面 | 已验证事实 |
|---|---|
| 支持的 Hook 事件 | SessionStart、UserPromptSubmit、PreToolUse、PermissionRequest、PostToolUse、PostToolUseFailure、Stop(恰好 7 个) |
| 不支持的事件 | PreCompact、SessionEnd、Notification、SubagentStart、SubagentStop |
| Manifest 探测顺序 | .zcode-plugin/plugin.json→.claude-plugin/plugin.json→.codex-plugin/plugin.json |
| 插件 Hook 模板变量 | ${CLAUDE_PLUGIN_ROOT}、${ZCODE_PLUGIN_ROOT}、${CLAUDE_PROJECT_DIR}、${ZCODE_PROJECT_DIR}、${CLAUDE_SESSION_ID} |
| config-file Hook 模板变量 | 无——config 文件中的 Hook 不做模板展开 |
| Hook 输出 schema | 严格 JSON——任何多余键都会校验失败、输出被丢弃 |
| MCP 配置位置 | ~/.zcode/cli/config.json→mcp.servers(用户作用域) |
| 插件 MCP 命名空间 | plugin:<plugin>:<server> |
| MCP 自动连接 | 会话启动时自动连接所有作用域 |
| Hook runner 启用 | 任一插件贡献 hook 时自动启用 |
| 超时单位 | command类型为秒;process类型timeoutMs为毫秒 |
async字段 | 无运行时效果——hooks 始终内联执行 |
已知未知项与使用注意事项
DESIGN.md 明确列出的"primary unknowns"(未知项)也值得了解,它们界定了本插件的边界:
- Hook stdin 字段名:Stop 载荷中用户消息字段名未文档化,已通过源码反向工程确认
responseText/responsePreview承载助手内容,用户内容依赖 rollout 文件; - 输出 schema 兼容性:
hookSpecificOutput包装层是否被 ZCode 原样接受,需在真实 ZCode 会话中验证; - MCP 工具名格式:
plugin:openviking:openviking的命名空间化工具名需与实际工具名匹配; - turn 身份:rollout 条目携带单调递增的
turnId,插件将其作为 OpenViking 的turn_id透传,仅在消息已发送或持久化排队后才记录去重键,且只通过完整确认的 rollout 条目推进lastTurnId。
实际使用中还需注意:本插件假设已有一台可访问的 OpenViking 服务,且~/.openviking/ovcli.conf中配置了正确的服务地址与凭据(由安装脚本写入)。若 Stop 事件从未触发(例如 Agent 被强制终止),增量对话的提交会被顺延到下一次 Stop 通过 rollout 文件恢复——这正是 rollout 优先设计的意义所在。
【免费下载链接】OpenVikingSelf-evolving Context Database for AI Agents. Unify Agent Memory, Knowledge RAG and Skills.项目地址: https://gitcode.com/GitHub_Trending/op/OpenViking
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考