Skip to content

hachiwii/twinny

Repository files navigation

Twinny

Twinny banner

npm version

English Version

环境要求

  • macOS、Linux、WSL2 或 Windows。installer 在 macOS 上管理 LaunchAgent(或 --system-daemon LaunchDaemon),在 Linux/启用 systemd 的 WSL2 上管理 systemd user service,在 Windows 上管理用户级计划任务。
  • Node.js 22.18.0 或更新版本。Twinny 使用 Node.js 内置 node:sqlite 模块,不需要额外安装 SQLite native addon。
  • PATH 中有 Codex CLI 0.134.0 或更新版本,或者设置 CODEX_BINARY;如果缺少 Codex,installer 可以自动安装。
  • 一个已配置下方权限和事件订阅的 Feishu/Lark 机器人应用。

安装

Codex 安装指南

把下面这段内容复制给你的 Codex,让它按指南完成 agent 安装:

Read https://raw.githubusercontent.com/hachiwii/twinny/master/agent_installation_guide.md and follow instructions in it.

手动安装

通过 npx 运行交互式 installer:

npx twinny@latest install

macOS 默认安装为当前 GUI session 的 LaunchAgent。如果在 SSH、CI 或其他没有 GUI LaunchAgent 的环境中安装,installer 会退出并提示改用 system daemon 模式:

npx twinny@latest install --system-daemon

--system-daemon 会通过 sudo 把 plist 写入 /Library/LaunchDaemons,并用当前用户写入 UserName;后续 startstoprestartstatus 会根据 config.toml 中的 service 配置继续操作 LaunchDaemon。

Windows 原生安装会创建用户级 Task Scheduler 任务,随用户登录启动。WSL2 如果没有启用 systemd,installer 不会安装托管服务;这种环境需要用 TWINNY_HOME=/path/to/home twinny run 前台运行,或先启用 WSL systemd 后重新安装。

常用服务命令:

npx twinny@latest doctor
npx twinny@latest status
npx twinny@latest update
npx twinny@latest start
npx twinny@latest stop
npx twinny@latest restart
npx twinny@latest uninstall

如果不使用默认 home,给任意命令加上 TWINNY_HOME=/path/to/home

secret 会存放在 config.toml 之外。非 macOS 安装(包括 Windows)默认把 Lark app_secret 写入 TWINNY_HOME/auth.jsonlark_app_secret 字段。macOS 安装默认使用系统 Keychain;如果加 --disable-keychain 或 Keychain 写入失败,也会写入 auth.json。启动时优先读取 auth.json,然后才回退到 TWINNY_LARK_APP_SECRET 和旧 secret store(macOS Keychain,其他平台 runtime/secrets.json)。

飞书/Lark 应用配置

你需要在飞书/Lark 开发者后台申请这些必要 API 权限:

im:message.p2p_msg:readonly
im:message.group_at_msg:readonly
im:message:readonly
im:message:send_as_bot
im:message:update
im:message:recall
im:message.reactions:write_only
im:chat:read
im:chat:create
im:chat:update
im:resource
contact:user.base:readonly
docs:document.comment:read
docs:document.comment:create
docs:document.comment:write_only
docs:document.media:download
wiki:node:read

推荐同时申请 im:message.group_msg。它允许 /activate owner/activate all 在群聊中接收非 @ 消息;如果只使用 owner_atall_at,基础的 im:message.group_at_msg:readonly 已足够。安装指引页面的导入 JSON 会预填这个推荐权限,便于后续切换群响应模式。

如果启用自动激活 greeting(私聊需 permissions.p2p_default_profile != "none"greeting.p2p.mode != "none";群聊需 group_default_profilegroup_default_mode 都不是 nonegreeting.group.mode != "none"),还需要申请 im:chat.members:bot_access。私聊 greeting 额外订阅 p2p_chat_create;群聊 greeting 额外订阅 im.chat.member.bot.added_v1

订阅这些事件/回调:

im.message.receive_v1
im.message.recalled_v1
drive.notice.comment_add_v1
application.bot.menu_v6
card.action.trigger

