- macOS、Linux、WSL2 或 Windows。installer 在 macOS 上管理 LaunchAgent(或
--system-daemonLaunchDaemon),在 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,让它按指南完成 agent 安装:
Read https://raw.githubusercontent.com/hachiwii/twinny/master/agent_installation_guide.md and follow instructions in it.
通过 npx 运行交互式 installer:
npx twinny@latest installmacOS 默认安装为当前 GUI session 的 LaunchAgent。如果在 SSH、CI 或其他没有 GUI LaunchAgent 的环境中安装,installer 会退出并提示改用 system daemon 模式:
npx twinny@latest install --system-daemon--system-daemon 会通过 sudo 把 plist 写入 /Library/LaunchDaemons,并用当前用户写入 UserName;后续 start、stop、restart、status 会根据 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.json 的 lark_app_secret 字段。macOS 安装默认使用系统 Keychain;如果加 --disable-keychain 或 Keychain 写入失败,也会写入 auth.json。启动时优先读取 auth.json,然后才回退到 TWINNY_LARK_APP_SECRET 和旧 secret store(macOS Keychain,其他平台 runtime/secrets.json)。
你需要在飞书/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_at 或 all_at,基础的 im:message.group_at_msg:readonly 已足够。安装指引页面的导入 JSON 会预填这个推荐权限,便于后续切换群响应模式。
如果启用自动激活 greeting(私聊需 permissions.p2p_default_profile != "none" 且 greeting.p2p.mode != "none";群聊需 group_default_profile 与 group_default_mode 都不是 none 且 greeting.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_at或owner,不要直接使用all或all_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 = null,version 从启动时运行的 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.com;whitespace 插入空格,例如 alice @ example.com;none 发送明文邮箱。飞书可能会拦截包含明文邮箱或手机号的 bot message。默认是 mask。 |
[lark.redaction].chinese_phone_number |
发往 Lark 的 payload 中中国手机号的脱敏策略。mask 保留前 3 位和后 4 位,例如 138****5678;whitespace 插入空格,例如 138 1234 5678;none 发送明文手机号。飞书可能会拦截包含明文邮箱或手机号的 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_at、owner、all_at、all 或 none。当它和 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 模式:none、text 或 codex_turn。text 会直接向单聊发送固定文案;codex_turn 会把 message 作为 Codex turn 输入并向单聊发送 turn card。默认是 none。 |
[greeting.p2p].message |
P2P greeting 文案或 Codex turn 输入。mode 不是 none 时必须配置非空字符串。 |
[greeting.group].mode |
群聊 conversation 创建后的 greeting 模式:none、text 或 codex_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 |
自动更新检查间隔,支持 ms、s、m、h、d 后缀。默认是 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 的默认推理强度。常见取值是 minimal、low、medium、high 和 xhigh;host 默认是 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.c 或 a.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,不支持自动替换重启。
给每个实例指定独立 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 的 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 startMIT