mcpField
H
handoff

handoff

github.com/verneagentAgent 工作流136 次安装29 周前更新
代码仓库

关于 handoff

handoff 是将受支持的 CLI 编码会话交给 Lark 的技能,让用户离开终端后仍可与 Claude 交互。它提供配置、会话交接、群组管理、诊断、配置档和 Agent 控制功能。

主要功能

  1. 01将会话交接到 Lark 群聊
  2. 02设置并检查交接配置
  3. 03管理群组和配置档
  4. 04诊断权限桥接往返通信
  5. 05升级或移除已安装组件
  6. 06控制 macOS 上已安装的 Agent

适用场景

  1. 01通过 Lark 继续 CLI 编码会话
  2. 02开始交接前检查配置
  3. 03以 sidecar 模式加入外部 Lark 群组
  4. 04查看或管理已安装的 Agent

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/verneagent/handoff --skill handoff

安装后在 Claude Code 中输入/handoff确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.gitignore
CLAUDE.md
SKILL-commands.md
SKILL-agent.md
README.md
hooks.json
SKILL-setup.md
assets/opencode/plugins/
handoff.ts
assets/opencode/scripts/
permission_bridge.py
handoff_tool_forwarding.js
scripts/
handoff_lifecycle.py
enter_handoff.py
group_config.py
hagent
handoff_db.py
handoff_config.py
end_and_cleanup.py
handoff_worker.py
handoff_agent.py
handoff_ops.py
install_hooks.py
install_agent.sh
iterm2_silence.py
lark_im.py
on_post_tool_use.py
on_pre_compact.py
lark_auth.py
on_notification.py
on_pre_tool_use_bash.py
on_session_end.py

详细介绍

将 CLI 会话移交给 Lark,让用户可以随时随地继续与 Claude 互动。

技能路径

本文档中的所有脚本路径都相对于此技能的基础目录。例如,scripts/preflight.py 表示 <base_directory>/scripts/preflight.py。加载此技能时,宿主工具会提供基础目录。

平台检测

遵循此协议之前,请先识别你的运行环境:

  • Claude Code — 你是 claude CLI。按本文档所述操作。
  • OpenCode — 你是 opencode Agent。请在整个操作过程中应用下方的 OpenCode 覆盖规则,然后遵循本文档其余部分。
  • Agent 模式 — 你正在通过 Claude Agent SDK 运行于 Handoff Agent 进程中(检查:HANDOFF_SESSION_TOOL 环境变量的值为 Claude Agent SDK)。请应用下方的 Agent 模式覆盖规则。不要进入 Handoff 循环,也不要调用等待脚本——Agent 进程会处理消息 I/O。
  • Codex 或任何其他工具 — 停止。 此技能仅支持 Claude Code、OpenCode 和 Agent 模式。请告知用户:“此平台不支持 Handoff。”

Agent 模式覆盖规则

仅限 Agent 模式。 设置了 HANDOFF_SESSION_TOOL=Claude Agent SDK 时,你运行于 handoff_agent.py Agent 进程中。

在 Agent 模式下,SDK 客户端是一个支持 Lark 的编码引擎。它处理用户请求并返回文本响应。Agent 进程(handoff_agent.py)负责所有消息 I/O——从 Lark 接收消息并发送消息到 Lark。

SDK 客户端的行为由 SKILL-agent.md 定义,而非本文档。本文档(SKILL.md)仅供 CLI 和 OpenCode Handoff 使用。

架构
  • Agent 进程:会话生命周期、消息接收、命令分发(/clear、/model 等)、向 Lark 发送响应
  • SDK 客户端:编码任务、文件操作、Lark 媒体 I/O(download-image、send-image、send-file、通过 handoff_ops.py 进行 parent-local 操作)
SDK 客户端不会执行的操作
  • 调用 send_to_group.py(Agent 进程会发送其文本结果)
  • 调用 wait_for_reply.py 或 send_and_wait.py
  • 管理会话、激活状态、停用状态或标签页
  • 遵循本文档 SKILL.md

OpenCode 覆盖规则

仅限 OpenCode 用户。 在下方整个协议中应用这些规则。Claude Code 用户可完全跳过本节。

插件依赖项

此技能需要 OpenCode Handoff 插件(.opencode/plugins/handoff.ts),用于处理权限桥接、会话生命周期、通知转发和环境注入(HANDOFF_PROJECT_DIR、HANDOFF_SESSION_ID、HANDOFF_SESSION_TOOL)。OpenCode 会自动从 .opencode/plugins/ 加载该插件。

如果尚未安装插件,/handoff init 的第 4 步会安装该插件(见下方的 init 覆盖规则)。

沙盒

不适用。不要使用 dangerouslyDisableSandbox。所有 python3 / curl / 网络命令均可正常运行,无需沙盒标志。

子命令

守卫列表包含 init、deinit、clear、delete_admin 和 purge_admin。

预检

运行 preflight.py --tool opencode(检查插件文件,跳过 Claude hooks)。

/handoff init — 第 4 步覆盖规则

跳过 Claude Code hooks 步骤。改为从技能资源中安装 OpenCode 插件文件:

SKILL=$(python3 -c "import os; p='.claude/skills/handoff'; print(p if os.path.isdir(p) else os.path.expanduser('~/.claude/skills/handoff'))")
mkdir -p .opencode/plugins .opencode/scripts
cp "$SKILL/assets/opencode/plugins/handoff.ts" .opencode/plugins/
cp "$SKILL/assets/opencode/scripts/permission_bridge.py" .opencode/scripts/
cp "$SKILL/assets/opencode/scripts/handoff_tool_forwarding.js" .opencode/scripts/
rm -rf "$SKILL/assets/opencode"

在汇总表中显示 “OpenCode 插件”,而不是“hooks”。

需要重启: OpenCode 会在启动时加载插件。init 安装插件后,请告知用户退出并重新打开 OpenCode,然后再运行 /handoff。

/handoff deinit 覆盖规则

遵循 SKILL-commands.md 中的 OpenCode 路径(删除插件文件,而非 hooks)。

AskUserQuestion

OpenCode 中不可用。将每次 AskUserQuestion 调用替换为对话中的纯文本问题。按范围应用:

  • 交接激活前(初始化向导、群组选择、Step C):在 CLI 对话中以纯文本提问。
  • 交接激活后(主循环):将所有问题通过 send_and_wait.py 路由到 Lark。绝不使用交互式 CLI 提示。

具体中断情况:

  • 等待期间按 Esc/Ctrl+C:直接在 CLI 中询问“结束交接吗?”(用户此时在终端)。
  • 主循环期间进行 BLOCKED 检查:通过 send_to_group.py 提问,并使用 wait_for_reply.py 等待回复。
权限桥接

插件通过 permission.asked 事件自动处理。无需配置钩子。

会话连续性

插件会在交接激活期间处理 session.idle,并自动发送续接提示。无需手动处理。

循环保活策略(OpenCode)

空闲期间绝对保持静默。 wait_for_reply.py 超时后:

  • 立即静默地重新运行 wait_for_reply.py --timeout 0。不发送消息,不更新状态。
  • 仅在以下情况发送更新:状态发生实质性变化、用户明确请求,或有长时间运行的工作正在进行(>60s)。
  • 禁止在空闲期间发送“正在等待……”、“我还在”或任何保活消息。
  • 如果确有必要,使用指数退避,最长为 1 小时;但优先保持静默。