事件订阅方式请选择飞书/Lark 事件长连接(WebSocket)。Twinny 不需要为消息事件暴露公网 HTTP callback URL。

可选机器人快捷菜单可以配置这些 event_key

Event key 动作
help 发送指令帮助。
status 查看当前会话和 thread 状态。
queue 切换下一条消息排队模式。
new 在当前会话中新开 Codex thread。
stop 停止当前 turn 并清空队列。

用法

向机器人发送普通消息即可开始或继续 Codex turn。在群聊中,owner 必须先激活群聊,普通消息才会被路由给 Codex。

会话指令

指令 用法
/help 查看可用指令。
/status 查看会话、Codex thread、模型、token 和队列状态。
/new 停止当前任务、清空队列,并新开 Codex thread。
/stop [all|<side_id>] 停止当前任务并清空队列。用 all 同时停止 side turn,或传 side id 停止指定 side turn。
/next 打断当前任务,并开始执行下一条排队消息。
/steer <message> message 注入当前正在运行的 Codex turn;message 可继续解析指令。
/queue [message] 不带 message 时让你的下一条消息排队;带 message 时把该消息加入下一轮,出队时可继续解析指令。
/goal <objective> 设置并运行 Codex goal。goal 运行中再次发送 /goal 会更新目标。
/plan [message] 进入 plan mode。带 message 时直接用 plan mode 处理该消息。
/exit 在下一轮队列控制步骤中退出 plan mode。
/side <message>/btw <message> 基于当前 Codex thread 发起临时 side conversation。
/compact 在下一轮队列控制步骤中压缩当前 Codex thread 上下文。
/thread [message] 创建一个新的 Lark 话题,并绑定新的 Codex thread。带 message 时会把消息代理到新话题内继续解析。
/fork [message] 从当前 Codex thread fork 出一个新的 Lark 话题。带 message 时会把消息代理到新话题内继续解析。
/watch <lark_doc_url> [owner|all]/watch rm <id|url> 监听飞书/Lark 文档中的 @bot 评论,并把评论路由到当前 thread。不带参数时列出当前 thread 的监听和 id;owner 只响应 owner,all 响应所有人;rm 删除当前 thread 的监听。
/workspace [dir|num] 仅 owner 可用。查看或设置当前 conversation workspace,并同步主会话 thread。可传绝对路径、~/...,或最近 workspace 列表中的序号。
/cd [dir|num] 仅 owner 可用。查看或设置当前非主 thread workspace。可传绝对路径、~/...,或最近 workspace 列表中的序号。
/resume [thread_id|num] [session|local] 仅 owner 可用。列出本机可恢复 Codex thread,或把指定 thread 恢复为 Twinny 话题;默认使用原会话 cwd,local 使用当前会话 cwd。
/model <model> <effort> 设置当前 thread 后续 turn 使用的模型和推理强度。
/cron <cron exp> <message>/cron/cron rm <id> 管理当前 conversation 的定时任务;触发时 message 可继续解析指令。
/logo 发送 Twinny logo 图片。
/twinny/banner 发送 Twinny banner 卡片。

指令按从左到右的语法解析器执行。固定参数只消费自己的参数,例如 /model <model> <effort> 后面仍可继续跟下一条指令;/plan/goal/side 会把后续文本作为原始消息,不再解析其中的指令;/queue/steer/thread/fork/cron <cron exp> <message>message 会在实际执行时递归解析。/queue/steer 只能作为最外层第一条指令,放在 /thread/fork 或其他指令尾部时会作为普通文本。

例如:

/queue /model gpt-5.5 xhigh /plan xxx
/cron * * * * * /goal xxx

群管理指令

只有配置中的 owner 可以执行这些指令:

指令 用法
/activate <owner_at|owner|all_at|all> [profile] 激活群聊,设置谁可以把消息路由给 Codex,刷新群名,并可选绑定 profile。
/deactivate 停用当前群聊并清空待处理任务。
/pair {guest_ou_id} <profile> 授权非 owner 的 P2P 用户,并绑定到某个 profile。
/reload [profile] 修改配置后重载所有 Codex profiles,或只重载指定 profile。
/restart 调度 Twinny 托管服务重启。
/upgrade [stable|beta] 检查并升级 Twinny。默认 stable,只会升级到更高版本。
/upgrade check [stable|beta] 只检查指定通道是否有新版本。

