关于 handoff
handoff 是将受支持的 CLI 编码会话交给 Lark 的技能,让用户离开终端后仍可与 Claude 交互。它提供配置、会话交接、群组管理、诊断、配置档和 Agent 控制功能。
主要功能
- 01将会话交接到 Lark 群聊
- 02设置并检查交接配置
- 03管理群组和配置档
- 04诊断权限桥接往返通信
- 05升级或移除已安装组件
- 06控制 macOS 上已安装的 Agent
适用场景
- 01通过 Lark 继续 CLI 编码会话
- 02开始交接前检查配置
- 03以 sidecar 模式加入外部 Lark 群组
- 04查看或管理已安装的 Agent
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ verneagent/ handoff --skill handoff安装后在 Claude Code 中输入/handoff确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
.gitignoreCLAUDE.mdSKILL-commands.mdSKILL-agent.mdREADME.mdhooks.jsonSKILL-setup.mdassets/opencode/plugins/handoff.tsassets/opencode/scripts/permission_bridge.pyhandoff_tool_forwarding.jsscripts/handoff_lifecycle.pyenter_handoff.pygroup_config.pyhagenthandoff_db.pyhandoff_config.pyend_and_cleanup.pyhandoff_worker.pyhandoff_agent.pyhandoff_ops.pyinstall_hooks.pyinstall_agent.shiterm2_silence.pylark_im.pyon_post_tool_use.pyon_pre_compact.pylark_auth.pyon_notification.pyon_pre_tool_use_bash.pyon_session_end.py详细介绍
将 CLI 会话移交给 Lark,让用户可以随时随地继续与 Claude 互动。
技能路径
本文档中的所有脚本路径都相对于此技能的基础目录。例如,scripts/preflight.py 表示 <base_directory>/scripts/preflight.py。加载此技能时,宿主工具会提供基础目录。
平台检测
遵循此协议之前,请先识别你的运行环境:
- Claude Code — 你是
claudeCLI。按本文档所述操作。 - OpenCode — 你是
opencodeAgent。请在整个操作过程中应用下方的 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.pyAgent 进程中。
在 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 可能会被摘要省略。以下要点必须保留:
- 循环(绝不退出):
wait_for_reply.py(仅第一条消息)→ 处理 →send_and_wait.py(发送回复并等待下一条消息)→ 处理 →send_and_wait.py→ ……循环仅在收到“handback”或发生接管时结束。 - 沙盒:所有网络 Python 脚本都必须使用
dangerouslyDisableSandbox: true - 不要使用 AskUserQuestion:通过
send_and_wait.py向 Lark 发送问题,绝不要使用 AskUserQuestion - 超时处理:脚本默认超时时长为 GPT 模型 540 秒,其他所有情况为 0(无限),并输出
{"timeout": true}。**超时时绝不要发送状态消息。**立即静默地重新调用同一脚本。若send_and_wait.py超时,消息已发送——调用wait_for_reply.py以恢复等待。 - 交还控制权:如果用户发送“handback”,则正常退出(发送告别消息 → 停用 → 恢复通知)
- 接管:如果轮询输出包含
"takeover": true,则另一个会话已接管此聊天。静默退出——运行handoff_ops.py deactivate、handoff_ops.py tabs-end --session-model '${session_model}'、iterm2_silence.py off。不要发送 Lark 消息(由新会话处理)。在本地打印“控制权已被另一个会话接管。” - 内置命令:处理消息前,检查是否包含以下命令:
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
仅在调试时才使用下面的手动流程。
- 静音终端通知(仅限 iTerm2):
python3 scripts/iterm2_silence.py on
- 确保交接标签页存在且顺序正确(先是消息,然后是工具/模型):
python3 scripts/handoff_ops.py tabs-start --session-model '${session_model}'
- 发送初始交接卡片(从环境变量自动解析工具,从参数自动解析模型):
python3 scripts/handoff_ops.py send-status-card start --session-model '${session_model}'
- 进入主循环(见下文)。
主循环
重要:这是一个无限循环,除非用户说“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”),请识别意图并执行:
- 按名称查找目标群组(使用
handoff_ops.py list-groups --scope all进行模糊匹配)。如果结果不明确,请通过表单卡片展示选项。 - 发送转发消息:
这会通过 Worker 发送消息(推送到活跃会话的目标 DO),并向目标群组发送 Lark 卡片(即使没有活跃会话也能看到)。python3 scripts/handoff_ops.py relay --target-chat-id '<CHAT_ID>' --message '<content>' - 向用户确认:“已发送到 [群组名称]”
收到转发消息时(回复包含 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."。
- 停止循环。
- 向 Lark 发送交接卡片(在停用之前,以确保会话仍处于活动状态):
解散并交接(回复中包含 "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."。
- 停止循环。
- 向 Lark 发送交接卡片(在停用之前):
步骤 3:处理用户的消息
将 Lark 回复视为用户在 CLI 中输入的内容。根据需要执行相应工作(读取文件、编辑代码、运行命令、回答问题等)。
禁止假装取得进展(严格执行):
- 对于任何要求采取行动(读取/编辑/运行/调试/审查)的 Lark 消息,必须先使用工具实际执行操作,再回复。
- 不要发送“正在处理”或“还在处理”之类的占位回复,除非确实已有耗时较长的操作在运行。
- 如果发送进度更新,必须说明当前正在运行的具体操作(命令/文件/任务)。
- 交接生效期间,绝不要声称工作需要切回 CLI 才能继续。
- 如果遇到阻碍,请说明具体问题,并在 Lark 中提出一个明确的问题。
线程回复(parent_id):如果回复中包含 parent_id,说明用户是在回复聊天中的特定消息。请获取父消息以了解上下文。由于 Lark API 返回的交互式卡片消息内容可能不完整,请采用以下两步流程:
- 先尝试本地查找——检查本地
messages表,该表由record_sent_message()保存机器人发送的每条消息的原始文本/标题(包括卡片消息):python3 scripts/handoff_ops.py parent-local --parent-id '<PARENT_ID>' - 再回退到 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 提示词。示例流程:
- 用户在 Lark 中说:“创建一个 3 人团队来重构 auth 模块”
- 负责人调用
TeamCreate,使用适当的提示词和成员数量 - PostToolUse hook 会自动将团队创建事件转发到 Lark
- 负责人向 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__"。 - 备用方式:如果用户回复的是文本数字,而不是使用卡片,也要解析。
- 2 个选项(是/否、批准/拒绝): 使用按钮卡片(带有
- 收集文本输入 — 当需要用户输入内容时(提交说明、搜索查询等),请使用带输入字段的表单卡片:
回调会以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解析正确的聊天:
绝不要编写临时 Python 代码来发送图片/文件——当存在多个会话时,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]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
三个组件协同工作:
- Lark App — 位于群聊中的机器人,接收事件并通过 IM API 发送消息
- Cloudflare Worker — 接收 Lark Webhook 事件和卡片操作回调,并将回复存储在 Durable Objects 中。通过 WebSocket(首选,使用 Hibernation API)或 HTTP 长轮询(备用)发送回复
- 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.md | Handoff 模式技能定义 |
.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.py | Lark IM API 客户端(令牌、发送、回复、下载、轮询) |
.claude/skills/handoff/scripts/on_notification.py | 通知钩子(发送 Lark 消息) |
.claude/skills/handoff/scripts/on_post_tool_use.py | PostToolUse/PostToolUseFailure 钩子(将工具输出和错误转发到 Lark) |
.claude/skills/handoff/scripts/on_pre_compact.py | PreCompact 钩子(上下文即将压缩时提醒 Lark 聊天) |
.claude/skills/handoff/scripts/on_pre_tool_use_bash.py | PreToolUse 钩子 — 仅在 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.js | Cloudflare Worker(webhook + 卡片回调 + Durable Objects) |
.claude/skills/handoff/worker/wrangler.toml | Worker 部署配置 |
~/.handoff/config.json | 凭据和配置(app_id、app_secret、worker_url、worker_api_key、email) |
~/.handoff/projects/<project>/handoff-data.db | SQLite 数据库(handoff 状态 + 双向消息历史) |
${HANDOFF_TMP_DIR:-/tmp/handoff}/handoff-images/ | 已下载的图片 |
${HANDOFF_TMP_DIR:-/tmp/handoff}/handoff-files/ | 已下载的文件 |
.opencode/plugins/handoff.ts | OpenCode 插件(权限桥接、生命周期、通知、环境注入) |
.opencode/scripts/permission_bridge.py | OpenCode 插件调用的权限桥接脚本 |
.opencode/scripts/handoff_tool_forwarding.js | OpenCode 插件使用的工具输出转发脚本 |