交互式提示 — 进入模式时的澄清
  • Step A(already_active=true)且用户未请求 no ask 时:在 CLI 对话中询问是继续还是重新选择。
  • Step C 默认模式下,所有群组均已占用时:在 CLI 对话中询问是接管还是新建。不要静默切换到 no-ask。
  • 展示群组选项时,直接根据 Step B 的输出生成;确认选项数量与 N 个群组相符。如果不符,重新运行 Step B。

子命令

此技能支持通过参数使用子命令:

  • /handoff(无参数)——运行预检;如果失败,则询问是否要直接运行设置向导。如果通过,则进入交接模式。
  • /handoff <group name>——运行预检,然后在所有机器人聊天中按名称查找群组。如果群组是外部群组(没有 workspace: 标签),则自动进入Sidecar 模式。如果是带有常规工作区标签的群组,则进入常规模式。将 --group-name '<GROUP_NAME>' 传递给 enter_handoff.py。响应中包含 sidecar_mode: true/false——如果为 true,按 Sidecar 模式的循环流程操作(第 4 步);如果为 false,则按常规流程操作(步骤 E)。
  • /handoff help——打印所有受支持的子命令及其说明。不要进入交接模式。可随时安全运行。
  • /handoff init——运行完整的交互式设置向导,执行所有步骤,即使已有配置值也不例外。对于每个步骤,提供“保留现有值”(如果已有值)、“提供新值”或“新建”(如适用)选项。仅限 CLI——无法在交接模式期间运行。
  • /handoff check——运行预检,并打印已配置项和缺失项的状态报告。不要进入交接模式。可随时安全运行。
  • /handoff chats——列出与当前用户关联的所有 Lark 聊天群组。不要进入交接模式。可随时安全运行。
  • /handoff chats_admin——列出机器人加入的所有 Lark 聊天群组,不论用户是谁。不要进入交接模式。可随时安全运行。
  • /handoff status——显示当前项目的交接状态(活跃/不活跃、所有者、聊天群组、上次活动时间)。不要进入交接模式。可随时安全运行。
  • /handoff delete_admin [group name]——按名称删除聊天群组。如果提供了名称,则查找并删除匹配的群组(先确认)。如果未提供名称,则列出群组并询问用户要删除哪些群组。不要进入交接模式。仅限 CLI——无法在交接模式期间运行。
  • /handoff purge_admin——查找并删除所有空聊天群组(没有人类成员,只有机器人)。列出这些群组,并在删除前请求确认。不要进入交接模式。仅限 CLI——无法在交接模式期间运行。
  • /handoff deinit——移除由 /handoff init 安装的所有内容(Claude Code 的钩子;OpenCode 的插件文件),然后询问是否也要删除 ~/.handoff/config.json(默认:否)。先请求确认。仅限 CLI——无法在交接模式期间运行。
  • /handoff clear——删除当前项目的聊天群组和交接数据库。先请求确认。仅限 CLI——无法在交接模式期间运行。
  • /handoff diag [--mode ws|http|both] [--chat-id ID] [--timeout N]——运行权限桥接诊断:发送带按钮的测试卡片、轮询响应,并报告往返流程是否正常。默认模式为 ws。不要进入交接模式。可随时安全运行。
  • /handoff sidecar——进入Sidecar 模式:加入现有的外部 Lark 群组(并非由机器人创建),并且只响应发给机器人的消息(@提及、回复机器人消息或表情/贴纸反应)。使用相同的交接循环,但会筛选消息并跳过群组修改。
  • /handoff upgrade——从 GitHub 下载最新版本并安装。如果 hooks.json 有变化,则重新安装钩子。报告更新了哪些文件。可随时安全运行,也可在交接模式期间运行。
  • /handoff upgrade --check——检查是否有可用更新,但不安装。可随时安全运行。
  • /handoff use:<profile>——使用指定名称的配置文件进入交接模式(例如 /handoff use:work)。将 --profile '<PROFILE>' 传递给 preflight.py、enter_handoff.py 和所有 handoff_ops.py 调用。可与群组名称组合使用:/handoff use:work MyGroup。
  • /handoff profile——列出所有配置文件(默认配置文件以及 ~/.handoff/profiles/ 中的配置文件)。显示当前使用的配置文件和默认配置文件。不要进入交接模式。可随时安全运行。
  • /handoff profile show——显示当前配置文件名称和配置文件路径。不要进入交接模式。可随时安全运行。
  • /handoff profile set-default <name>——设置默认配置文件。写入 ~/.handoff/default_profile。不要进入交接模式。可随时安全运行。
  • /handoff init profile:<profile>——为指定名称的配置文件运行设置向导,而不是使用默认配置文件。创建 ~/.handoff/profiles/<profile>.json。
  • /handoff agent 或 /handoff agent list——列出所有已安装的 Agent。不要进入交接模式。仅限 macOS。可随时安全运行。
  • /handoff agent install——交互式操作:选择群组、选择项目目录和模型,并安装新的 launchd Agent 服务。不要进入交接模式。仅限 CLI。仅限 macOS。
  • /handoff agent status [name]——显示 Agent 的状态和近期日志。如果只有一个 Agent,则名称可选。不要进入交接模式。可随时安全运行。
  • /handoff agent stop <name>——停止正在运行的 Agent。不要进入交接模式。可随时安全运行。
  • /handoff agent start <name>——启动已停止的 Agent。不要进入交接模式。可随时安全运行。
  • /handoff agent uninstall <name>——停止并移除 Agent。不要进入交接模式。仅限 CLI。
  • /handoff agent log [name]——显示 Agent 的近期日志。不要进入交接模式。可随时安全运行。 解析参数字符串,以确定要执行哪个子命令。

**配置文件语法:**当参数字符串包含 use:<name> 时,提取配置文件名称,并在所有 Python 脚本调用中传入 --profile '<name>'。在匹配其他子命令之前,从参数字符串中移除 use:<name> 这个标记。例如,/handoff use:work MyGroup → 配置文件为 work,群组名称为 MyGroup。当存在 profile:<name> 时(用于 init),以同样的方式提取。

**防护:**运行 init、deinit、clear、delete_admin 或 purge_admin 前,检查 handoff 当前是否处于启用状态。如果是,则拒绝执行,并告知用户:“无法在 handoff 模式下运行此命令。请先发送 handback,以返回 CLI。”

**子命令实现:**对于 chats、chats_admin、status、delete_admin、purge_admin、deinit、clear、diag 和 profile,请阅读同一目录中的 SKILL-commands.md,了解详细代码和说明。

沙盒:重要

**所有使用 curl 的 Bash 命令,以及调用使用 curl 的 Python 脚本(preflight.py、wait_for_reply.py、send_to_group.py、lark_im.py)的 Bash 命令,都必须使用 dangerouslyDisableSandbox: true 运行。**沙盒会阻止访问 Lark API 和 Cloudflare Worker 的网络连接,导致静默失败。在整个 handoff 过程中,每次调用都必须遵守此要求——包括上下文压缩之后。

此外,在 Bash 中传递消息字符串时,始终使用单引号(而不是双引号),以避免反斜杠与感叹号转义相关的问题。

压缩后的恢复