响应模式:

  • owner_at:只响应 owner 且提及 bot 的消息。
  • owner:响应 owner 的所有消息。
  • all_at:响应任意群成员且提及 bot 的消息。
  • all:响应任意群成员的所有消息。

推荐实践

为项目创建一个专用飞书/Lark 群。在该群的 workspace 中写一份 AGENTS.md。由 owner 用尽量小的可用权限激活群聊,然后为每个开发任务创建一个独立话题:

/activate all host
/thread fix the login callback race
/thread add the GitHub README

当一个任务需要尝试替代方向,同时保留原 Codex thread 历史时,用 /fork

/fork try the smaller refactor path

把每个任务的讨论留在对应话题里。这样 Codex 上下文、本地 workspace 状态、Lark 讨论和状态卡都会按任务隔离。

安全说明

Twinny 运行在 owner 的本地机器上。请把它当作本地自动化桥接工具,而不是已经加固过的多人共享执行服务。

当前默认配置还没有 fully ready for 广泛多人共享。尤其是当你用 host profile 和 all 响应模式激活群聊时,群里所有能发言的成员都可以用 owner 的 Codex 执行权限来运行任务:

/activate all host

也要谨慎使用 all_at:如果群聊绑定到高权限 profile,所有能提及 bot 的成员都可以提交任务。

在共享群聊中使用 Twinny 前:

  • 优先给 guest 或团队 profile 配置独立的 codex_home,不要共享 owner 的 ~/.codex
  • 为该 profile 配置 Codex sandbox、filesystem、network 和 approval 相关安全设置。
  • 如果你的 Codex 设置支持项目级安全策略,在 workspace 的 .codex 中加入 override。
  • 除非你明确希望未配对 P2P 用户获得访问权限,否则保持 permissions.p2p_default_profile = "none"
  • 除非你明确希望新群聊在第一条满足触发条件的消息上自动激活,否则保持 permissions.group_default_profile = "none"permissions.group_default_mode = "none"
  • 除非群成员范围非常可控,否则优先使用 owner_atowner,不要直接使用 allall_at

进阶配置

Twinny 从 TWINNY_HOME 读取 config.toml

支持的字段:

