@cjhyy/code-shell-chat
可独立安装的 Node.js/Bun 多渠道 Chat Gateway。包本身不依赖 CodeShell runtime:第三方应用可以
直接组合 channel adapter、middleware、白名单和 webhook ingress,并使用自己的业务处理器。
独立使用
bun add @cjhyy/code-shell-chat
import { ChatGateway, createAllowlistMiddleware } from "@cjhyy/code-shell-chat";
import { TelegramAdapter } from "@cjhyy/code-shell-chat/telegram";
const telegram = new TelegramAdapter({ botToken: process.env.TELEGRAM_BOT_TOKEN! });
const chat = new ChatGateway({ adapters: [telegram] });
chat.use(
createAllowlistMiddleware({
telegram: { targetIds: [process.env.TELEGRAM_CHAT_ID!] },
}),
);
chat.use(async ({ message, reply }) => {
await reply({ text: `echo: ${message.text}` });
});
const shutdown = new AbortController();
process.once("SIGINT", () => shutdown.abort());
await chat.run(shutdown.signal);
第三方也可以实现 ChannelAdapter 接入新平台,或用 middleware 接自己的 Agent、Webhook、工单系统与自动化服务。
配置驱动的 host 应使用异步工厂;它只会动态加载实际选择的平台模块:
import { createChannelAdapterAsync } from "@cjhyy/code-shell-chat/factory";
const telegram = await createChannelAdapterAsync({
channel: "telegram",
botToken: process.env.TELEGRAM_BOT_TOKEN!,
apiBaseUrl: "https://api.telegram.org",
allowedTargetIds: [process.env.TELEGRAM_CHAT_ID!],
allowedUserIds: [],
});
旧的同步 createChannelAdapter() 继续兼容,但返回的是首次 run、send 或 webhook 请求时
才加载具体平台的轻量代理。内置 CLI 和 Desktop 已使用异步入口,因此启动阶段会直接报告所选
adapter 的模块加载或构造错误。
当前各平台 SDK 仍是默认依赖:动态加载减少的是未启用平台的模块求值、副作用和启动开销,
不是安装体积。暂不改成 peer/optional dependency,是为了保证标准 bun add @cjhyy/code-shell-chat 和内置 CLI 安装后仍可直接启用任一支持渠道;后续若拆独立 adapter
包,需要同时提供安装检测和明确的按渠道安装指引。
CodeShell integration
包内置了可选的 @cjhyy/code-shell-chat/codeshell integration 和 code-shell-chat CLI。
在 CodeShell Desktop 中可直接进入「凭证 → Link → CodeShell Chat Gateway」查看全部支持渠道、
逐渠道连接状态和接入说明,一键打开可用平台的官方配置后台,启动/停止 gateway、编辑配置,
或扫码连接个人微信;卡片还会显示本次桌面运行中最近的收发消息。无需再单独打开终端运行 CLI。
钉钉渠道提供独立的结构化向导:填写应用凭据后可建立临时 Stream 连接,通过向机器人发送
测试消息自动发现 conversationId 和 senderStaffId,勾选后保存白名单并启动。Desktop 会把
Client Secret 放入 CodeShell 安全凭据库(系统支持时使用系统加密),config.json 只保留
Client ID 和白名单;旧版配置中的
明文 Secret 会在首次使用新向导保存时迁移。独立 CLI 继续支持配置文件或
CODE_SHELL_DINGTALK_CLIENT_SECRET 环境变量。
CLI 会把普通文字和附件转到桌面端的 Mimi Pet 长期会话。Mimi 先通过只读 Gateway 工具发现
本进程已启用的渠道及某一渠道的精确能力,需要回复时再用 GatewayReply 把结果送回原 IM
会话。
不同渠道继续共享 Mimi 的长期上下文,但每条入站消息会标明来源渠道,当前消息来源也会进入
Mimi 的运行时上下文,避免多渠道同时使用时无法分辨入口。
/open、/close、/status 仍作为确定性快捷入口;“帮我打开手机遥控”“关闭公网入口”
和“看看手机遥控状态”等明确自然语言也会触发相同操作。mobile remote 继续复用 tunnel +
pairing + passcode,并把 10 分钟一次性配对入口作为按钮回到原会话。
富媒体纵向链路现在覆盖绝大多数内置渠道。附件在 sender/target 白名单通过后才按需下载,
受数量、单文件、总大小和超时限制;随后由 Electron main 校验并暂存到 Pet 会话的
.code-shell/attachments 目录。Mimi 仅在对应 IM 回合显式调用 GatewayReply 后,由宿主
校验并发送项目、no-repo 或用户 Downloads 目录中的本地图片、文件、音频或视频;任务
产物记录本身不会触发回传。
CLI 不直接拥有 cloudflared 或 mobile WebSocket host。Electron main 仍是生命周期唯一
owner,并通过 ~/.code-shell/im-gateway/desktop-control.json(0600)提供随机 bearer token
保护的 loopback control API。CLI 与 Desktop 使用同一个原子进程锁,不能同时消费平台更新。
支持的渠道
所有渠道可以在同一 gateway 进程中同时启用,每个渠道有独立的会话和用户白名单。
| Telegram | Bot API getUpdates 长轮询 | 不需要 |
| Discord | Gateway + REST | 不需要 |
| Slack | Socket Mode + Web API | 不需要 |
| 飞书 / Lark | 官方 SDK WebSocket 长连接 | 不需要 |
| 钉钉 | Stream Mode | 不需要 |
| 个人微信 | 腾讯 ClawBot iLink + 扫码登录 | 不需要 |
| 企业微信 | 智能机器人 WebSocket 长连接 | 不需要 |
| Matrix | Client-Server API /sync | 不需要 |
| Mattermost | WebSocket events + REST | 不需要 |
| LINE | Messaging API webhook | 需要,/webhooks/line |
| WhatsApp Business | Meta Cloud API webhook | 需要,/webhooks/whatsapp |
| Microsoft Teams | Bot Framework messaging endpoint | 需要,/webhooks/teams |
Gateway 能力矩阵
下表描述的是 CodeShell 当前适配器已经实现并经过测试的能力,不代表上游平台 API 的全部潜在
功能。Gateway 通过 ChannelAdapter.capabilities 把同一份声明用于 QR、附件过滤、主动通知和
Pet 的两层工具;Pet 不再按渠道名猜测能力。
| Telegram | 图片、文件、音频、视频 | 文字、原生按钮 | 图片、文件、音频、视频 | 已知 chat 可回复或主动推送 |
| Discord | 图片、文件、音频、视频 | 文字、原生按钮 | 图片、文件、音频、视频 | 需 Attach Files;已知 channel |
| Slack | 图片、文件、音频、视频 | 文字、原生按钮 | 图片、文件、音频、视频 | 需 files:read/files:write;已知 channel |
| 飞书 / Lark | 图片、文件、音频、视频 | 文字、原生按钮 | 图片、文件、音频、视频 | 需消息资源读取和图片/文件上传权限 |
| 钉钉 | — | 文字、ActionCard | — | Stream 临时 session webhook 不承载本地媒体 |
| 企业微信 | 图片、文件、视频 | 文字、Markdown 链接 | 图片、文件、音频、视频 | 入站语音当前协议只提供转写;出站音频按文件发送 |
| 个人微信 | 图片、文件、音频、视频 | 文字、普通链接 | 图片、文件、音频、视频 | 音频按可播放文件发送;依赖 context_token |
| Matrix | 图片、文件、音频、视频 | 文字、HTML 链接 | 图片、文件、音频、视频 | 当前媒体管线不解密 E2EE 附件 |
| Mattermost | 图片、文件、音频、视频 | 文字、Markdown 链接 | 图片、文件、音频、视频 | 需 bot 文件上传权限 |
| LINE | 图片、文件、音频、视频 | 文字、按钮模板 | — | 出站媒体需另配公网 HTTPS 媒体存储,未虚报为已支持 |
| WhatsApp Business | 图片、文件、音频、视频 | 文字、CTA 按钮 | 图片、文件、音频、视频 | 受 customer-care window 约束 |
| Microsoft Teams | 图片、文件、音频、视频 | 文字、Markdown 链接 | 图片 | Bot Framework 内联图片;其他文件需另接 Graph/SharePoint 流程 |
出站附件统一限制为每次最多 4 个、通常单个最多 10 MiB;Teams 内联图片遵循平台的 1 MiB
上限。适配器遇到未声明的附件类型会明确报错,Gateway 在调用适配器前会按能力过滤
Desktop/Pet 产物。
Pet 使用分层、原子化工具:
- 只读
Gateway:search 只在本回合实际授予的渠道中查询,不带 query 时返回全部渠道名,
也可用 outbound:image、inbound:file、button:native 等条件过滤;describe 再返回一个
匹配渠道的精确入站/出站契约。能力目录每回合从当前适配器集合重建,只含渠道名和能力,
不传递 token、白名单或会话 target。
- 有副作用的
GatewayReply:只绑定当前入站消息的原会话,一次声明完整文字、可选 URL 按钮
和可选本地附件。它不能借第一层目录向其他渠道或其他 target 任意发送。
- 有副作用的
SendMessage:用于桌面 Mimi 主动发往宿主授权的 owner 目标。模型只看到宿主生成
的不透明 target_id,看不到原始平台用户 ID;宿主在执行前重新读取白名单并校验文字、附件与
渠道能力。工具调用只登记请求,Mimi 的原始回复会被宿主的实际“平台已接受”或失败回执替换;
平台接受仍不等于收件人设备已经展示消息。
普通纯文字回复可以直接进入第二层;富媒体能力不确定、或用户询问另一个已启用渠道时,Mimi
先查询第一层。Gateway 按当前路由动态移除 GatewayReply 不支持的附件参数,并在真正调用
适配器前再次校验。
工具文字最多 8000 字,Gateway 使用 1800 字的跨渠道安全分片发送,按钮和附件只跟随最后一片;
中途失败时从未发送的分片续传,不会重新运行 Pet 或重复已经送达的前半段。需要多次
平台 API 调用的文字+媒体组合也会记录已完成子步骤,同一进程内重试时从失败子项继续。
个人微信使用腾讯 ClawBot 公开的 iLink Bot 协议,不接逆向 Web 协议。首次使用先执行:
code-shell-chat wechat login
终端会显示二维码,用手机微信扫码并确认。token 保存在 owner-only 凭据文件中,不写入
gateway 配置。登录命令会自动增加 wechat.accountId,扫码者默认成为唯一允许的用户。
@cjhyy/code-shell-chat/wechat 同时导出 loginWechatWithQr、WechatAdapter、
sendWechatDirect 和 FileWechatCredentialStore,第三方应用可以完全不使用 CodeShell CLI。
sendWechatDirect 是供定时任务和通知 worker 使用的独立入口:同一进程里若已有相同账号与
凭据的长轮询 adapter,它会排队复用该实例;否则才创建一次性 adapter。同一账号只允许
一个 live adapter;所有 adapter 实例的发送、限流冷却和生命周期请求共用账号级串行边界。
运行中若更换 token,必须先重启 Gateway,不会用新旧凭据打开竞争会话。CodeShell Desktop 的
扫码流程也把游标和上下文视为凭据会话状态:token 或服务地址变化时清空旧状态;凭据未变时保留
有效状态,而一次用户主动发起且平台确认成功的登录(含“已绑定”确认)可以修复损坏或不安全的
状态文件。普通启动仍然失败关闭。
Desktop 宿主包装还会在整个直发期间持有与 CLI 共用的 Gateway 单实例锁;若锁已由 CLI 持有,通知会
留给 CLI 的事件 watcher,不会再开一条短连接。这样可避免并行连接得到 HTTP 成功却没有在微信
客户端显示。返回值区分
status: "accepted"、terminalDeliveryConfirmed: false 和 viaLiveAdapter,调用方不得把它
渲染成“已送达”。
import { FileWechatCredentialStore, sendWechatDirect } from "@cjhyy/code-shell-chat/wechat";
const store = new FileWechatCredentialStore();
const credentials = store.load("my-account");
if (!credentials?.userId) throw new Error("请先完成微信扫码登录");
const receipt = await sendWechatDirect(
credentials,
credentials.userId,
{ text: "这是一条定时提醒" },
{ stateStore: store.stateStore(credentials.accountId) },
);
这层路由直接借鉴 Hermes 的
send_weixin_direct:
先按凭据查找同事件循环里的 live adapter,找不到时才创建短生命周期 adapter,并让两条路径复用
同一套上下文、媒体上传和响应校验。CodeShell 把这个做法收敛为渠道 adapter 的 send() 能力和
宿主单实例锁,而不是让定时任务直接依赖微信模块。
个人微信的公开协议契约仍以 context_token 为主。adapter 会持久保存每个 owner 最近一次入站
消息带来的 token;遇到明确的过期响应时清除它,并用同一个 client_id 做一次无 token 的兼容
尝试,但该降级并非所有账号都支持。CodeShell Desktop 因此只在 owner 先给 Mimi 发过一条消息、
且状态中存在可用 token 时向 SendMessage 暴露微信目标;若返回 prepare failed,界面会要求
先从微信发一条新消息刷新上下文,而不会继续宣称推送成功。
入站长轮询只在上层持久接收消息后才提交 get_updates_buf,临时处理失败会保留旧游标重投。
媒体下载只允许已知微信 CDN 的 HTTPS 主机,并逐跳重新校验重定向。
桌面任务和定时通知只发布渠道无关的事件;Gateway 停止时,宿主会用同一套独立直发能力处理
支持 direct 的目标,Gateway 在线时则由事件 watcher 独占发送。两者共享事件进度,启动交接不会
重复发送;独立直发完整成功时可直接确认当前队首,不必等 watcher 以后上线才释放容量。进度文件在事件游标旁保留最近
200 个事件、每个目标的文本分段和附件子步骤;文件使用 owner-only 权限和原子替换,
符号链接、过大或损坏状态会失败关闭,不能伪造“已投递”进度。单个
渠道失败或 Gateway 正常重启时只续传未完成部分。传输已受理但未来得及落盘就崩溃的窗口仍是
「至少一次」,不会伪称精确一次。
控制事件本身也会在对 watcher 可见前,先原子写入 owner-only outbox(最多 200 条未确认事件)。
Desktop 重启后恢复同一 streamId 和递增事件 ID,所以 Gateway 会从已确认游标继续,而不是
把重启前尚未接收的通知遗留在内存里。落盘失败时发布直接失败,不会暴露半成功事件。
watcher 在下一次请求带回持久游标后,Desktop 才原子删除已确认前缀;未确认队列满时施加背压并拒绝
新发布,不会静默挤掉旧通知。如果恢复后发现本地游标高于 outbox,协议会显式要求 watcher 重置,
且 checkpoint 必须先写盘成功,内存游标才能前进。
每个 live watcher 事件在真正调用 adapter 前还会重读当前配置验证 target 白名单;用户已撤销的目标会被安全跳过,
不会因为 Gateway 启动时的旧快照继续发送。
长任务 closure、Work Session 主动上报和定时任务终态都会携带匿名稳定 delivery key;Desktop
控制事件若在上游恢复时被重新发布,仍会接续原目标/分段/附件进度,而不是把它当成全新通知。
定时任务的完成、失败、永久停用和用户取消分别发布 automation.completed、
automation.failed、automation.stopped 和 automation.cancelled;开始事件不外发,避免每次
执行产生两条通知。
协议实现对齐
Tencent/openclaw-weixin 2.4.x 的公开文档。
CodeShell CLI 配置
创建 ~/.code-shell/im-gateway/config.json 并设置为 0600。只需保留实际启用的平台:
Desktop 的「编辑配置」会生成包含所有渠道的模板;在模板里把要使用的渠道设为
"enabled": true 并填写凭据和白名单即可,未启用的占位配置不会加载。
{
"telegram": {
"botToken": "123456:replace-me",
"allowedChatIds": ["123456789"],
"allowedUserIds": ["123456789"]
},
"discord": {
"botToken": "replace-me",
"allowedChannelIds": ["channel-id"],
"allowedUserIds": ["user-id"]
},
"slack": {
"botToken": "xoxb-replace-me",
"appToken": "xapp-replace-me",
"allowedChannelIds": ["C0123456789"],
"allowedUserIds": ["U0123456789"]
},
"lark": {
"appId": "cli_replace_me",
"appSecret": "replace-me",
"allowedChatIds": ["oc_chat_id"],
"allowedUserIds": ["ou_open_id"]
},
"dingtalk": {
"clientId": "replace-me",
"clientSecret": "replace-me",
"allowedConversationIds": ["cid_replace_me"],
"allowedUserIds": ["staff-id"]
},
"wecom": {
"botId": "replace-me",
"secret": "replace-me",
"allowedChatIds": ["chat-id-or-user-id"],
"allowedUserIds": ["user-id"]
},
"wechat": {
"accountId": "generated-by-wechat-login"
},
"matrix": {
"homeserverUrl": "https://matrix.example.com",
"accessToken": "replace-me",
"botUserId": "@codeshell:example.com",
"allowedRoomIds": ["!room:example.com"],
"allowedUserIds": ["@owner:example.com"]
},
"mattermost": {
"serverUrl": "https://mattermost.example.com",
"botToken": "replace-me",
"botUserId": "bot-user-id",
"allowedChannelIds": ["channel-id"],
"allowedUserIds": ["owner-user-id"]
},
"line": {
"channelSecret": "replace-me",
"channelAccessToken": "replace-me",
"allowedTargetIds": ["group-room-or-user-id"],
"allowedUserIds": ["owner-user-id"]
},
"whatsapp": {
"accessToken": "replace-me",
"appSecret": "replace-me",
"verifyToken": "choose-a-random-value",
"phoneNumberId": "replace-me",
"apiVersion": "v25.0",
"allowedPhoneNumbers": ["8613800000000"]
},
"teams": {
"appId": "replace-me",
"appPassword": "replace-me",
"appType": "MultiTenant",
"allowedConversationIds": ["conversation-id"],
"allowedUserIds": ["aad-or-channel-user-id"]
},
"webhook": {
"host": "127.0.0.1",
"port": 8787,
"maxBodyBytes": 1048576
},
"runtime": {
"maxPending": 1000,
"maxConcurrent": 4,
"maxPerTarget": 1,
"maxMessagesPerUserPerMinute": 20
},
"notifications": {
"enabled": true,
"targets": {
"telegram": ["123456789"]
}
}
}
每个已启用渠道都必须配置 target 白名单;allowedUserIds 可选,但群聊强烈建议配置。
未知会话和用户会被静默忽略。配置文件包含 bot secret、允许远程拉起桌面端的命令,非 Windows
系统只接受 0600 权限。
可序列化的文字入站会先原子写入 ~/.code-shell/im-gateway/inbox.json,webhook 平台随后才会
收到确认;同一账号的相同平台 message ID 会持久去重。处理采用有界队列、全局并发和 per-target
串行,暂时失败按指数退避重试。附件字节先写入 inbox 旁的 spool 再确认;spool 本地写入失败时
拒绝确认,交由平台重投。超出 spool 上限或下载失败的媒体保留在内存队列,仍受平台重投策略约束。
notifications.targets 最多 32 个,只能引用已启用渠道且必须同时位于该渠道 target 白名单。Desktop 主动
通知游标会按事件流实例原子写入 ~/.code-shell/im-gateway/desktop-events.json(可用
runtime.eventCursorPath 覆盖);Gateway 和 Desktop 正常重启后都会续传。只有 Desktop 初始化了全新的
事件 outbox 时,watcher 才会识别新 streamId 并安全重置游标。
所有字段都有 CODE_SHELL_<CHANNEL>_* 环境变量对应项;数组使用逗号分隔。例如:
export CODE_SHELL_DISCORD_BOT_TOKEN='replace-me'
export CODE_SHELL_DISCORD_ALLOWED_CHANNEL_IDS='channel-1,channel-2'
export CODE_SHELL_DISCORD_ALLOWED_USER_IDS='owner-user-id'
也可用 CODE_SHELL_IM_GATEWAY_CONFIG 指定配置路径。完整环境变量名以仓库中的
packages/chat/src/config.ts
为准。
Webhook 渠道
LINE、WhatsApp、Teams 要求一个固定公网 HTTPS origin。gateway 默认仅监听
127.0.0.1:8787,应由同机的反向代理或固定隧道暴露上表中的 path。手机遥控使用的
Cloudflare quick tunnel 指向 mobile host、URL 也会变化,不能当作这些平台的 webhook origin。
- LINE POST 请求校验
x-line-signature;
- WhatsApp 支持 GET challenge,并用 app secret 校验
x-hub-signature-256;
- Teams 使用官方 Bot Framework SDK 校验 bearer JWT 和 channel identity;
- request body 默认最多 1 MiB,且 webhook server 有请求超时。
本地探针为 GET /healthz 与 GET /readyz;探针不会由生成的公网 ingress 暴露。生成固定
HTTPS 反向代理配置:
code-shell-chat ingress print --host chat.example.com --format caddy
平台侧最低要求
- Discord 需启用 Message Content intent;gateway 启动时会补注册
/open、/close、/status
application commands;
- Slack app 需启用 Socket Mode、订阅 message events、注册三个同名 slash commands,并给 bot
chat:write;
- 飞书需开启机器人能力、订阅
im.message.receive_v1,选择长连接接收事件;
- 钉钉应用机器人选择 Stream 模式;
- 企业微信使用智能机器人长连接的 bot ID + secret;
- 个人微信需先运行
code-shell-chat wechat login,支持二维码确认和手机数字验证;
- Telegram 不得同时配置 webhook,因为 webhook 与
getUpdates 互斥;
- WhatsApp 自由格式回复受 Cloud API 的 customer-care window 约束。
运行
桌面端需要先设置 mobile remote 访问口令。macOS 默认用 open -a code-shell 拉起离线桌面端;
其他安装方式可覆盖 desktop.command 和 desktop.args。
bun run --filter '@cjhyy/code-shell-chat' build
node packages/chat/dist/cli.js
也可以直接开发运行:
bun run --filter '@cjhyy/code-shell-chat' dev
安装为当前用户的常驻服务(macOS launchd、Linux systemd user、Windows Task Scheduler):
code-shell-chat service install
code-shell-chat service status
code-shell-chat service uninstall
服务定义不写入 secret,只引用 owner-only 配置;运行时单实例锁会阻止 Desktop 与 daemon 双开。
真实平台发布前运行凭据 canary。命令会生成随机 nonce;在每个启用 channel 的白名单会话中发送
提示的 /canary <nonce>,只有真实入站、验签/鉴权、allowlist 和真实回发全部成功才会通过:
code-shell-chat canary --timeout-ms 600000