自动压缩后,完整的 SKILL.md 可能会被摘要省略。以下要点必须保留:

  1. 循环(绝不退出):wait_for_reply.py(仅第一条消息)→ 处理 → send_and_wait.py(发送回复并等待下一条消息)→ 处理 → send_and_wait.py → ……循环仅在收到“handback”或发生接管时结束。
  2. 沙盒:所有网络 Python 脚本都必须使用 dangerouslyDisableSandbox: true
  3. 不要使用 AskUserQuestion:通过 send_and_wait.py 向 Lark 发送问题,绝不要使用 AskUserQuestion
  4. 超时处理:脚本默认超时时长为 GPT 模型 540 秒,其他所有情况为 0(无限),并输出 {"timeout": true}。**超时时绝不要发送状态消息。**立即静默地重新调用同一脚本。若 send_and_wait.py 超时,消息已发送——调用 wait_for_reply.py 以恢复等待。
  5. 交还控制权:如果用户发送“handback”,则正常退出(发送告别消息 → 停用 → 恢复通知)
  6. 接管:如果轮询输出包含 "takeover": true,则另一个会话已接管此聊天。静默退出——运行 handoff_ops.py deactivate、handoff_ops.py tabs-end --session-model '${session_model}'、iterm2_silence.py off。不要发送 Lark 消息(由新会话处理)。在本地打印“控制权已被另一个会话接管。”
  7. 内置命令:处理消息前,检查是否包含以下命令:filter verbose/important/concise(设置消息筛选器)、autoapprove on/off(切换自动批准——运行 handoff_ops.py set-autoapprove <on|off>)、handback(退出)。这些是 handoff 内部命令,不是要执行的任务。

如果在 handoff 仍处于启用状态时发现自己已离开循环,请重新阅读此文件,并从 Main Loop 部分继续。

工作区 ID 和数据

工作区 ID 的格式为 {machine}-{folder},其中 folder 根据 HANDOFF_PROJECT_DIR(若未设置则使用当前工作目录)确定。例如:MacBookPro-Users-alice-projects-myapp。该 ID 由 lark_im.get_workspace_id() 计算,用于标识实际的代码位置(机器 + 文件夹路径),并以 workspace:{id} 的形式存储在 Lark 群组描述中。

Handoff 数据存储在单个 SQLite 数据库中,路径为 ~/.handoff/projects/{project}/handoff-data.db。数据库使用 WAL 模式,以确保并发访问安全(钩子和主进程)。其中包含:

  • sessions 表 — 每个会话的交接状态:session_id(主键)、chat_id(唯一)、session_tool、session_model、last_checked、activated_at、operator_open_id(激活时根据配置中的电子邮件解析得出——仅筛选操作员的消息)、bot_open_id(激活时根据机器人信息解析得出——用于侧车模式交互筛选)、sidecar_mode(侧车模式为 1,否则为 0——脚本从会话中读取该值,无需 CLI 标志)、config_profile(用于激活的配置配置文件名称——钩子从会话中读取该值,以加载正确的凭据)。
  • messages 表 — 双向消息历史记录(direction=sent|received),包含 message_id、source_message_id、chat_id、message_time、text、title、sent_at。

帮助(/handoff help)

打印所有受支持子命令的格式化表格。不要进入 Handoff 模式。

命令描述
/handoff进入交接模式(包含预检和引导式设置)
/handoff <group name>加入指定群组(自动检测侧车模式或常规模式)
/handoff help显示此帮助
/handoff init完整的交互式设置向导(仅限 CLI)
/handoff check运行预检并打印状态报告
/handoff chats列出你的 Lark 交接群组
/handoff chats_admin列出所有 Lark 交接群组
/handoff status显示当前项目的交接状态
/handoff delete_admin [name]删除交接群组(仅限 CLI)
/handoff purge_admin删除没有人类成员的空群组(仅限 CLI)
/handoff deinit移除已安装的钩子和插件文件;可选择删除配置(仅限 CLI)
/handoff clear删除当前项目的聊天群组和数据库(仅限 CLI)
/handoff sidecar侧车模式:加入外部群组,仅回复 @ 提及
/handoff diag运行权限桥接诊断(测试卡片操作 → 轮询往返流程)
/handoff upgrade从 GitHub 下载并安装最新版本
/handoff upgrade --check检查是否有可用更新
/handoff use:<profile>使用指定名称的配置文件进入交接模式
/handoff profile列出所有配置文件
/handoff profile show显示当前配置文件详情
/handoff profile set-default <name>设置默认配置文件
/handoff init profile:<profile>运行指定配置文件的设置向导
/handoff agent列出已安装的 Agent(macOS)
/handoff agent install为聊天群组安装新的 Agent
/handoff agent status [name]显示 Agent 状态和近期日志
/handoff agent stop <name>停止正在运行的 Agent
/handoff agent start <name>启动已停止的 Agent
/handoff agent uninstall <name>移除已安装的 Agent
/handoff agent log [name]显示近期 Agent 日志

预检

运行预检以验证所有要求:

python3 scripts/preflight.py

如果指定了配置文件(通过 use:<profile>),请传入 --profile '<profile>':

python3 scripts/preflight.py --profile '<profile>'
对于 /handoff check

运行详细报告并停止。不要进入 Handoff 模式,也不要运行设置:

python3 scripts/preflight.py --report

将输出打印给用户。此报告会显示所有已配置的值、钩子状态和交接数据。

对于 /handoff(不带参数)

如果脚本以非零代码退出,请告知用户:“尚未设置 Handoff。运行 /handoff init 即可开始。” 询问用户现在是否要运行设置,或退出。无论如何都不要进入 Handoff 模式。

对于 /handoff init

跳过预检。读取 SKILL-setup.md,并无条件执行所有步骤的引导式设置。

Sidecar 模式(/handoff sidecar)

Sidecar 模式允许交接机器人加入一个现有的外部 Lark 群组,并且只回复来自操作员(其电子邮件地址位于交接配置中的用户)且发给机器人的消息。符合以下任一条件的消息均视为发给机器人:(1) @提及机器人;(2) 回复机器人发送的消息;或 (3) 是表情回应或贴纸。与会创建专用群组的常规交接不同,Sidecar 模式适用于机器人已加入的任何群组。

步骤 1:预检

运行标准预检。如果失败,告知用户运行 /handoff init。

步骤 2:发现外部群组
python3 scripts/handoff_ops.py discover-bot

这会返回机器人已加入但没有 workspace: 标签的群组(即不是由交接创建的群组)。输出包含 bot_open_id 和 open_id(用于激活时填充会话表)。

解析 JSON 输出。令 N = 群组数量。

  • 如果 N == 0: 告知用户:“未找到外部群组。请先将机器人添加到 Lark 群组,然后重试。”
  • 如果 N == 1: 自动选择该群组。
  • 如果 N >= 2: 询问用户要加入哪个群组。
步骤 3:激活
python3 scripts/handoff_ops.py activate --chat-id '<CHAT_ID>' --session-model '${session_model}' --sidecar

--sidecar 标志会在会话表中存储 sidecar_mode=1,这样所有脚本都会自动知道需要应用机器人交互过滤。

步骤 4:进入 Sidecar 模式循环
python3 scripts/start_and_wait.py --session-model '${session_model}'

所有 Sidecar 模式行为都从会话表读取:sidecar_mode(跳过标签页/卡片,启用交互过滤)、bot_open_id(@提及匹配)、operator_open_id(发送者过滤)。无需 --sidecar CLI 标志——所有内容都已在步骤 3 激活期间存储。

步骤 5:主循环(与常规交接相同)