字段 含义和取值
[codex].binary Codex CLI 可执行文件路径或命令名。默认是 codex。如果托管服务不能通过 PATH 找到 Codex,建议使用绝对路径。Windows 上通常是 codex.cmd
[codex].masquerade_as_codex_cli 初始化 Codex app-server 时把 Twinny 的 clientInfo 伪装为 Codex TUI:name = "codex-tui"title = nullversion 从启动时运行的 codex --version 输出中解析。默认是 false
[lark.reaction].working Twinny 工作中给 Lark 消息添加的 emoji type。默认是 JubilantRabbit
[lark.reaction].queued 消息进入队列时添加的 Lark emoji type。默认是 OneSecond
[lark.redaction].email 发往 Lark 的 payload 中邮箱地址的脱敏策略。mask 保留域名并遮蔽邮箱用户名,例如 alice@example.com 会变成 a***e@example.comwhitespace 插入空格,例如 alice @ example.comnone 发送明文邮箱。飞书可能会拦截包含明文邮箱或手机号的 bot message。默认是 mask
[lark.redaction].chinese_phone_number 发往 Lark 的 payload 中中国手机号的脱敏策略。mask 保留前 3 位和后 4 位,例如 138****5678whitespace 插入空格,例如 138 1234 5678none 发送明文手机号。飞书可能会拦截包含明文邮箱或手机号的 bot message。默认是 mask
[permissions].p2p_default_profile 未配对 P2P 用户第一次给 Twinny 发消息时使用的 profile。用 none 表示默认拒绝,也可以填已配置的 profile 名称来自动授权。默认是 none
[permissions].p2p_default_workspace 新 P2P conversation 的默认 workspace 模板。支持 {{twinny_home}}{{conversation_key}} 变量;默认是 {{twinny_home}}/workspaces/{{conversation_key}}。如果渲染后的目录已存在会直接使用,否则会创建。
[permissions].group_default_profile 新群聊自动激活时使用的 profile。必须和 group_default_mode 同时为非 none 才会生效;默认是 none
[permissions].group_default_mode 新群聊自动激活后的群响应模式,取值为 owner_atownerall_atallnone。当它和 group_default_profile 都不是 none 时,新群聊收到第一条满足该模式触发条件的消息会自动创建 workspace 并激活。默认是 none
[permissions].group_default_workspace 新 group conversation 的默认 workspace 模板。支持 {{twinny_home}}{{conversation_key}} 变量;默认是 {{twinny_home}}/workspaces/{{conversation_key}}。如果渲染后的目录已存在会直接使用,否则会创建。
[greeting.p2p].mode P2P conversation 创建后的 greeting 模式:nonetextcodex_turntext 会直接向单聊发送固定文案;codex_turn 会把 message 作为 Codex turn 输入并向单聊发送 turn card。默认是 none
[greeting.p2p].message P2P greeting 文案或 Codex turn 输入。mode 不是 none 时必须配置非空字符串。
[greeting.group].mode 群聊 conversation 创建后的 greeting 模式:nonetextcodex_turn。默认是 none
[greeting.group].message 群聊 greeting 文案或 Codex turn 输入。mode 不是 none 时必须配置非空字符串。
[service.launchd].mode macOS launchd 安装位置。gui 使用当前 gui/<uid> LaunchAgent(默认);daemon 使用系统 LaunchDaemon。通常由 twinny install --system-daemon 写入。
[service.launchd].user_name mode = "daemon" 时写入 plist 的 UserName。通常由 twinny install --system-daemon 自动写入当前用户。
[upgrade].channel 自动更新通道,stable 使用 npm latest dist-tag,beta 使用 npm beta dist-tag。默认是 stable
[upgrade].check_interval 自动更新检查间隔,支持 mssmhd 后缀。默认是 1d
[upgrade].auto_update 检测到新版本后是否自动更新。默认是 true。设置为 false 时只向 owner 发送提示消息。
[upgrade].registry 可选 npm registry URL。配置后手动和自动更新的 npm 查询、下载都会带上该 registry。
[profiles.<name>].codex_home 该 profile 使用的 CODEX_HOME。绝对路径会直接使用;相对路径会以 TWINNY_HOME 为基准解析。host 默认是 ~/.codex;其他 profile 未设置时继承 host
[profiles.<name>].default_model 该 profile 新 thread 的默认模型。host 默认是 gpt-5.5;其他 profile 未设置时继承 host
[profiles.<name>].default_effort 该 profile 新 thread 的默认推理强度。常见取值是 minimallowmediumhighxhighhost 默认是 medium;其他 profile 未设置时继承 host
[telemetry].enabled 支持 telemetry 的构建使用的布尔关闭开关。设置为 false 会关闭事件采集。详见 Telemetry

Telemetry 的数据范围和关闭方式见文末的 Telemetry

示例 config.toml

[codex]
binary = "/opt/homebrew/bin/codex"
masquerade_as_codex_cli = false

[lark.reaction]
working = "JubilantRabbit"
queued = "OneSecond"

[lark.redaction]
email = "mask"
chinese_phone_number = "mask"

[permissions]
p2p_default_profile = "none"
p2p_default_workspace = "{{twinny_home}}/workspaces/{{conversation_key}}"
group_default_profile = "none"
group_default_mode = "none"
group_default_workspace = "{{twinny_home}}/workspaces/{{conversation_key}}"

[greeting.p2p]
mode = "none"
message = ""

[greeting.group]
mode = "none"
message = ""

[upgrade]
channel = "stable"
check_interval = "1d"
auto_update = true

[profiles.host]
codex_home = "~/.codex"
default_model = "gpt-5.5"
default_effort = "medium"

