diff --git a/adapters/common/__tests__/format.test.ts b/adapters/common/__tests__/format.test.ts index 25dc360a..943ee6bb 100644 --- a/adapters/common/__tests__/format.test.ts +++ b/adapters/common/__tests__/format.test.ts @@ -110,6 +110,8 @@ describe('formatImHelp', () => { expect(text).toContain('/clear') expect(text).toContain('/stop') expect(text).toContain('/help') + expect(text).toContain('项目列表') + expect(text).toContain('/allow ') }) }) diff --git a/adapters/common/format.ts b/adapters/common/format.ts index 3002e6da..09e62349 100644 --- a/adapters/common/format.ts +++ b/adapters/common/format.ts @@ -26,12 +26,13 @@ type ImStatusSummary = { } const IM_HELP_LINES = [ - '/new [项目] — 新建会话或切换项目', - '/projects — 查看最近项目', - '/status — 查看当前会话状态', - '/clear — 清空当前会话上下文', - '/stop — 停止当前生成', - '/help — 显示这份帮助', + '/new [项目] / 新会话 — 新建会话或切换项目', + '/projects / 项目列表 — 查看最近项目', + '/status / 状态 — 查看当前会话状态', + '/clear / 清空 — 清空当前会话上下文', + '/stop / 停止 — 停止当前生成', + '/help / 帮助 — 显示这份帮助', + '权限审批:/allow 、/always 、/deny ', ] /** Split text into chunks that fit within a character limit, respecting paragraph/sentence boundaries. */ diff --git a/adapters/dingtalk/index.ts b/adapters/dingtalk/index.ts index b6637c11..7e73bf3b 100644 --- a/adapters/dingtalk/index.ts +++ b/adapters/dingtalk/index.ts @@ -567,7 +567,7 @@ async function handleRobotMessage(data: DingTalkRobotMessage): Promise { await sendText( chatId, success - ? '✅ 配对成功!现在可以开始聊天了。\n\n发送消息即可与 Claude 对话。' + ? '✅ 配对成功!现在可以开始聊天了。\n\n发送消息即可与 Claude 对话。发送 /help 查看可用命令。' : '🔒 未授权。请先在 Claude Code 桌面端完成钉钉扫码绑定,再生成 IM 配对码后发送给我。', ) return diff --git a/adapters/wechat/index.ts b/adapters/wechat/index.ts index 1f7d99d4..b48d38ef 100644 --- a/adapters/wechat/index.ts +++ b/adapters/wechat/index.ts @@ -456,7 +456,7 @@ async function routeUserMessage(message: WechatMessage): Promise { await sendText( chatId, success - ? '配对成功!现在可以开始聊天了。\n\n发送消息即可与 Claude 对话。' + ? '配对成功!现在可以开始聊天了。\n\n发送消息即可与 Claude 对话。发送 /help 查看可用命令。' : '未授权。请先在 Claude Code 桌面端完成微信扫码绑定,再生成 IM 配对码后发送给我。', ) return diff --git a/docs/im/dingtalk.md b/docs/im/dingtalk.md index 2caed8a1..0fa0f92e 100644 --- a/docs/im/dingtalk.md +++ b/docs/im/dingtalk.md @@ -1,39 +1,106 @@ # 钉钉接入 -钉钉接入走 DingTalk Stream,不需要公网回调地址。桌面端设置页支持扫码创建并授权机器人,授权成功后会把 `clientId / clientSecret` 写入 `~/.claude/adapters.json`。 +> 钉钉 Adapter 的接入教程。钉钉走 DingTalk Stream,不需要公网回调地址。 -## 配置方式 +## 适用场景 -打开 Desktop Webapp 的 `Settings -> IM 接入 -> 钉钉`。 +钉钉方案适合通过钉钉单聊远程驱动本机 Claude Code。当前实现只处理单聊消息,不处理群聊。 -推荐方式: +当前实现支持文本聊天、图片附件、项目选择、状态查看、停止生成、AI Card 流式输出,以及文本或卡片形式的权限审批。 -1. 点击“扫码绑定” -2. 使用钉钉手机 App 扫码 -3. 在钉钉里确认创建机器人 -4. 等待桌面端显示已绑定 +实现入口:`adapters/dingtalk/index.ts` + +## 1. 绑定钉钉机器人 + +打开桌面端 `设置 -> IM 接入 -> 钉钉`。 + +推荐使用扫码绑定: + +1. 点击「扫码绑定」。 +2. 使用钉钉手机 App 扫码。 +3. 在钉钉里确认创建并授权机器人。 +4. 等待桌面端显示「钉钉机器人已绑定」。 +5. 点击保存,或确认当前配置已经写入本地。 + +授权成功后,桌面端会把 `clientId` 和 `clientSecret` 写入 `~/.claude/adapters.json` 的 `dingtalk` 配置。发布版桌面端会重启 adapter sidecar,让新凭据立即生效。 + +![钉钉机器人绑定和手动凭据配置](../images/im/dingding/dingding01.png) + +## 2. 手动填写凭据 如果扫码不可用,也可以手动填写: -- `Client ID` -- `Client Secret` -- 可选 `Stream Endpoint` -- 可选 `Allowed Users` -- 可选 `权限卡片模板 ID` +- `Client ID` — 钉钉应用的 `appKey` +- `Client Secret` — 钉钉应用的 `appSecret` +- `Stream Endpoint` — 可选,默认 `https://api.dingtalk.com` +- `权限卡片模板 ID` — 可选,配置后权限请求优先使用钉钉互动卡片 +- `Allowed Users` — 可选,直接放行指定钉钉用户 ID -## 配对用户 +保存后 adapter 会用 DingTalk Stream 建立长连接。 -钉钉机器人绑定完成后,还需要让具体用户通过 IM 配对码授权: +## 3. 授权具体钉钉用户 -1. 在 `配对管理` 里生成配对码 -2. 在钉钉私聊里把配对码发给机器人 -3. 显示配对成功后即可开始聊天 +机器人绑定完成后,还需要让具体用户通过配对码授权: -配对用户会出现在“已配对用户”列表里,可以随时解绑。解绑后该用户需要重新发送新的配对码才能使用。 +1. 在 `设置 -> IM 接入` 顶部的「配对管理」里点击「生成配对码」。 +2. 在钉钉机器人单聊里发送这枚 6 位配对码。 +3. 看到配对成功提示后,就可以开始聊天。 -## 运行 adapter +![钉钉里发送配对码并开始对话](../images/im/dingding/dingding02.png) -桌面端发布版会通过 sidecar 自动启动。开发时可以手动运行: +配对码有效期 60 分钟,一次性使用。配对用户会出现在「已配对用户」列表里,可以随时解绑。解绑后该用户需要重新发送新的配对码才能使用。 + +## 4. 选择项目并开始对话 + +如果已经配置默认项目,发送任意消息会直接在该目录下创建或复用 Claude Code session。 + +如果没有配置默认项目,adapter 会返回最近项目列表。回复编号、项目名或绝对路径后,会新建会话并绑定到当前钉钉 chat。 + +后续消息会复用 `~/.claude/adapter-sessions.json` 里的 chat 到 session 映射。发送 `/new` 可以重新选择项目并开启新会话。 + +## 支持的命令 + +钉钉这里没有像飞书那样的机器人菜单配置流程;当前命令入口就是单聊输入框。配对成功后,bot 会提示发送 `/help`;用户也可以随时发送 `/help` 或 `帮助` 查看完整命令列表。 + +- `/help` 或 `帮助` — 显示可用命令 +- `/status` 或 `状态` — 查看当前会话的项目、分支、模型、运行状态和任务摘要 +- `/projects` 或 `项目列表` — 重新显示最近项目列表 +- `/new` 或 `新会话` — 清空当前 chat 绑定的 session,并重新选择项目 +- `/new <编号、项目名或绝对路径>` — 直接在指定项目下新建会话 +- `/clear` 或 `清空` — 清空当前会话上下文,保留项目绑定 +- `/stop` 或 `停止` — 向当前 session 发送 `stop_generation` + +## 权限审批 + +默认情况下,钉钉 adapter 会发送文本审批消息。回复: + +- `/allow ` — 允许一次权限请求 +- `/always ` — 永久允许同类权限请求 +- `/deny ` — 拒绝一次权限请求 + +如果配置了已发布的钉钉互动卡片模板 ID,adapter 会优先发送权限卡片,并通过 DingTalk Stream 的 `/v1.0/card/instances/callback` 接收按钮回调。卡片发送失败或不可见时,仍可使用文本命令审批。 + +## 返回消息的表现 + +- 使用 `dingtalk-stream` 接收机器人单聊消息。 +- 使用消息里的 `sessionWebhook` 回复 Markdown 文本。 +- 普通回复优先使用 AI Card 做流式输出。 +- 权限审批可以使用互动卡片;没有模板时自动回退文本命令。 +- 图片附件会下载后以内联图片形式进入模型输入。 +- 附件会走公共大小限制检查,超限时 adapter 会在钉钉里返回提示。 + +## 解绑 + +有两种解绑: + +- 解绑钉钉机器人账号:在钉钉标签页点击「解绑机器人账号」,会清空 `dingtalk.clientId`、`dingtalk.clientSecret`、`allowedUsers`、`pairedUsers` 和权限卡片模板 ID。 +- 解绑某个用户:在「已配对用户」列表里点击对应钉钉用户右侧的「解绑」,只移除该用户的 `pairedUsers` 记录。 + +解绑机器人账号后需要重新扫码或手动填写凭据。解绑用户后,该用户需要重新发送新的配对码才能继续使用。 + +## 本地开发启动 + +发布版桌面端会自动启动 adapter sidecar。只有本地开发或单独调试时才需要手动运行: ```bash cd adapters @@ -41,13 +108,64 @@ bun install bun run dingtalk ``` -## 当前行为 +可选环境变量: -- 只处理钉钉单聊消息 -- 使用 `sessionWebhook` 回复文本 / Markdown -- 支持用户图片附件进入模型输入 -- 支持 AI Card 流式输出和 thinking / 输入状态展示 -- 支持 `/new`、`/projects`、`/status`、`/clear`、`/stop`、`/help` -- 权限确认支持 `/allow `、`/always `、`/deny ` 文本回复 -- 如果配置了已发布的钉钉互动卡片模板 ID,会优先发送权限卡片,并通过 DingTalk Stream 的 `/v1.0/card/instances/callback` 接收按钮回调;卡片发送失败时自动回退到文本命令 -- 群聊后续再扩展 +```bash +export DINGTALK_CLIENT_ID="..." +export DINGTALK_CLIENT_SECRET="..." +export DINGTALK_STREAM_ENDPOINT="https://api.dingtalk.com" +export DINGTALK_PERMISSION_CARD_TEMPLATE_ID="..." +export ADAPTER_SERVER_URL="ws://127.0.0.1:3456" +``` + +正常桌面端使用不需要手动设置这些环境变量,扫码绑定或手动填写会写入本地配置。 + +## 常见问题 + +### 扫码成功后发消息仍提示未授权 + +这是正常授权流程的一部分。扫码绑定只写入机器人凭据;具体钉钉用户还需要发送配对码,或被加入 `Allowed Users`。 + +### 扫码失败或一直等待确认 + +重新点击「扫码绑定」生成新二维码。如果仍失败,可以手动填写 `Client ID` 和 `Client Secret`。 + +### adapter 启动时报缺少 clientId / clientSecret + +说明 `DINGTALK_CLIENT_ID / DINGTALK_CLIENT_SECRET` 和 `~/.claude/adapters.json` 里的 `dingtalk.clientId / dingtalk.clientSecret` 都没有生效。先在桌面端钉钉标签页完成扫码绑定或手动填写凭据。 + +### 权限卡片没有出现 + +优先检查: + +- 是否已经配置发布后的权限卡片模板 ID。 +- 模板是否支持 adapter 使用的回调路由。 +- 当前机器人是否已经发布新版本。 + +即使卡片不可见,也可以用 `/allow `、`/always `、`/deny ` 完成审批。 + +### 收不到回复 + +优先检查: + +- 桌面端是否正在运行。 +- 钉钉标签页是否显示已绑定。 +- 当前用户是否已经配对或在 `Allowed Users` 中。 +- `~/.claude/adapters.json` 是否能正常写入。 +- 本地开发时 `ADAPTER_SERVER_URL` 是否指向正在运行的 Desktop server WebSocket 地址。 + +### 会话没恢复 + +检查 `~/.claude/adapter-sessions.json` 是否能正常写入,以及 Desktop server 里的 session 是否仍存在。 + +## 源码入口 + +- `adapters/dingtalk/index.ts` +- `adapters/dingtalk/helpers.ts` +- `adapters/dingtalk/ai-card.ts` +- `adapters/dingtalk/permission-card.ts` +- `adapters/dingtalk/media.ts` +- `adapters/common/pairing.ts` +- `adapters/common/session-store.ts` +- `adapters/common/ws-bridge.ts` +- `adapters/common/http-client.ts` diff --git a/docs/im/index.md b/docs/im/index.md index 26dd09a8..4ef0d512 100644 --- a/docs/im/index.md +++ b/docs/im/index.md @@ -70,11 +70,13 @@ flowchart TD - 码有效期 60 分钟 - 配对成功后立即失效 -配对码是平台无关的,同一个码可以在钉钉、Telegram 或飞书私聊里使用一次。微信不使用配对码;在微信标签页扫码并确认后就会把扫码用户绑定到本机。 +配对码是平台无关的,同一个码可以在微信、钉钉、Telegram 或飞书私聊里使用一次。 + +微信和钉钉的“扫码绑定”只负责把机器人账号凭据写到本机;具体 IM 用户仍然需要发送配对码,或被加入 `allowedUsers`。 ### 3. 启动对应 Adapter 进程 -Webapp 负责配置和配对,不负责代你启动 bot 进程。当前仍需要手动启动: +发布版桌面端会在本地 server 启动后自动拉起 adapter sidecar,并在保存凭据、扫码绑定或解绑后重启 adapter 让新配置生效。本地开发或单独调试 adapter 时可以手动启动: ```bash cd adapters diff --git a/docs/im/wechat.md b/docs/im/wechat.md index e2cf3550..2dabe031 100644 --- a/docs/im/wechat.md +++ b/docs/im/wechat.md @@ -1,48 +1,104 @@ # 微信接入 -> 微信 Adapter 的接入教程。微信不需要手动填写 Bot Token,在桌面端扫码确认后即可绑定。 +> 微信 Adapter 的接入教程。桌面端扫码绑定机器人账号后,再用配对码授权具体微信用户。 ## 适用场景 -微信方案适合个人私聊远程使用。当前实现支持文本聊天、图片附件理解、项目选择、状态查看、停止生成和权限审批。 +微信方案适合个人私聊远程使用。当前实现支持文本聊天、语音转文字内容、图片和文件附件、项目选择、状态查看、停止生成和权限审批。 -## 1. 在桌面端扫码绑定 +当前只按私聊用户维度授权和保存会话,不面向微信群聊设计。 -打开桌面端 `设置 -> IM 接入 -> 微信`: +实现入口:`adapters/wechat/index.ts` -1. 点击「扫码绑定」 -2. 使用微信扫描页面里的二维码 -3. 在微信中确认 -4. 页面显示绑定成功后,桌面端会重启 IM adapter sidecar +## 1. 扫码绑定微信机器人账号 -扫码成功后,桌面端会把微信返回的账号凭据写入 `~/.claude/adapters.json` 的 `wechat` 配置,并把扫码用户加入 `pairedUsers`。 +打开桌面端 `设置 -> IM 接入 -> 微信`。 -## 2. 发送消息 +在微信标签页里: -绑定后,直接在微信里发送自然语言消息即可。 +1. 点击「扫码绑定」。 +2. 使用微信扫描页面里的二维码。 +3. 在微信里确认登录。 +4. 等待页面显示绑定成功。 +5. 点击保存,或确认当前配置已经写入本地。 -如果没有配置默认项目,Adapter 会先返回最近项目列表,回复编号后再开始会话。 +![微信未绑定时的扫码入口](../images/im/wechat/wechat01.png) + +扫码成功后,桌面端会把微信网关返回的 `accountId`、`botToken`、`baseUrl` 和 `userId` 写入 `~/.claude/adapters.json` 的 `wechat` 配置。发布版桌面端会重启 adapter sidecar,让新凭据立即生效。 + +绑定成功后,微信标签页会显示「微信已绑定」,并提供「重新扫码」和「解除微信绑定」: + +![微信绑定成功状态](../images/im/wechat/wechat03.png) + +注意:扫码绑定的是机器人账号凭据,不等于把所有微信用户都授权了。用户访问仍然走 `allowedUsers + pairedUsers` 授权模型。 + +## 2. 授权具体微信用户 + +推荐走配对码: + +1. 在 `设置 -> IM 接入` 顶部的「配对管理」里点击「生成配对码」。 +2. 把 6 位配对码发给微信里的机器人。 +3. 看到配对成功提示后,就可以直接发送自然语言消息。 + +![微信里发送配对码并开始对话](../images/im/wechat/wechat02.jpg) + +配对码有效期 60 分钟,一次性使用。重新生成配对码后,旧码会失效。 + +也可以把已知微信用户 ID 手动写进微信标签页的 `Allowed Users`。这种方式适合固定账号白名单,但一般没有配对码直观。 + +## 3. 选择项目并开始对话 + +如果已经配置默认项目,发送任意消息会直接在该目录下创建或复用 Claude Code session。 + +如果没有配置默认项目,微信 adapter 会返回最近项目列表。回复编号、项目名或绝对路径后,会新建会话并绑定到当前微信用户。 + +后续消息会复用 `~/.claude/adapter-sessions.json` 里的 chat 到 session 映射。发送 `/new` 可以重新选择项目并开启新会话。 ## 支持的命令 -- `/projects` — 切换项目,重新显示最近项目列表 -- `/status` — 查看当前会话的项目、模型和运行状态 -- `/clear` — 清空当前会话上下文,保留项目绑定 -- `/new` — 清空当前 chat 绑定的 session,并重新选择项目 -- `/help` — 显示当前可用命令 -- `/stop` — 向当前 session 发送 `stop_generation` +微信没有可配置菜单,命令入口就是聊天框。配对成功后,bot 会提示发送 `/help`;后续随时发 `/help` 或 `帮助` 都能看到当前支持的命令。 + +- `/help` 或 `帮助` — 显示可用命令 +- `/status` 或 `状态` — 查看当前会话的项目、分支、模型、运行状态和任务摘要 +- `/projects` 或 `项目列表` — 重新显示最近项目列表 +- `/new` 或 `新会话` — 清空当前 chat 绑定的 session,并重新选择项目 +- `/new <编号、项目名或绝对路径>` — 直接在指定项目下新建会话 +- `/clear` 或 `清空` — 清空当前会话上下文,保留项目绑定 +- `/stop` 或 `停止` — 向当前 session 发送 `stop_generation` + +## 权限审批 + +当 Claude 请求敏感权限时,微信 adapter 会发送一条文本审批消息,里面包含 request id。 + +在微信里回复: + - `/allow ` — 允许一次权限请求 - `/always ` — 永久允许同类权限请求 - `/deny ` — 拒绝一次权限请求 +审批结果会通过 WebSocket 桥接回桌面端 session。 + +## 返回消息的表现 + +- 使用 `getupdates` 长轮询接收微信消息。 +- 文本回复按 3500 字左右分片发送。 +- thinking 和工具执行期间会尽量发送微信 typing 状态。 +- 图片会以内联图片形式进入模型输入;非图片文件会以本地临时文件路径传给会话。 +- 附件会走公共大小限制检查,超限时 adapter 会在微信里返回提示。 + ## 解绑 -在桌面端 `设置 -> IM 接入` 的「已配对用户」列表里点击微信用户右侧的「解绑」。 +有两种解绑: -解绑会清空微信账号凭据和已配对用户,并重启 adapter sidecar。解绑后需要重新扫码才能继续使用。 +- 解绑微信机器人账号:在微信标签页点击「解绑微信账号」,会清空 `wechat.accountId`、`wechat.botToken`、`wechat.userId`、`allowedUsers` 和 `pairedUsers`。 +- 解绑某个用户:在「已配对用户」列表里点击对应微信用户右侧的「解绑」,只移除该用户的 `pairedUsers` 记录。 + +解绑机器人账号后需要重新扫码绑定。解绑用户后,该用户需要重新发送新的配对码才能继续使用。 ## 本地开发启动 +发布版桌面端会自动启动 adapter sidecar。只有本地开发或单独调试时才需要手动运行: + ```bash cd adapters bun install @@ -55,7 +111,46 @@ bun run wechat export WECHAT_ACCOUNT_ID="..." export WECHAT_BOT_TOKEN="..." export WECHAT_BASE_URL="https://ilinkai.weixin.qq.com" +export WECHAT_USER_ID="..." export ADAPTER_SERVER_URL="ws://127.0.0.1:3456" ``` 正常桌面端使用不需要手动设置这些环境变量,扫码绑定会写入本地配置。 + +## 常见问题 + +### 扫码成功后发消息仍提示未授权 + +这是正常授权流程的一部分。扫码绑定只写入机器人账号凭据;具体微信用户还需要发送配对码,或被加入 `Allowed Users`。 + +### 页面显示二维码过期 + +重新点击「扫码绑定」生成新二维码。微信二维码登录状态只在短时间内有效。 + +### adapter 启动时报缺少微信账号 + +说明 `WECHAT_ACCOUNT_ID / WECHAT_BOT_TOKEN` 和 `~/.claude/adapters.json` 里的 `wechat.accountId / wechat.botToken` 都没有生效。先在桌面端微信标签页完成扫码绑定。 + +### 收不到回复 + +优先检查: + +- 桌面端是否正在运行。 +- 微信标签页是否显示已绑定。 +- 当前用户是否已经配对或在 `Allowed Users` 中。 +- `~/.claude/adapters.json` 是否能正常写入。 +- 本地开发时 `ADAPTER_SERVER_URL` 是否指向正在运行的 Desktop server WebSocket 地址。 + +### 会话没恢复 + +检查 `~/.claude/adapter-sessions.json` 是否能正常写入,以及 Desktop server 里的 session 是否仍存在。 + +## 源码入口 + +- `adapters/wechat/index.ts` +- `adapters/wechat/protocol.ts` +- `adapters/wechat/media.ts` +- `adapters/common/pairing.ts` +- `adapters/common/session-store.ts` +- `adapters/common/ws-bridge.ts` +- `adapters/common/http-client.ts` diff --git a/docs/images/im/dingding/dingding01.png b/docs/images/im/dingding/dingding01.png new file mode 100644 index 00000000..bdfe6d3b Binary files /dev/null and b/docs/images/im/dingding/dingding01.png differ diff --git a/docs/images/im/dingding/dingding02.png b/docs/images/im/dingding/dingding02.png new file mode 100644 index 00000000..bc149130 Binary files /dev/null and b/docs/images/im/dingding/dingding02.png differ diff --git a/docs/images/im/wechat/wechat01.png b/docs/images/im/wechat/wechat01.png new file mode 100644 index 00000000..6769bf8e Binary files /dev/null and b/docs/images/im/wechat/wechat01.png differ diff --git a/docs/images/im/wechat/wechat02.jpg b/docs/images/im/wechat/wechat02.jpg new file mode 100644 index 00000000..a66c3f8f Binary files /dev/null and b/docs/images/im/wechat/wechat02.jpg differ diff --git a/docs/images/im/wechat/wechat03.png b/docs/images/im/wechat/wechat03.png new file mode 100644 index 00000000..41e69937 Binary files /dev/null and b/docs/images/im/wechat/wechat03.png differ