主循环与常规 Handoff 模式相同,但有一点不同:只会接收来自操作员且发给机器人的消息。照常使用 send_and_wait.py——Sidecar 模式、操作员和交互过滤都会自动应用(从会话表读取)。send_and_wait.py 无需 --sidecar 标志。

所有交接命令的用法都相同:“handback”可退出,长任务可使用心跳,等等。

进入 Handoff 模式

步骤 A–D:enter_handoff.py

运行唯一入口脚本。它会一次性处理环境解析、会话检查、群组发现、自动选择和激活:

python3 scripts/enter_handoff.py --session-model '${session_model}'

当用户明确要求使用这些模式时,传入 --mode no-ask 或 --mode new。

当用户指定了配置(通过 use:<profile>)时,传入 --profile '<PROFILE>'。配置会存储在会话 DB 中,以便钩子加载正确的配置。

当用户提供了特定的群组名称时(例如 /handoff MyGroup),传入 --group-name '<GROUP_NAME>'。此参数会在所有 bot 聊天中查找群组,并自动检测它是外部群组(sidecar)还是带工作区标签的常规群组。如果响应中的 sidecar_mode 为 true,请遵循 sidecar 循环路径(按照 Sidecar Mode Step 4 使用 start_and_wait.py)。

状态值:

  • "status": "hooks_pending" — 钩子刚刚安装,但尚未加载。立即停止。 告诉用户:“请退出并重启 Claude Code,然后运行 /handoff。”不要继续。不要解释技术细节。
  • "status": "restart_required" — 会话环境变量缺失(钩子尚未运行)。立即停止。 告诉用户:“请退出并重启 Claude Code,然后再次运行 /handoff。”不要尝试绕过此问题,也不要手动生成缺失的值。不要解释技术细节。
  • "status": "ready" — 激活已完成。提取 chat_id、session_id、project_dir。如果响应包含 "rules" 字段,这些就是群组专属规则(类似每个群组各自的 CLAUDE.md)。在整个交接会话中,必须遵循这些规则——其权威性与 CLAUDE.md 指令相同。继续执行步骤 E。
  • "status": "already_active" — 此会话已有正在进行的交接。
    • 如果用户要求 no ask / auto:继续使用当前聊天(步骤 E)。
    • 否则询问:继续当前聊天,还是通过 --mode new 重新运行以获取新群组。
  • "status": "choose" — 所有群组都已占用;Claude 必须询问用户:
    • 根据 groups 数组构建选项(每个群组:label = 名称 [active],description = chat_id)。
    • 添加“新建”选项。
    • 选择后:
      • “新建”:运行 python3 scripts/handoff_ops.py create-group --existing-names-json '<JSON>',然后运行 activate。
      • 已占用的群组:运行接管操作,然后跳过 activate:
        python3 scripts/handoff_ops.py takeover --chat-id '<CHAT_ID>' --session-model '${session_model}'
        
        如果接管返回 ok: false,请重新运行 enter_handoff.py 并再次选择。
步骤 E:静音 + 发送初始消息 + 进入循环

快捷方式:

python3 scripts/start_and_wait.py --session-model '${session_model}'

此命令会自动运行步骤 E.1–E.4(静音 → 标签页 → 状态卡片 → wait_for_reply.py)。选项:

  • --tab-url <url> — 覆盖工具/模型标签页使用的 URL
  • --skip-silence、--skip-tabs、--skip-card — 如果某个步骤 已完成,则跳过该步骤
  • --timeout、--no-ws、--interval — 转发给 wait_for_reply.py

仅在调试时才使用下面的手动流程。

  1. 静音终端通知(仅限 iTerm2):
python3 scripts/iterm2_silence.py on
  1. 确保交接标签页存在且顺序正确(先是消息,然后是工具/模型):
python3 scripts/handoff_ops.py tabs-start --session-model '${session_model}'
  1. 发送初始交接卡片(从环境变量自动解析工具,从参数自动解析模型):
python3 scripts/handoff_ops.py send-status-card start --session-model '${session_model}'
  1. 进入主循环(见下文)。

主循环

重要:这是一个无限循环,除非用户说“handback”或发生接管,否则永远不会退出。处理完每条消息后,Bash 工具调用会阻塞并等待下一条消息——你总会收到一条新消息要处理。绝不要得出“交接已运行/已激活/已就绪”的结论并停止。始终会有下一条消息需要处理。

卡片标题由 send-status-card 自动解析:

  • 开始:Handoff from <tool> (<model>)
  • 结束:Hand back to <tool>

工具从 HANDOFF_SESSION_TOOL 环境变量读取;模型来自 --session-model 参数。

步骤 1:等待用户的第一条消息(仅限首次迭代)

此步骤仅在循环开始时运行一次。后续消息会通过步骤 4 的 send_and_wait.py 到达。

python3 scripts/wait_for_reply.py
  • 解析 JSON 输出。对于 Claude 模型,脚本会无限期等待(timeout=0);对于 GPT 模型,最多等待 540 秒。如果没有收到回复,则会干净地退出并返回 {"timeout": true}。
  • 如果用户中断(按 Esc 或 Ctrl+C——Bash 工具调用被拒绝):使用 AskUserQuestion 询问用户“结束交接?”(这是循环中唯一允许使用 AskUserQuestion 的地方,因为用户此时在命令行中)。如果用户确认,则干净地退出 Handoff 模式——向 Lark 发送“正在交还给命令行。”,然后停用、恢复通知并停止循环。如果用户拒绝,则重新进入步骤 1(再次调用 wait_for_reply.py)。
  • 如果是 takeover:表示另一个会话已接管。静默退出 Handoff 模式(停用、恢复通知)。在本地打印“交接已被另一个会话接管。”不要发送 Lark 消息(新会话会处理)。
  • 如果是 timeout:表示在超时时间内没有收到回复。立即再次调用 wait_for_reply.py。这是正常的空闲行为。
  • 如果找到回复:将所有回复文本拼接为用户消息。同时检查每条回复中的 mentions 数组——其中每个被 @ 提及的用户都对应一项,例如 {"key": "@_user_1", "id": "ou_xxx", "name": "Lucy"}。文本字段会使用 @_user_1 这样的占位符表示提及——使用 mentions 数组将其解析为实际姓名和 open_id。这对访客/共同所有者命令至关重要。继续执行步骤 2。
步骤 2:检查命令

过滤器命令:如果任何回复文本与 filter verbose、filter important 或 filter concise 匹配(不区分大小写),则更新消息过滤器:

python3 scripts/handoff_ops.py set-filter <level>

向 Lark 发送简短确认(例如“过滤器已设为 verbose”),然后继续循环(返回等待下一条通过 send_and_wait.py 到达的消息)。

过滤级别控制转发到 Lark 的 PostToolUse 内容:

  • verbose — 转发 Edit + Write + Bash 输出
  • important — 仅转发 Edit + Write(跳过 Bash,除非出错)
  • concise — 不转发 PostToolUse 内容(默认)

自动批准命令:如果任何回复文本与 autoapprove on、autoapprove off、auto approve on/off 或类似内容匹配(不区分大小写),则切换自动批准模式:

python3 scripts/handoff_ops.py set-autoapprove <on|off>

向 Lark 发送确认。自动批准设为 on 时,所有权限请求都会自动批准,不会发送批准/拒绝卡片。这适用于用户不希望被打断的可信会话。设为 off(默认)时,使用常规权限桥接流程。