[profiles.guest]
codex_home = "./profiles/guest-codex"
default_model = "gpt-5.5"
default_effort = "medium"

相对路径形式的 codex_home 会以 TWINNY_HOME 为基准解析。每个 profile 会启动独立的 Codex app-server 进程,并把 CODEX_HOME 设置为该 profile 的 codex_home

修改 profile 配置后,可以在 Lark 中发送 /reload [profile],或者重启 daemon。

自动更新

Twinny 只在当前运行版本符合 a.b.ca.b.c-数字时间字符串 时启用自动更新;本地开发版 dev 等其他格式会禁用自动更新,手动 /upgrade 也不会执行升级,因为无法可靠判断“只升级到更新版本”。

版本比较先看 major.minor.patch;同一个基础版本下,无后缀版本高于有后缀版本;两个版本都有数字时间后缀时,后缀数值更大的版本更高。

自动更新会先查询 npm dist-tag,再把目标版本下载到 runtime/upgrade 临时目录。真正替换由 detached Node helper 完成:停止当前 daemon、备份整个 sqlite 目录、备份并替换 runner、启动新 runner,并在 2 分钟内等待新版本写入运行状态。超时或失败时 helper 会恢复 sqlite 目录和旧 runner,再启动旧版本。sqlite 备份会包含 -wal-shm 文件;daemon 已停机后 WAL 里仍可能有尚未 checkpoint 的已提交事务,复制整个 sqlite 目录是最稳妥的做法。

macOS LaunchAgent、macOS LaunchDaemon、systemd user service 和 Windows Task Scheduler 都通过 detached helper 调度。LaunchDaemon 场景下 helper 会用哨兵文件和当前进程信号配合 launchd 的 KeepAlive,避免依赖交互式 sudo。前台 twinny run 没有托管 supervisor,不支持自动替换重启。

通过 TWINNY_HOME 多实例部署

给每个实例指定独立 home,就可以运行多个隔离的 Twinny 实例:

TWINNY_HOME="$HOME/.twinny-work" npx twinny@latest install
TWINNY_HOME="$HOME/.twinny-personal" npx twinny@latest install

TWINNY_HOME="$HOME/.twinny-work" npx twinny@latest status
TWINNY_HOME="$HOME/.twinny-personal" npx twinny@latest logs

每个 home 都有独立的 config,并且需要一个独立的飞书机器人应用。

Telemetry

启用 telemetry 的 Twinny 构建可能会发送匿名、best-effort 的使用和可靠性事件。这些数据用于监测产品质量、理解失败模式,并支持维护者围绕本地 agent 工作流的个人研究兴趣。

Twinny 不会收集或上传对话内容和凭据。这包括 Lark 消息正文、prompt、Codex 回答、飞书/Lark app secret 或 token、Codex 凭据或 session token、群名、发送者名称、原始 Lark 或 Codex ID、本地路径、环境变量值、API key 以及其他 secret。install、conversation、thread、turn、sender、message、Codex binary 等标识会先在本地加 salt 并 hash,再上传 hash 后的值。

Telemetry 可能包含:

  • 安装和启动生命周期状态、启动耗时、托管服务配置状态;
  • 运行时健康信号,例如 heartbeat、uptime、队列和 active turn 数量、内存使用量、Lark/Codex ready 状态;
  • 消息路由元数据,例如 conversation type、message 或 action type、route kind、queue depth、resource count;
  • turn 元数据,例如状态、类型、模型、reasoning effort、token 数、耗时、生成图片数量,以及失败时的 error code/category;
  • 环境元数据,例如 Twinny、Codex、Node、OS version/platform/arch、Lark brand、profile count。

Telemetry 上报失败不会影响主流程,不应阻断安装、启动、消息处理或 Codex turn。

可以在 config.toml 中关闭 telemetry:

[telemetry]
enabled = false

也可以用环境变量关闭当前进程:

TWINNY_TELEMETRY_ENABLED=false npx twinny@latest start

License

MIT

About

A Bridge between Feishu/Lark and Codex

Resources

License

Stars

Watchers

Forks

Contributors