群规命令:群规是一个键值映射(类似于每个群组各自的 CLAUDE.md)。仅当出现“群规”、“group norm”或“gnorm”时才触发——不要仅因出现“rule”、“rules”或“规则”等通用词就触发(这些可能指 Cursor 规则、inscribe 或其他技能指令)。

  • 添加/更新:“群规 lang: 回复用中文”、“gnorm prod: don't touch prod”、“group norm workflow: explain before coding”。提取键(触发词后的第一个词)和文本(冒号/空格后的内容),然后:

    python3 scripts/handoff_ops.py add-norm --key '<KEY>' --text '<RULE_TEXT>'
    

    向 Lark 发送确认,说明已添加/更新的规则。

  • 删除:“删群规 prod”、“群规删 lang”、“gnorm remove prod”。提取键,然后:

    python3 scripts/handoff_ops.py remove-norm --key '<KEY>'
    

    向 Lark 发送确认。

  • 显示全部:“群规”、“gnorm”或“group norms”(单独出现,不带键/文本)。运行:

    python3 scripts/handoff_ops.py get-norms
    

    将规则以格式化列表的形式发送到 Lark(如果为空,则发送“No rules set.”)。

规则会保存在群组的置顶配置卡片中,并在每次后续会话激活时自动加载。

访客和共同所有者命令:所有者可以管理允许与机器人互动的成员白名单。此功能在常规模式和 sidecar 模式下均可使用。灵活识别这些命令(自然语言,任意语言):

  • 添加访客:所有者提及用户,意图授予访客访问权限。示例:“add @jack @alice as guest”“@jack 和 @alice 可以和你对话”“let @bob talk to you”。文本中会包含 @_user_1 之类的占位符——请从回复的 mentions 数组中查找实际的 open_id 和 name(过滤掉机器人自身的提及)。然后:

    python3 scripts/handoff_ops.py guest-add --guests-json '[{"open_id":"ou_xxx","name":"Jack"},{"open_id":"ou_yyy","name":"Alice"}]'
    

    向 Lark 发送确认消息,列出已添加的用户。

  • 添加共同所有者:所有者提及用户,意图授予共同所有者权限。示例:“add @alice as coowner”“@alice 是 coowner”“make @bob a coowner”。与上述相同——从回复的 mentions 数组中提取 open_id 和 name(不要从文本中提取)。然后:

    python3 scripts/handoff_ops.py guest-add --role coowner --guests-json '[{"open_id":"ou_xxx","name":"Alice"}]'
    

    向 Lark 发送确认消息,列出已添加为共同所有者的用户。

  • 移除成员:所有者提及用户,意图撤销其访问权限。示例:“remove @jack”“@alice 不要了”“revoke @bob”。从提及中提取 open_id,然后:

    python3 scripts/handoff_ops.py guest-remove --open-ids-json '["ou_xxx"]'
    

    向 Lark 发送确认消息,列出已移除的用户。

  • 列出成员:所有者说“guests”“members”或“who has access”。运行:

    python3 scripts/handoff_ops.py guest-list
    

    将列表发送到 Lark(显示每位成员的角色)。

共同所有者权限规则:当回复包含 "privilege": "coowner" 时,按与所有者相同的方式处理:

  • 与所有者权限相同:shell、git、文件编辑、完整项目访问权限
  • 权限请求:共同所有者可以批准或拒绝权限卡片(与所有者相同)
  • 所有者覆盖:所有者可以覆盖共同所有者的决定。如果双方都对权限卡片作出响应,则以所有者的决定为准。

访客权限规则:当回复包含 "privilege": "guest" 时,将其视为低权限请求:

  • 允许:提问、获取解释、在临时文件夹($TMPDIR)下创建新文件、修改临时文件
  • 不允许:运行 shell 命令、执行 git 操作、读取或修改现有项目文件、访问机密(.env、凭据、API 密钥)、在项目目录下创建文件
  • 权限请求:仅处理所有者和共同所有者对权限卡片(批准/拒绝)的操作。忽略访客的按钮点击。
  • 所有者覆盖:始终优先执行所有者的命令。如果所有者说“停止”,立即停止访客请求的所有工作。
  • 回复访客时,使用 send_and_wait.py 中的 --mention-user-id 来 @提及对方(另请参阅“重要说明”中的 @提及目标规则):
    python3 scripts/send_and_wait.py '<response>' --mention-user-id '<guest_open_id>'
    

Sidecar 与常规模式:Sidecar 模式与常规模式之间唯一的区别是机器人交互筛选器(filter_bot_interactions)。在 Sidecar 模式下,消息必须明确发给机器人(@提及机器人、回复机器人,或使用表情回应/贴纸)。在常规模式下,会直接处理所有获准发送者的消息。访客和共同所有者支持在两种模式下完全相同。

转发命令:如果用户要求将信息分享、转发或转送到另一个交接群组(例如“share this to group X”“relay to meadow”“把这个发到 X 群”“分享到 X”),请识别意图并执行:

  1. 按名称查找目标群组(使用 handoff_ops.py list-groups --scope all 进行模糊匹配)。如果结果不明确,请通过表单卡片展示选项。
  2. 发送转发消息:
    python3 scripts/handoff_ops.py relay --target-chat-id '<CHAT_ID>' --message '<content>'
    
    这会通过 Worker 发送消息(推送到活跃会话的目标 DO),并向目标群组发送 Lark 卡片(即使没有活跃会话也能看到)。
  3. 向用户确认:“已发送到 [群组名称]”

收到转发消息时(回复包含 msg_type: "relay"),请向用户展示:

  • 使用表情指示来源:💬from_chat_name(避免群组名称与正文混在一起)
  • 展示消息内容
  • 用户可以回复——使用 relay 将回复发送回来源群组

结束交接命令:如果任何回复文本匹配 handback 或 hand back(不区分大小写),则退出 Handoff 模式。文本还可以选择包含 dissolve 一词(例如“handback dissolve”“hand back dissolve”),以便在结束交接后同时解散(删除)聊天群组。 重要:绝不要自行发起交接。 只有当用户明确发送 "handback" 或 "hand back" 作为消息时,才执行交接。不要因为以下原因而交接:(a) 任务似乎已完成;(b) 对话似乎处于闲置状态;或 (c) 你在自己的回复中写了 "handback?",并假定用户已同意。必须由用户说出这些内容——你自己的消息不算。也不要随意提议交接。 任务已完成或对话暂时停顿,都不是说“要交接回去吗?”的理由。只有在用户确实能从返回 CLI 中受益,且有具体原因时,才提及交接(例如,工具限制确实阻碍了进展)。用户知道如何结束会话。

正常交接(不解散):

  • 首选辅助工具:
    python3 scripts/end_and_cleanup.py --session-model '${session_model}'
    
    这会自动执行卡片 → 标签页 → 停用 → 静默这一系列操作。
  • 手动步骤(如果辅助工具无法运行):
    • 向 Lark 发送交接卡片(在停用之前,以确保会话仍处于活动状态):
      python3 scripts/handoff_ops.py send-status-card end --session-model '${session_model}'
      
    • 移除此交接对应的会话标签页(工具/模型):
      python3 scripts/handoff_ops.py tabs-end --session-model '${session_model}'
      
    • 停用交接(从本地数据库中移除会话):
      python3 scripts/handoff_ops.py deactivate
      
    • 恢复终端通知:
      python3 scripts/iterm2_silence.py off
      
    • 在本地打印 "Handoff ended. Back to CLI."。
    • 停止循环。

解散并交接(回复中包含 "dissolve"):

  • 首选辅助工具:
    python3 scripts/end_and_cleanup.py --session-model '${session_model}' --dissolve --body 'Handing back to CLI. Dissolving chat group...'
    
    --body 控制结束文本,--dissolve 则会在停用后运行 remove-user / dissolve-chat / cleanup-sessions。
  • 手动步骤:
    • 向 Lark 发送交接卡片(在停用之前):
      python3 scripts/handoff_ops.py send-status-card end --session-model '${session_model}' --body 'Handing back to CLI. Dissolving chat group...'
      
    • 移除此交接对应的会话标签页(工具/模型):
      python3 scripts/handoff_ops.py tabs-end --session-model '${session_model}'
      
    • 记下停用输出中的 chat_id,以便用于解散步骤。
    • 停用交接(从本地数据库中移除会话):
      python3 scripts/handoff_ops.py deactivate
      
    • 将用户移出群组(使群组从其聊天列表中消失):
      python3 scripts/handoff_ops.py remove-user --chat-id '<CHAT_ID>'
      
    • 解散群聊并清理所有剩余会话:
      python3 scripts/handoff_ops.py dissolve-chat --chat-id '<CHAT_ID>'
      python3 scripts/handoff_ops.py cleanup-sessions --chat-id '<CHAT_ID>'
      
    • 恢复终端通知:
      python3 scripts/iterm2_silence.py off
      
    • 在本地打印 "Handoff ended. Chat group dissolved. Back to CLI."。
    • 停止循环。
步骤 3:处理用户的消息

将 Lark 回复视为用户在 CLI 中输入的内容。根据需要执行相应工作(读取文件、编辑代码、运行命令、回答问题等)。

禁止假装取得进展(严格执行):

  • 对于任何要求采取行动(读取/编辑/运行/调试/审查)的 Lark 消息,必须先使用工具实际执行操作,再回复。
  • 不要发送“正在处理”或“还在处理”之类的占位回复,除非确实已有耗时较长的操作在运行。
  • 如果发送进度更新,必须说明当前正在运行的具体操作(命令/文件/任务)。
  • 交接生效期间,绝不要声称工作需要切回 CLI 才能继续。
  • 如果遇到阻碍,请说明具体问题,并在 Lark 中提出一个明确的问题。

线程回复(parent_id):如果回复中包含 parent_id,说明用户是在回复聊天中的特定消息。请获取父消息以了解上下文。由于 Lark API 返回的交互式卡片消息内容可能不完整,请采用以下两步流程:

  1. 先尝试本地查找——检查本地 messages 表,该表由 record_sent_message() 保存机器人发送的每条消息的原始文本/标题(包括卡片消息):
    python3 scripts/handoff_ops.py parent-local --parent-id '<PARENT_ID>'
    
  2. 再回退到 Lark API——如果本地未找到(父消息来自其他用户,而不是机器人):
    python3 scripts/handoff_ops.py parent-api --parent-id '<PARENT_ID>'
    

使用父消息内容理解用户回复中的“这个”“那个”或“它”所指的对象。

图片消息:如果回复带有 image_key(msg_type: "image",或包含内嵌图片的帖子),请在处理前下载图片。帖子中可能包含多个以逗号分隔的 image_key,此时请逐一下载:

python3 scripts/handoff_ops.py download-image --image-key '<IMAGE_KEY>' --message-id '<MESSAGE_ID>'

然后使用 Read 工具读取下载的图片文件并查看其内容。用户可能会发送截图供你分析、Figma 设计稿、错误截图等。

文件消息:如果回复的 msg_type: "file" 且带有 file_key,请在处理前下载文件:

python3 scripts/handoff_ops.py download-file --file-key '<FILE_KEY>' --message-id '<MESSAGE_ID>' --file-name '<FILE_NAME>'

然后使用 Read 工具读取下载的文件。用户可能会发送代码文件、日志、配置文件、文档等。

合并转发消息:如果回复的 msg_type: "merge_forward",说明用户转发了来自其他聊天的对话线程。请使用 Lark API 获取子消息:

python3 scripts/handoff_ops.py merge-forward --message-id '<MESSAGE_ID>'

解析 JSON 输出——每行都是一条转发线程中的消息。将这段对话作为上下文呈现给用户。如果任何子消息包含图片(msg_type: "image"),请使用上述图片下载流程,并传入该子消息的 message_id。根据用户的要求总结或分析该线程。

工作卡片:PostToolUse 钩子会在工具执行期间自动向 Lark 发送并更新“Working...”卡片。卡片标题会随时间逐步变化(“Working...” → “Working hard...” 等),并包含 Stop 按钮。你无需另行发送心跳消息——钩子会处理进度显示。发送回复后,卡片会更新为“Done ✓”。

步骤 4:发送回复并等待下一条消息

完成工作后,使用 send_and_wait.py 向 Lark 发送回复。此脚本会发送消息,并阻塞,直到收到下一条用户消息。其输出就是下一条用户消息(JSON 格式与 wait_for_reply.py 相同)。

python3 scripts/send_and_wait.py '<your response>'

此调用不会返回“Sent.”,而是会阻塞并返回下一条用户消息。 按照步骤 1 的方式解析 JSON 输出,然后使用这条新消息继续执行步骤 2。

  • 如果是 takeover:静默退出 Handoff 模式(与步骤 1 相同)。
  • 如果是 timeout:消息已发送。调用 wait_for_reply.py 恢复等待(无需重新发送)。
  • 如果用户中断(Esc/Ctrl+C):消息已发送。像步骤 1 一样询问“结束 handoff?”。
  • 如果找到回复:将所有回复文本串联为用户消息。检查 mentions 数组——文本会使用 @_user_1 等占位符表示 @ 提及;通过每条回复的 mentions 数组(key、id/open_id、name)解析这些占位符。继续执行步骤 2。

格式选项(与 send_to_group.py 使用相同的标志):

  • Markdown 卡片(默认,不使用 --card):用于所有对话回复,包括答案、解释、问题、确认和分析结果。这是默认格式。
  • 状态卡片(--card):仅用于简短的系统消息。
  • 表单卡片:用于选项选择和文本输入,使用 handoff_ops.py send-form-select / send-form-input,然后使用 wait_for_reply.py --timeout 0 获取表单回复。之后继续处理,并使用 send_and_wait.py 发送最终回复。

在消息中包含代码块时,请使用 2 个空格缩进,以便在手机上阅读。

消息应保持简洁——Lark 有长度限制。对于较长的输出,请概述内容,并告知用户可以查看 CLI 获取完整详情。

Agent Teams 集成

当设置了 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 时,handoff 负责人 Agent 可以在 handoff 模式下创建和协调 Agent 团队。

检测团队请求

在步骤 2(命令检查)中,检测用户是否要求创建团队、查看团队状态或与队友沟通。例如:

  • “创建一个 3 人团队来重构 auth 模块”
  • “团队状态” / “团队进展如何?”
  • “告诉审查员重点关注安全性”
  • “关闭团队”
创建团队

直接使用 TeamCreate 工具。用户的 Lark 消息描述了工作内容——将其转化为 TeamCreate 提示词。示例流程:

  1. 用户在 Lark 中说:“创建一个 3 人团队来重构 auth 模块”
  2. 负责人调用 TeamCreate,使用适当的提示词和成员数量
  3. PostToolUse hook 会自动将团队创建事件转发到 Lark
  4. 负责人向 Lark 发送确认,并附上团队成员姓名
监控团队进展

使用 team_status.py 读取团队和任务状态,并发送更新:

# List active teams
python3 scripts/team_status.py list

# Detailed status with progress bar
python3 scripts/team_status.py status <team_id>

# Format as Lark card content
python3 scripts/team_status.py card <team_id>

当用户询问团队状态时,运行 team_status.py card <team_id>,并将输出作为卡片发送:

python3 scripts/send_to_group.py "$(python3 scripts/team_status.py card <team_id>)" --title 'Team Progress'
与队友沟通

使用 SendMessage 将 Lark 用户的消息转达给指定队友:

  • “告诉编码人员使用 TypeScript” → 向“coder”队友发送 SendMessage
  • “询问所有队友的状态更新” → 广播 SendMessage

队友的回复会通过共享任务列表到达。在较长的团队任务中,定期检查 team_status.py status,并向 Lark 发送进度更新。

handoff 期间的团队生命周期
  • 创建:TeamCreate 会将队友作为本地进程启动。他们会独立工作。
  • 监控:负责人定期检查任务进度,并向 Lark 汇报。
  • 完成:所有任务完成后,负责人总结结果并发送到 Lark。
  • 清理:团队工作完成后,使用 TeamDelete。
  • 交接返回:如果团队仍在运行时发生交接返回,应提醒用户队友仍在本地运行。
PostToolUse 转发

该钩子会自动将以下团队事件转发到 Lark(不受消息筛选级别影响):

  • TeamCreate — 团队创建,并附带成员名称
  • SendMessage — Agent 间消息(仅摘要)
  • TeamDelete — 团队解散

重要说明

  • 所有通信都通过 Lark 进行。 每条回复、问题、确认和状态更新都必须发送到 Lark 线程。CLI 会显示相同的输出,供本地参考,但用户正在查看 Lark。
  • 循环中的执行优先级。 如果 Lark 回复要求执行实际工作(代码编辑、调试、读取文件、运行命令、审查、分析),请先完成步骤 3 的工作,再在步骤 4 中发送结果。不要将这些任务简化为仅发送保活或状态回复。
  • 保活范围有限。 “继续循环”/“继续等待”的行为仅适用于空闲时段。不得因此忽略 Lark 中待处理的用户任务。
  • 不要虚构 CLI 要求。 在交接处于活动状态时,不要声称编码必须切回 CLI。请直接在当前活动的交接会话中完成代码工作。
  • 交接循环期间,绝不要使用 AskUserQuestion 或 EnterPlanMode。 这些工具只会在 CLI 中显示提示,用户在交接模式下看不到。请改用 send_and_wait.py 将问题发送到 Lark(该脚本也会等待回复)。问题应按编号列出选项,以便用户只需回复数字。(注意:在进入交接循环之前运行的引导式设置步骤 1-4 中,CLI 模式下会使用 AskUserQuestion。)
  • 多人群组中的所有者隐私。 群组中有访客或非所有者成员时,绝不要在 Lark 消息中透露所有者的个人信息,包括真实姓名、系统用户名、文件路径(例如 /Users/xxx/)、电子邮件地址,或从环境中推断出的任何个人身份信息。提及所有者时,只使用其 Lark 显示名称,或称其为“所有者”。系统级详情(路径、配置位置等)只能保留在 CLI 输出中。
  • 多人群组中的 @ 提及目标。 群组中有 2 位或以上人类成员(不包括机器人)且回复是针对特定人员时(例如该人员提出了问题、为其完成了任务,或正在答复其请求),始终使用 send_and_wait.py 或 send_to_group.py 上的 --mention-user-id '<their_open_id>' 来 @ 提及对方。这样可以确保通知发送给正确的人。从你正在回复的消息的 sender_id 字段中提取 open_id。如果群组中只有一位人类成员(仅所有者),则无需 @ 提及。
  • 确认和权限: 执行任何破坏性或不可逆操作(git push、合并、删除等)之前,先向 Lark 发送确认消息并等待用户回复。没有明确的 Lark 确认,不得继续操作。
  • 选项选择 — 选择正确的格式:
    • 2 个选项(是/否、批准/拒绝): 使用按钮卡片(带有 buttons 的 build_card)。用户可快速点击,无需提交。
    • 3 个或更多选项: 使用表单卡片(带有 selects 的 build_form_card)。下拉菜单便于用户选择。始终在卡片正文中以文本列出选项,这样用户无需打开下拉菜单即可查看:
      python3 scripts/handoff_ops.py send-form-select --title '<TITLE>' --body '**Options:\n1. Option A — description\n2. Option B — description\n3. Option C — description' --field-name choice --placeholder 'Select...' --options-json '[["Option A","a"],["Option B","b"],["Option C","c"]]'
      
      回调会以 msg_type: "form_action" 到达,text 中包含所选值(多个字段时为 JSON)。 添加 --cancel-label Cancel,即可在表单下方显示 Cancel 按钮。点击后,回调会以 msg_type: "button_action" 到达,且 text: "__cancel__"。
    • 备用方式:如果用户回复的是文本数字,而不是使用卡片,也要解析。
  • 收集文本输入 — 当需要用户输入内容时(提交说明、搜索查询等),请使用带输入字段的表单卡片:
    python3 scripts/handoff_ops.py send-form-input --title '<TITLE>' --body '<PROMPT_TEXT>' --field-name '<FIELD_NAME>' --placeholder '<PLACEHOLDER>'
    
    回调会以 msg_type: "form_action" 到达,其中包含用户输入的值。用户也可以直接回复文本消息,而不使用卡片。
  • 如果任务需要多次调用工具,请全部完成后再向 Lark 发送一条汇总回复(不要每次调用工具都发送一条消息)。
  • 超时处理:wait_for_reply.py 和 send_and_wait.py 默认对 GPT 模型使用 540s,对其他所有情况使用 0(无限等待);若未收到回复,则会通过 {"timeout": true} 正常退出。
    • 重要:超时后,立即重新调用脚本,且不要向用户发送任何状态消息。这是正常的空闲行为,不是退出信号。
    • 绝对规则:空闲期间不要发送“正在等待”或“我还在”之类的消息。保持安静并继续等待。
    • 例外情况:仅在以下情况发送状态更新:(a) 状态发生有意义的变化,(b) 用户明确要求,或 (c) 有长时间运行的工作正在进行(任务耗时超过 60s)。
    • 对于 send_and_wait.py 超时的情况,消息已经发送——调用 wait_for_reply.py 仅恢复等待(不要重新发送)。
  • “把 XX 发给我” = 文件附件。 用户要求发送文件时(例如“把 SKILL.md 发给我”),请使用 handoff_ops.py send-file 上传并发送。不要将文件内容粘贴为文本。
  • 发送图片和文件。 始终使用 handoff_ops.py 命令——这些命令会根据 HANDOFF_SESSION_ID 解析正确的聊天:
    python3 scripts/handoff_ops.py send-image /path/to/image.png
    python3 scripts/handoff_ops.py send-file /path/to/file.pdf [--file-type pdf]
    
    绝不要编写临时 Python 代码来发送图片/文件——当存在多个会话时,get_active_sessions()[0] 可能会选错群组。
  • 提交哈希链接。 在 Lark 消息中提及提交哈希时,请将其格式化为指向 GitHub 提交页面的可点击链接:hash。通过 git remote get-url origin 确定 org/repo。
  • 反应路由。 send_to_group.py 发送消息时,会自动通过 register_message() 向工作器注册该消息。这样,工作器便能将针对机器人消息的表情符号反应路由回交接会话。反应会以 msg_type: "reaction" 到达,text 中包含表情类型(例如 "THUMBSUP")。无需特殊处理——此功能会自动生效。
  • 贴纸和反应礼仪。 自然互动,切勿像机器人一样生硬。
    • 收到反应时(例如用户对你的消息发送 👍):这只是表示认可,通常无需回复。绝不要回以相同贴纸。
    • 收到贴纸消息时:根据上下文回应。你报告完成后收到点赞,意思是“做得好,继续吧”——保持安静或继续工作才是正确做法。只有在有有意义的内容可说时才回复。
    • 主动发送反应时:对用户的消息添加反应,表明你正在积极参与。使用 lark_im.add_reaction(token, message_id, emoji_type)。常见类型及其使用场景:
      • THUMBSUP — 确认收到请求、确认理解
      • DONE — 任务已成功完成
      • MUSCLE — 即将着手处理具有挑战性的任务
      • OK — 简单确认,表示收到
      • LAUGH / LOL — 轻松时刻、有趣的事情
      • FACEPALM — 自己犯了个小错误
      • THINKING — 正在分析复杂问题
      • LOVE / FINGERHEART — 用户提供了帮助或表达了善意
      • APPLAUSE — 庆祝里程碑或好消息
      • SOB — 出了问题,表达对挫折的理解
      • JIAYI(+1)— 赞同用户的建议
    • 发送贴纸回复时:使用 lark_im.reply_sticker(token, message_id, file_key) 来表达更丰富的情绪。从用户发送的贴纸消息中获取 file_key(file_key 位于消息载荷中)。缓存并重复使用见过的贴纸。
    • 少量且自然地使用贴纸/反应。以贴纸回复贴纸会显得尴尬。恰到好处的反应胜过文字回复。
  • 所有脚本都会通过 lark_im.get_workspace_id() 自动计算工作区 ID。它标识代码所在的实际位置(机器 + 文件夹路径)。

架构

You (Lark) --> Lark Event --> Cloudflare Worker (Durable Objects)
                                     ^
Claude Code connects via WebSocket   |  (HTTP long-poll fallback)
Claude Code sends responses via Lark IM API --> Lark Group

三个组件协同工作:

  1. Lark App — 位于群聊中的机器人,接收事件并通过 IM API 发送消息
  2. Cloudflare Worker — 接收 Lark Webhook 事件和卡片操作回调,并将回复存储在 Durable Objects 中。通过 WebSocket(首选,使用 Hibernation API)或 HTTP 长轮询(备用)发送回复
  3. Claude Code hooks & scripts — 通知、权限桥接、会话生命周期钩子和交接模式技能

故障排除

Worker 无法访问
  • 确认 ~/.handoff/config.json 中的 worker_url 正确
  • 测试:curl -s -H "Authorization: Bearer <your-api-key>" 'https://<your-worker>.workers.dev/poll/test?timeout=1'
  • 应返回:{"replies":[],"count":0}(约 1 秒后)
  • 如果收到 Unauthorized,请检查配置中的 worker_api_key 是否与 Worker 密钥 API_KEY 匹配
下载图片时返回 JSON 错误
  • 图片 API 要求使用消息资源端点,而非图片端点
  • 正确:/im/v1/messages/{message_id}/resources/{image_key}?type=image
  • 错误:/im/v1/images/{image_key}(会返回权限错误)
  • 确保已为应用授予 im:resource 权限范围
未收到回复
  • 检查 Lark App 的事件订阅 URL 是否指向你的 Worker(/webhook)
  • 确认已配置卡片回调 URL(/card-action)
  • 确认机器人已添加到群聊
  • 确保你在交接群组中发送消息(每个项目都有自己的群组)
  • 检查 Worker 日志:npx wrangler tail
令牌错误
  • ~/.handoff/config.json 中的凭据可能包含错误的 app_id / app_secret
  • 令牌每 2 小时过期一次;脚本会自动刷新令牌

文件参考

文件用途
.claude/skills/handoff/SKILL.mdHandoff 模式技能定义
.claude/skills/handoff/SKILL-setup.md引导式设置向导说明
.claude/skills/handoff/SKILL-commands.md子命令实现(chats、status、deinit 等)
.claude/skills/handoff/hooks.json规范钩子定义(单一事实来源)
.claude/skills/handoff/scripts/lark_im.pyLark IM API 客户端(令牌、发送、回复、下载、轮询)
.claude/skills/handoff/scripts/on_notification.py通知钩子(发送 Lark 消息)
.claude/skills/handoff/scripts/on_post_tool_use.pyPostToolUse/PostToolUseFailure 钩子(将工具输出和错误转发到 Lark)
.claude/skills/handoff/scripts/on_pre_compact.pyPreCompact 钩子(上下文即将压缩时提醒 Lark 聊天)
.claude/skills/handoff/scripts/on_pre_tool_use_bash.pyPreToolUse 钩子 — 仅在 handoff 处于活动状态时批准 Bash
.claude/skills/handoff/scripts/permission_bridge.py权限桥接钩子(通过 Lark 批准/拒绝)— Claude Code
.claude/skills/handoff/scripts/permission_core.py共享的权限桥接轮询/决策核心(Claude + OpenCode)
.claude/skills/handoff/scripts/worker_http.py共享的 urllib worker 轮询/确认辅助函数(OpenCode + 实用工具)
.claude/skills/handoff/scripts/on_session_start.py会话开始钩子(检测活动的 handoff)
.claude/skills/handoff/scripts/on_session_end.py会话结束钩子(通知 Lark 并清理)
.claude/skills/handoff/scripts/send_to_group.py向 handoff 群组发送消息(即发即弃,用于心跳/状态)
.claude/skills/handoff/scripts/send_and_wait.py发送回复并等待下一条回复(主循环步骤 4)
.claude/skills/handoff/scripts/wait_for_reply.py等待新回复(仅首次迭代时,或 send_and_wait 超时后)
.claude/skills/handoff/scripts/handoff_ops.py替代内联 Python 代码片段的确定性辅助命令
.claude/skills/handoff/scripts/run_tests.py用于 CI/本地检查的一键 handoff 测试运行器
.claude/skills/handoff/scripts/iterm2_silence.py切换 iTerm2 终端通知
.claude/skills/handoff/scripts/preflight.py预检验证
.claude/skills/handoff/worker/src/index.jsCloudflare Worker(webhook + 卡片回调 + Durable Objects)
.claude/skills/handoff/worker/wrangler.tomlWorker 部署配置
~/.handoff/config.json凭据和配置(app_id、app_secret、worker_url、worker_api_key、email)
~/.handoff/projects/<project>/handoff-data.dbSQLite 数据库(handoff 状态 + 双向消息历史)
${HANDOFF_TMP_DIR:-/tmp/handoff}/handoff-images/已下载的图片
${HANDOFF_TMP_DIR:-/tmp/handoff}/handoff-files/已下载的文件
.opencode/plugins/handoff.tsOpenCode 插件(权限桥接、生命周期、通知、环境注入)
.opencode/scripts/permission_bridge.pyOpenCode 插件调用的权限桥接脚本
.opencode/scripts/handoff_tool_forwarding.jsOpenCode 插件使用的工具输出转发脚本

评论

支持 Markdown