From 16d2f1af5d93fe5aedbabdd1f13a6421527d6acb Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Fri, 13 Mar 2026 18:29:20 +0800 Subject: [PATCH] chore: restore feishu worktree snapshot --- .mcp.json | 10 + ARCHITECTURE.md | 27 +- docs/handover/bridge-system.md | 38 + .../mobile-remote-control-overall-plan.md | 8 +- package-lock.json | 880 +++++++++++++++- src/app/api/bridge/route.ts | 6 +- src/app/api/chat/route.ts | 17 +- src/i18n/en.ts | 47 + src/i18n/zh.ts | 47 + src/lib/bridge/adapters/feishu-adapter.ts | 955 +----------------- src/lib/bridge/bridge-manager.ts | 235 ++++- src/lib/bridge/channel-adapter.ts | 6 + src/lib/bridge/conversation-engine.ts | 15 + src/lib/db.ts | 13 + 14 files changed, 1314 insertions(+), 990 deletions(-) diff --git a/.mcp.json b/.mcp.json index ec1ba23..93bc19c 100644 --- a/.mcp.json +++ b/.mcp.json @@ -8,6 +8,16 @@ "chrome-devtools-mcp@latest" ], "env": {} + }, + "feishu": { + "command": "node", + "args": ["packages/feishu-mcp/dist/index.js"], + "env": { + "FEISHU_APP_ID": "${bridge_feishu_app_id}", + "FEISHU_APP_SECRET": "${bridge_feishu_app_secret}", + "FEISHU_DOMAIN": "${bridge_feishu_domain}", + "FEISHU_USER_ACCESS_TOKEN": "${bridge_feishu_user_token}" + } } } } \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0d5f9f4..91692a5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -107,7 +107,32 @@ Schema 定义在 `src/lib/db.ts`,12 张表: - `bridge-manager.ts` — 生命周期编排 - `markdown/` — Markdown → IR → 渠道特定格式(Telegram HTML / 飞书卡片) - `adapters/telegram-adapter.ts` — Telegram 长轮询 -- `adapters/feishu-adapter.ts` — 飞书 WebSocket + REST +- `adapters/feishu-adapter.ts` — 飞书薄代理(委托给 Channel Plugin) + +### Channel Plugin 层 + +`src/lib/channels/` — 结构化渠道插件,提供 `ChannelPlugin` 合约。 + +- `types.ts` — `ChannelPlugin`/`ChannelCapabilities`/`ProbeResult`/`CardStreamController` 接口 +- `channel-plugin-adapter.ts` — Plugin → `BaseChannelAdapter` 桥接适配器 +- `feishu/` — 飞书渠道插件(模块化拆分) + - `types.ts` — 飞书内部类型常量 + - `config.ts` — `FeishuConfig` 结构化配置 + 校验 + - `gateway.ts` — WSClient 生命周期 + 连接状态机 + probe + - `inbound.ts` — 入站消息处理 + 资源下载 + - `outbound.ts` — 出站消息渲染(card/post/permission) + - `identity.ts` — Bot 身份解析 + @mention 检测 + - `policy.ts` — 访问控制 + 群策略 + - `card-controller.ts` — CardStreamController 接口 + 占位 + - `index.ts` — `FeishuChannelPlugin` 入口 + +### Remote Core 层 + +`src/lib/remote/` — 远程 Host/Controller/Session/Lease 合约(接口 + 骨架)。 + +- `types.ts` — `RemoteHost`/`RemoteController`/`SessionLease`/`RemoteEvent` 接口 +- `remote-manager.ts` — 轻量运行时骨架 +- `index.ts` — 公开导出 详细文档:`docs/handover/bridge-system.md` diff --git a/docs/handover/bridge-system.md b/docs/handover/bridge-system.md index 2430dff..cd9ac47 100644 --- a/docs/handover/bridge-system.md +++ b/docs/handover/bridge-system.md @@ -281,3 +281,41 @@ Claude 回复按内容分流渲染(对齐 Openclaw 方案): - Android App、桌面 Controller 和 IM 渠道都将共享同一套 Remote Core - 飞书不再只是一个 adapter,而会逐步拆分成独立的渠道模块族 - 当前 Bridge 仍是实现基础,但不再是远程能力的最终抽象边界 + +### V2 实施状态(codex/feishu-remote-v2) + +**已完成:** + +1. **Channel Plugin 合约** (`src/lib/channels/types.ts`) + - `ChannelPlugin` 接口:config/capabilities/lifecycle/inbound/outbound/policy + - `ChannelCapabilities`:streaming、inlineButtons、reactions 等能力声明 + - `ProbeResult`:健康检查结果 + - `CardStreamController`:流式卡片接口(占位) + +2. **ChannelPluginAdapter** (`src/lib/channels/channel-plugin-adapter.ts`) + - 将 `ChannelPlugin` 桥接为 `BaseChannelAdapter` + - bridge-manager 无需修改即可使用新插件 + +3. **飞书模块拆分** (`src/lib/channels/feishu/`) + - `types.ts` — 内部类型 + 常量 + - `config.ts` — `FeishuConfig` 结构化配置 + 校验 + `configFromSettings()` + - `gateway.ts` — WSClient 生命周期 + 连接状态机 + 指标 + probe + - `inbound.ts` — 入站消息处理 + 内容解析 + 资源下载 + - `outbound.ts` — 出站消息渲染(card/post/text/permission) + - `identity.ts` — Bot 身份解析 + @mention 检测 + - `policy.ts` — 用户授权 + 群聊策略 + - `card-controller.ts` — CardStreamController 占位 + - `index.ts` — `FeishuChannelPlugin` 组合入口 + +4. **结构化配置** (`channel_configs` DB 表 + API 路由) + - `GET/PUT /api/channels/feishu/config` — 读写结构化配置 + - 向后兼容:读 channel_configs → 回退到 settings + +5. **Status / Probe** (`/api/channels/feishu/status`) + - 连接状态、Bot 身份、指标、可选 probe + +6. **Remote Core 合约** (`src/lib/remote/`) + - `RemoteHost`/`RemoteController`/`SessionLease`/`RemoteEvent` 接口 + - `RemoteManager` 轻量骨架 + +7. **原 feishu-adapter.ts** 改为薄代理(~15 行) diff --git a/docs/research/mobile-remote-control-overall-plan.md b/docs/research/mobile-remote-control-overall-plan.md index c01a10e..d5f6908 100644 --- a/docs/research/mobile-remote-control-overall-plan.md +++ b/docs/research/mobile-remote-control-overall-plan.md @@ -516,7 +516,7 @@ OpenClaw 官方飞书插件最值得借的不是“功能数量”,而是能 如果继续推进,下一份文档最值得做的是: -- `远程方案 V2 执行计划` -- `Remote Contract / Channel Contract 草案` -- `Feishu V2 渠道模块拆分设计` -- `Remote App 信息架构 + API 清单` +- ~~`远程方案 V2 执行计划`~~ ✅ 已完成 → `docs/exec-plans/active/feishu-v2-channel-plugin.md` +- ~~`Remote Contract / Channel Contract 草案`~~ ✅ 已完成 → `src/lib/channels/types.ts` + `src/lib/remote/types.ts` +- ~~`Feishu V2 渠道模块拆分设计`~~ ✅ 已完成 → `src/lib/channels/feishu/` +- `Remote App 信息架构 + API 清单` — 待后续实施 diff --git a/package-lock.json b/package-lock.json index 212fff3..16e3220 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1479,6 +1479,56 @@ "zod": "^4.0.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", + "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -2082,6 +2132,10 @@ "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", "license": "Apache-2.0" }, + "node_modules/@codepilot/feishu-mcp": { + "resolved": "packages/feishu-mcp", + "link": true + }, "node_modules/@codepilot/site": { "resolved": "apps/site", "link": true @@ -7955,6 +8009,356 @@ "url": "https://opencollective.com/immer" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -9147,6 +9551,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -9891,6 +10305,18 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -9959,6 +10385,18 @@ "node": ">= 14" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ahooks": { "version": "3.9.6", "resolved": "https://registry.npmjs.org/ahooks/-/ahooks-3.9.6.tgz", @@ -10221,6 +10659,13 @@ "react": ">=18" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/app-builder-bin": { "version": "5.0.0-alpha.12", "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", @@ -11137,6 +11582,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -11157,6 +11618,16 @@ "node": ">= 0.8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/cacache": { "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", @@ -11790,6 +12261,16 @@ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "license": "MIT" }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -13351,7 +13832,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -14287,6 +14767,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", @@ -14813,6 +15302,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -14924,6 +15425,12 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, "node_modules/format": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -14932,6 +15439,28 @@ "node": ">=0.4.x" } }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -16126,6 +16655,15 @@ "node": ">=18.18.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -17099,6 +17637,16 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/js-cookie": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", @@ -17651,6 +18199,19 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -17920,6 +18481,16 @@ "@types/trusted-types": "^2.0.2" } }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -20092,6 +20663,18 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nan": { "version": "2.25.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz", @@ -21098,6 +21681,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/pkce-challenge": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", @@ -23078,6 +23671,51 @@ "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", "license": "Unlicense" }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, "node_modules/roughjs": { "version": "4.6.6", "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", @@ -24699,6 +25337,39 @@ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", "license": "MIT" }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", @@ -24929,6 +25600,29 @@ "node": ">= 10.0.0" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -25110,6 +25804,12 @@ "node": ">=16" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -25172,6 +25872,13 @@ "node": ">=6.10" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/ts-md5": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ts-md5/-/ts-md5-2.0.1.tgz", @@ -26123,6 +26830,22 @@ "node": ">= 8" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -26492,6 +27215,161 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "packages/feishu-mcp": { + "name": "@codepilot/feishu-mcp", + "version": "0.1.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0" + }, + "bin": { + "feishu-mcp": "dist/index.js" + }, + "devDependencies": { + "tsup": "^8.0.0", + "tsx": "^4.0.0" + } + }, + "packages/feishu-mcp/node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "packages/feishu-mcp/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "packages/feishu-mcp/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "packages/feishu-mcp/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "packages/feishu-mcp/node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "packages/feishu-mcp/node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } } } } diff --git a/src/app/api/bridge/route.ts b/src/app/api/bridge/route.ts index c1021cc..14a530f 100644 --- a/src/app/api/bridge/route.ts +++ b/src/app/api/bridge/route.ts @@ -5,7 +5,11 @@ export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; /** - * GET /api/bridge — Return bridge status (pure query, no side effects) + * GET /api/bridge — Return bridge status (pure query, no side effects). + * + * Does NOT run probe — probe is an explicit action via the per-channel + * status endpoint (e.g. /api/channels/feishu/status?probe=true). + * This endpoint is polled every 5s by useBridgeStatus so it must stay cheap. */ export async function GET() { try { diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index c3a488f..884aa24 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -14,7 +14,7 @@ import type { MCPServerConfig } from '@/types'; export const runtime = 'nodejs'; export const dynamic = 'force-dynamic'; -/** Read MCP server configs from ~/.claude.json and ~/.claude/settings.json */ +/** Read MCP server configs from ~/.claude.json, ~/.claude/settings.json, and project .mcp.json */ function loadMcpServers(): Record | undefined { try { const readJson = (p: string): Record => { @@ -23,10 +23,25 @@ function loadMcpServers(): Record | undefined { }; const userConfig = readJson(path.join(os.homedir(), '.claude.json')); const settings = readJson(path.join(os.homedir(), '.claude', 'settings.json')); + // Also read project-level .mcp.json + const projectMcp = readJson(path.join(process.cwd(), '.mcp.json')); const merged = { ...((userConfig.mcpServers || {}) as Record), ...((settings.mcpServers || {}) as Record), + ...((projectMcp.mcpServers || {}) as Record), }; + // Resolve ${...} placeholders in env values against DB settings + for (const server of Object.values(merged)) { + if (server.env) { + for (const [key, value] of Object.entries(server.env)) { + if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) { + const settingKey = value.slice(2, -1); + const resolved = getSetting(settingKey); + server.env[key] = resolved || ''; + } + } + } + } return Object.keys(merged).length > 0 ? merged : undefined; } catch { return undefined; diff --git a/src/i18n/en.ts b/src/i18n/en.ts index d81877d..b6838df 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -555,6 +555,53 @@ const en = { 'feishu.step4': 'Add event subscription: im.message.receive_v1', 'feishu.step5': 'Publish the app version and approve it in the admin console', 'feishu.step6': 'Paste the credentials above and click "Test Connection" to verify', + 'feishu.replyMode': 'Reply Mode', + 'feishu.replyModeInline': 'Inline — reply directly in chat', + 'feishu.replyModeThread': 'Thread — reply in message thread', + 'feishu.streaming': 'Streaming Cards', + 'feishu.streamingDesc': 'Stream responses via updateable cards (requires CardKit v2)', + 'feishu.blockStreaming': 'Block Streaming', + 'feishu.blockStreamingDesc': 'Stream responses at block level', + 'feishu.threadSession': 'Thread Sessions', + 'feishu.threadSessionDesc': 'Use message threads as separate conversation sessions', + 'feishu.connectionState': 'Connection State', + 'feishu.connectionStateConnected': 'Connected', + 'feishu.connectionStateDisconnected': 'Disconnected', + 'feishu.connectionStateConnecting': 'Connecting...', + 'feishu.connectionStateReconnecting': 'Reconnecting...', + 'feishu.metrics': 'Metrics', + 'feishu.metricsMessagesIn': 'Messages In', + 'feishu.metricsMessagesOut': 'Messages Out', + 'feishu.metricsErrors': 'Errors', + 'feishu.probe': 'Probe', + 'feishu.probeSuccess': 'Probe successful', + 'feishu.probeFailed': 'Probe failed', + 'feishu.capabilities': 'Platform Capabilities', + 'feishu.capabilities.doc': 'Documents', + 'feishu.capabilities.sheet': 'Spreadsheets', + 'feishu.capabilities.calendar': 'Calendar', + 'feishu.capabilities.task': 'Tasks', + 'feishu.capabilities.bitable': 'Bitable (Tables)', + 'feishu.diagnose': 'Diagnostics', + 'feishu.diagnose.title': 'Feishu Connection Diagnostics', + 'feishu.diagnose.config': 'Configuration', + 'feishu.diagnose.connection': 'Connection', + 'feishu.diagnose.permissions': 'Permissions', + 'feishu.diagnose.running': 'Running diagnostics...', + + // ── Channels (generic) ──────────────────────────────────── + 'channels.configSaved': 'Configuration saved', + 'channels.configError': 'Configuration error', + + // ── Remote ──────────────────────────────────────────────── + 'remote.title': 'Remote Control', + 'remote.host': 'Host', + 'remote.controllers': 'Controllers', + 'remote.sessions': 'Remote Sessions', + 'remote.lease': 'Session Lease', + 'remote.leaseAcquired': 'Lease acquired', + 'remote.leaseReleased': 'Lease released', + 'remote.leaseExpired': 'Lease expired', // ── Settings: Remote Bridge ──────────────────────────────── 'settings.bridge': 'Remote Bridge', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index abb540f..ce4ec79 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -552,6 +552,53 @@ const zh: Record = { 'feishu.step4': '添加事件订阅:im.message.receive_v1', 'feishu.step5': '发布应用版本并在管理后台审批通过', 'feishu.step6': '将凭据粘贴到上方,点击「测试连接」验证', + 'feishu.replyMode': '回复模式', + 'feishu.replyModeInline': '内联 — 直接在聊天中回复', + 'feishu.replyModeThread': '话题 — 在消息话题中回复', + 'feishu.streaming': '流式卡片', + 'feishu.streamingDesc': '通过可更新卡片流式输出响应(需要 CardKit v2)', + 'feishu.blockStreaming': 'Block 流式', + 'feishu.blockStreamingDesc': '按 Block 级别流式输出响应', + 'feishu.threadSession': '话题会话', + 'feishu.threadSessionDesc': '将消息话题作为独立的对话会话', + 'feishu.connectionState': '连接状态', + 'feishu.connectionStateConnected': '已连接', + 'feishu.connectionStateDisconnected': '未连接', + 'feishu.connectionStateConnecting': '连接中...', + 'feishu.connectionStateReconnecting': '重连中...', + 'feishu.metrics': '指标', + 'feishu.metricsMessagesIn': '接收消息', + 'feishu.metricsMessagesOut': '发送消息', + 'feishu.metricsErrors': '错误', + 'feishu.probe': '探测', + 'feishu.probeSuccess': '探测成功', + 'feishu.probeFailed': '探测失败', + 'feishu.capabilities': '平台能力', + 'feishu.capabilities.doc': '云文档', + 'feishu.capabilities.sheet': '电子表格', + 'feishu.capabilities.calendar': '日历', + 'feishu.capabilities.task': '任务', + 'feishu.capabilities.bitable': '多维表格', + 'feishu.diagnose': '诊断', + 'feishu.diagnose.title': '飞书连接诊断', + 'feishu.diagnose.config': '配置', + 'feishu.diagnose.connection': '连接', + 'feishu.diagnose.permissions': '权限', + 'feishu.diagnose.running': '正在运行诊断...', + + // ── 渠道(通用)──────────────────────────────────────────── + 'channels.configSaved': '配置已保存', + 'channels.configError': '配置错误', + + // ── 远程控制 ────────────────────────────────────────────── + 'remote.title': '远程控制', + 'remote.host': '主机', + 'remote.controllers': '控制器', + 'remote.sessions': '远程会话', + 'remote.lease': '会话租约', + 'remote.leaseAcquired': '租约已获取', + 'remote.leaseReleased': '租约已释放', + 'remote.leaseExpired': '租约已过期', // ── Settings: Remote Bridge ──────────────────────────────── 'settings.bridge': '远程桥接', diff --git a/src/lib/bridge/adapters/feishu-adapter.ts b/src/lib/bridge/adapters/feishu-adapter.ts index 9f9fe6e..1174254 100644 --- a/src/lib/bridge/adapters/feishu-adapter.ts +++ b/src/lib/bridge/adapters/feishu-adapter.ts @@ -1,951 +1,16 @@ /** - * Feishu (Lark) Adapter — implements BaseChannelAdapter for Feishu Bot API. + * Feishu adapter — thin proxy that delegates to FeishuChannelPlugin. * - * Uses the official @larksuiteoapi/node-sdk WSClient for real-time event - * subscription and REST Client for message sending / resource downloading. - * Routes messages through an internal async queue (same pattern as Telegram). - * - * Rendering strategy (aligned with Openclaw): - * - Code blocks / tables → interactive card (schema 2.0 markdown) - * - Other text → post (msg_type: 'post') with md tag - * - Permission prompts → interactive card with action buttons - * - * card.action.trigger events are handled via EventDispatcher (Openclaw pattern): - * button clicks are converted to synthetic text messages and routed through - * the normal /perm command processing pipeline. + * The actual implementation lives in src/lib/channels/feishu/. + * This file exists only to maintain the existing self-registration pattern + * used by bridge-manager via adapters/index.ts. */ -import crypto from 'crypto'; -import * as lark from '@larksuiteoapi/node-sdk'; -import type { - ChannelType, - InboundMessage, - OutboundMessage, - SendResult, -} from '../types'; -import type { FileAttachment } from '@/types'; -import { BaseChannelAdapter, registerAdapterFactory } from '../channel-adapter'; -import { insertAuditLog } from '../../db'; -import { getSetting } from '../../db'; -import { - htmlToFeishuMarkdown, - preprocessFeishuMarkdown, - hasComplexMarkdown, - buildCardContent, - buildPostContent, -} from '../markdown/feishu'; - -/** Max number of message_ids to keep for dedup. */ -const DEDUP_MAX = 1000; - -/** Max file download size (20 MB). */ -const MAX_FILE_SIZE = 20 * 1024 * 1024; - -/** Feishu emoji type for typing indicator (same as Openclaw). */ -const TYPING_EMOJI = 'Typing'; - -/** Shape of the SDK's im.message.receive_v1 event data. */ -type FeishuMessageEventData = { - sender: { - sender_id?: { - open_id?: string; - union_id?: string; - user_id?: string; - }; - sender_type: string; - tenant_key?: string; - }; - message: { - message_id: string; - chat_id: string; - chat_type: string; - message_type: string; - content: string; - create_time: string; - mentions?: Array<{ - key: string; - id: { open_id?: string; union_id?: string; user_id?: string }; - name: string; - }>; - }; -}; - - -/** MIME type guesses by message_type. */ -const MIME_BY_TYPE: Record = { - image: 'image/png', - file: 'application/octet-stream', - audio: 'audio/ogg', - video: 'video/mp4', - media: 'application/octet-stream', -}; - -export class FeishuAdapter extends BaseChannelAdapter { - readonly channelType: ChannelType = 'feishu'; - - private running = false; - private queue: InboundMessage[] = []; - private waiters: Array<(msg: InboundMessage | null) => void> = []; - private wsClient: lark.WSClient | null = null; - private restClient: lark.Client | null = null; - private seenMessageIds = new Map(); - private botOpenId: string | null = null; - /** All known bot IDs (open_id, user_id, union_id) for mention matching. */ - private botIds = new Set(); - /** Track last incoming message ID per chat for typing indicator. */ - private lastIncomingMessageId = new Map(); - /** Track active typing reaction IDs per chat for cleanup. */ - private typingReactions = new Map(); - - // ── Lifecycle ─────────────────────────────────────────────── - - async start(): Promise { - if (this.running) return; - - const configError = this.validateConfig(); - if (configError) { - console.warn('[feishu-adapter] Cannot start:', configError); - return; - } - - const appId = getSetting('bridge_feishu_app_id') || ''; - const appSecret = getSetting('bridge_feishu_app_secret') || ''; - const domainSetting = getSetting('bridge_feishu_domain') || 'feishu'; - const domain = domainSetting === 'lark' - ? lark.Domain.Lark - : lark.Domain.Feishu; - - // Create REST client - this.restClient = new lark.Client({ - appId, - appSecret, - domain, - }); - - // Resolve bot identity for @mention detection - await this.resolveBotIdentity(appId, appSecret, domain); - - this.running = true; - - // Create EventDispatcher and register event handlers. - // NOTE: card.action.trigger requires HTTP webhook (not supported via WSClient). - // Openclaw uses an HTTP server for card callbacks — CodePilot is a desktop app - // without a public endpoint, so we rely on text-based /perm commands instead. - const dispatcher = new lark.EventDispatcher({}).register({ - 'im.message.receive_v1': async (data) => { - await this.handleIncomingEvent(data as FeishuMessageEventData); - }, - }); - - // Create and start WSClient - this.wsClient = new lark.WSClient({ - appId, - appSecret, - domain, - }); - this.wsClient.start({ eventDispatcher: dispatcher }); - - console.log('[feishu-adapter] Started (botOpenId:', this.botOpenId || 'unknown', ')'); - } - - async stop(): Promise { - if (!this.running) return; - this.running = false; - - // Close WebSocket connection (SDK exposes close()) - if (this.wsClient) { - try { - this.wsClient.close({ force: true }); - } catch (err) { - console.warn('[feishu-adapter] WSClient close error:', err instanceof Error ? err.message : err); - } - this.wsClient = null; - } - this.restClient = null; - - // Reject all waiting consumers - for (const waiter of this.waiters) { - waiter(null); - } - this.waiters = []; - - // Clear state - this.seenMessageIds.clear(); - this.lastIncomingMessageId.clear(); - this.typingReactions.clear(); - - console.log('[feishu-adapter] Stopped'); - } - - isRunning(): boolean { - return this.running; - } - - // ── Queue ─────────────────────────────────────────────────── - - consumeOne(): Promise { - const queued = this.queue.shift(); - if (queued) return Promise.resolve(queued); - - if (!this.running) return Promise.resolve(null); - - return new Promise((resolve) => { - this.waiters.push(resolve); - }); - } - - private enqueue(msg: InboundMessage): void { - const waiter = this.waiters.shift(); - if (waiter) { - waiter(msg); - } else { - this.queue.push(msg); - } - } - - // ── Typing indicator (Openclaw-style reaction) ───────────── - - /** - * Add a "Typing" emoji reaction to the user's message. - * Called by bridge-manager via onMessageStart(). - */ - onMessageStart(chatId: string): void { - const messageId = this.lastIncomingMessageId.get(chatId); - if (!messageId || !this.restClient) return; - - // Fire-and-forget — typing indicator is non-critical - this.restClient.im.messageReaction.create({ - path: { message_id: messageId }, - data: { reaction_type: { emoji_type: TYPING_EMOJI } }, - }).then((res) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const reactionId = (res as any)?.data?.reaction_id; - if (reactionId) { - this.typingReactions.set(chatId, reactionId); - } - }).catch((err) => { - // Non-critical — don't log rate limit errors - const code = (err as { code?: number })?.code; - if (code !== 99991400 && code !== 99991403) { - console.warn('[feishu-adapter] Typing indicator failed:', err instanceof Error ? err.message : err); - } - }); - } - - /** - * Remove the "Typing" emoji reaction from the user's message. - * Called by bridge-manager via onMessageEnd(). - */ - onMessageEnd(chatId: string): void { - const reactionId = this.typingReactions.get(chatId); - const messageId = this.lastIncomingMessageId.get(chatId); - if (!reactionId || !messageId || !this.restClient) return; - - this.typingReactions.delete(chatId); - - // Fire-and-forget — failure is fine (reaction may already be gone) - this.restClient.im.messageReaction.delete({ - path: { message_id: messageId, reaction_id: reactionId }, - }).catch(() => { /* ignore */ }); - } - - // ── Send ──────────────────────────────────────────────────── - - async send(message: OutboundMessage): Promise { - if (!this.restClient) { - return { ok: false, error: 'Feishu client not initialized' }; - } - - let text = message.text; - - // Convert HTML to markdown for Feishu rendering (e.g. command responses) - if (message.parseMode === 'HTML') { - text = htmlToFeishuMarkdown(text); - } - - // Preprocess markdown for Claude responses - if (message.parseMode === 'Markdown') { - text = preprocessFeishuMarkdown(text); - } - - // If there are inline buttons (permission prompts), send card with action buttons - if (message.inlineButtons && message.inlineButtons.length > 0) { - return this.sendPermissionCard(message.address.chatId, text, message.inlineButtons); - } - - // Rendering strategy (aligned with Openclaw): - // - Code blocks / tables → interactive card (schema 2.0 markdown) - // - Other text → post (md tag) - if (hasComplexMarkdown(text)) { - return this.sendAsCard(message.address.chatId, text); - } - return this.sendAsPost(message.address.chatId, text); - } - - /** - * Send text as an interactive card (schema 2.0 markdown). - * Used for code blocks and tables — card renders them properly. - */ - private async sendAsCard(chatId: string, text: string): Promise { - const cardContent = buildCardContent(text); - - try { - const res = await this.restClient!.im.message.create({ - params: { receive_id_type: 'chat_id' }, - data: { - receive_id: chatId, - msg_type: 'interactive', - content: cardContent, - }, - }); - - if (res?.data?.message_id) { - return { ok: true, messageId: res.data.message_id }; - } - console.warn('[feishu-adapter] Card send failed:', res?.msg, res?.code); - } catch (err) { - console.warn('[feishu-adapter] Card send error, falling back to post:', err instanceof Error ? err.message : err); - } - - // Fallback to post - return this.sendAsPost(chatId, text); - } - - /** - * Send text as a post message (msg_type: 'post') with md tag. - * Used for simple text — renders bold, italic, inline code, links. - */ - private async sendAsPost(chatId: string, text: string): Promise { - const postContent = buildPostContent(text); - - try { - const res = await this.restClient!.im.message.create({ - params: { receive_id_type: 'chat_id' }, - data: { - receive_id: chatId, - msg_type: 'post', - content: postContent, - }, - }); - - if (res?.data?.message_id) { - return { ok: true, messageId: res.data.message_id }; - } - console.warn('[feishu-adapter] Post send failed:', res?.msg, res?.code); - } catch (err) { - console.warn('[feishu-adapter] Post send error, falling back to text:', err instanceof Error ? err.message : err); - } - - // Final fallback: plain text - try { - const res = await this.restClient!.im.message.create({ - params: { receive_id_type: 'chat_id' }, - data: { - receive_id: chatId, - msg_type: 'text', - content: JSON.stringify({ text }), - }, - }); - if (res?.data?.message_id) { - return { ok: true, messageId: res.data.message_id }; - } - return { ok: false, error: res?.msg || 'Send failed' }; - } catch (err) { - return { ok: false, error: err instanceof Error ? err.message : 'Send failed' }; - } - } - - // ── Permission card (with real action buttons) ───────────── - - /** - * Send a permission card with real Feishu card action buttons. - * Button clicks trigger card.action.trigger events handled by handleCardActionEvent(). - * Feishu card action callbacks require HTTP webhook (not supported via WSClient). - * CodePilot is a desktop app without a public endpoint, so we send a - * well-formatted card with /perm text commands instead of clickable buttons. - * The user replies with the /perm command to approve/deny. - */ - private async sendPermissionCard( - chatId: string, - text: string, - inlineButtons: import('../types').InlineButton[][], - ): Promise { - if (!this.restClient) { - return { ok: false, error: 'Feishu client not initialized' }; - } - - // Build /perm command lines from inline buttons - const permCommands = inlineButtons.flat().map((btn) => { - if (btn.callbackData.startsWith('perm:')) { - const parts = btn.callbackData.split(':'); - const action = parts[1]; - const permId = parts.slice(2).join(':'); - return `\`/perm ${action} ${permId}\``; - } - return btn.text; - }); - - // Schema 2.0 card with markdown — permission info + copyable commands - const cardContent = [ - text, - '', - '---', - '**Reply with one of these commands:**', - ...permCommands, - ].join('\n'); - - const cardJson = JSON.stringify({ - schema: '2.0', - config: { wide_screen_mode: true }, - header: { - template: 'orange', - title: { tag: 'plain_text', content: '🔐 Permission Required' }, - }, - body: { - elements: [ - { tag: 'markdown', content: cardContent }, - ], - }, - }); - - try { - const res = await this.restClient.im.message.create({ - params: { receive_id_type: 'chat_id' }, - data: { - receive_id: chatId, - msg_type: 'interactive', - content: cardJson, - }, - }); - if (res?.data?.message_id) { - return { ok: true, messageId: res.data.message_id }; - } - console.warn('[feishu-adapter] Permission card send failed:', res?.msg); - } catch (err) { - console.warn('[feishu-adapter] Permission card error:', err instanceof Error ? err.message : err); - } - - // Fallback: plain text - const plainCommands = inlineButtons.flat().map((btn) => { - if (btn.callbackData.startsWith('perm:')) { - const parts = btn.callbackData.split(':'); - return `/perm ${parts[1]} ${parts.slice(2).join(':')}`; - } - return btn.text; - }); - const fallbackText = text + '\n\nReply with:\n' + plainCommands.join('\n'); - - try { - const res = await this.restClient.im.message.create({ - params: { receive_id_type: 'chat_id' }, - data: { - receive_id: chatId, - msg_type: 'text', - content: JSON.stringify({ text: fallbackText }), - }, - }); - if (res?.data?.message_id) { - return { ok: true, messageId: res.data.message_id }; - } - return { ok: false, error: res?.msg || 'Send failed' }; - } catch (err) { - return { ok: false, error: err instanceof Error ? err.message : 'Send failed' }; - } - } - - // ── Config & Auth ─────────────────────────────────────────── - - validateConfig(): string | null { - const enabled = getSetting('bridge_feishu_enabled'); - if (enabled !== 'true') return 'bridge_feishu_enabled is not true'; - - const appId = getSetting('bridge_feishu_app_id'); - if (!appId) return 'bridge_feishu_app_id not configured'; - - const appSecret = getSetting('bridge_feishu_app_secret'); - if (!appSecret) return 'bridge_feishu_app_secret not configured'; - - return null; - } - - isAuthorized(userId: string, chatId: string): boolean { - const allowedUsers = getSetting('bridge_feishu_allowed_users') || ''; - if (!allowedUsers) { - // No restriction configured — allow all - return true; - } - - const allowed = allowedUsers - .split(',') - .map((s) => s.trim()) - .filter(Boolean); - - if (allowed.length === 0) return true; - - return allowed.includes(userId) || allowed.includes(chatId); - } - - // ── Incoming event handler ────────────────────────────────── - - private async handleIncomingEvent(data: FeishuMessageEventData): Promise { - try { - await this.processIncomingEvent(data); - } catch (err) { - console.error( - '[feishu-adapter] Unhandled error in event handler:', - err instanceof Error ? err.stack || err.message : err, - ); - } - } - - private async processIncomingEvent(data: FeishuMessageEventData): Promise { - const msg = data.message; - const sender = data.sender; - - // [P1] Filter out bot messages to prevent self-triggering loops - if (sender.sender_type === 'bot') return; - - // Dedup by message_id - if (this.seenMessageIds.has(msg.message_id)) return; - this.addToDedup(msg.message_id); - - const chatId = msg.chat_id; - // [P2] Complete sender ID fallback chain: open_id > user_id > union_id - const userId = sender.sender_id?.open_id - || sender.sender_id?.user_id - || sender.sender_id?.union_id - || ''; - const isGroup = msg.chat_type === 'group'; - - // Authorization check - if (!this.isAuthorized(userId, chatId)) { - console.warn('[feishu-adapter] Unauthorized message from userId:', userId, 'chatId:', chatId); - return; - } - - // Group chat policy - if (isGroup) { - const policy = getSetting('bridge_feishu_group_policy') || 'open'; - - if (policy === 'disabled') { - console.log('[feishu-adapter] Group message ignored (policy=disabled), chatId:', chatId); - return; - } - - if (policy === 'allowlist') { - const allowedGroups = (getSetting('bridge_feishu_group_allow_from') || '') - .split(',') - .map((s) => s.trim()) - .filter(Boolean); - if (!allowedGroups.includes(chatId)) { - console.log('[feishu-adapter] Group message ignored (not in allowlist), chatId:', chatId); - return; - } - } - - // Require @mention check - const requireMention = getSetting('bridge_feishu_require_mention') !== 'false'; - if (requireMention && !this.isBotMentioned(msg.mentions)) { - console.log('[feishu-adapter] Group message ignored (bot not @mentioned), chatId:', chatId, 'msgId:', msg.message_id); - try { - insertAuditLog({ - channelType: 'feishu', - chatId, - direction: 'inbound', - messageId: msg.message_id, - summary: '[FILTERED] Group message dropped: bot not @mentioned (require_mention=true)', - }); - } catch { /* best effort */ } - return; - } - } - - // Track last message ID per chat for typing indicator - this.lastIncomingMessageId.set(chatId, msg.message_id); - - // Extract content based on message type - const messageType = msg.message_type; - let text = ''; - const attachments: FileAttachment[] = []; - - if (messageType === 'text') { - text = this.parseTextContent(msg.content); - } else if (messageType === 'image') { - // [P1] Download image with failure fallback - console.log('[feishu-adapter] Image message received, content:', msg.content); - const fileKey = this.extractFileKey(msg.content); - console.log('[feishu-adapter] Extracted fileKey:', fileKey); - if (fileKey) { - const attachment = await this.downloadResource(msg.message_id, fileKey, 'image'); - if (attachment) { - attachments.push(attachment); - } else { - text = '[image download failed]'; - try { - insertAuditLog({ - channelType: 'feishu', - chatId, - direction: 'inbound', - messageId: msg.message_id, - summary: `[ERROR] Image download failed for key: ${fileKey}`, - }); - } catch { /* best effort */ } - } - } - } else if (messageType === 'file' || messageType === 'audio' || messageType === 'video' || messageType === 'media') { - // [P2] Support file/audio/video/media downloads - const fileKey = this.extractFileKey(msg.content); - if (fileKey) { - const resourceType = messageType === 'audio' || messageType === 'video' || messageType === 'media' - ? messageType - : 'file'; - const attachment = await this.downloadResource(msg.message_id, fileKey, resourceType); - if (attachment) { - attachments.push(attachment); - } else { - text = `[${messageType} download failed]`; - try { - insertAuditLog({ - channelType: 'feishu', - chatId, - direction: 'inbound', - messageId: msg.message_id, - summary: `[ERROR] ${messageType} download failed for key: ${fileKey}`, - }); - } catch { /* best effort */ } - } - } - } else if (messageType === 'post') { - // [P2] Extract text and image keys from rich text (post) messages - const { extractedText, imageKeys } = this.parsePostContent(msg.content); - text = extractedText; - for (const key of imageKeys) { - const attachment = await this.downloadResource(msg.message_id, key, 'image'); - if (attachment) { - attachments.push(attachment); - } - // Don't add fallback text for individual post images — the text already carries context - } - } else { - // Unsupported type — log and skip - console.log(`[feishu-adapter] Unsupported message type: ${messageType}, msgId: ${msg.message_id}`); - return; - } - - // Strip @mention markers from text - text = this.stripMentionMarkers(text); - - if (!text.trim() && attachments.length === 0) return; - - const timestamp = parseInt(msg.create_time, 10) || Date.now(); - const address = { - channelType: 'feishu' as const, - chatId, - userId, - }; - - // [P1] Check for /perm text command (permission approval fallback) - const trimmedText = text.trim(); - if (trimmedText.startsWith('/perm ')) { - const permParts = trimmedText.split(/\s+/); - // /perm - if (permParts.length >= 3) { - const action = permParts[1]; // allow / allow_session / deny - const permId = permParts.slice(2).join(' '); - const callbackData = `perm:${action}:${permId}`; - - const inbound: InboundMessage = { - messageId: msg.message_id, - address, - text: trimmedText, - timestamp, - callbackData, - }; - this.enqueue(inbound); - return; - } - } - - const inbound: InboundMessage = { - messageId: msg.message_id, - address, - text: text.trim(), - timestamp, - attachments: attachments.length > 0 ? attachments : undefined, - }; - - // Audit log - try { - const summary = attachments.length > 0 - ? `[${attachments.length} attachment(s)] ${text.slice(0, 150)}` - : text.slice(0, 200); - insertAuditLog({ - channelType: 'feishu', - chatId, - direction: 'inbound', - messageId: msg.message_id, - summary, - }); - } catch { /* best effort */ } - - this.enqueue(inbound); - } - - // ── Content parsing ───────────────────────────────────────── - - private parseTextContent(content: string): string { - try { - const parsed = JSON.parse(content); - return parsed.text || ''; - } catch { - return content; - } - } - - /** - * Extract file key from message content JSON. - * Handles multiple key names: image_key, file_key, imageKey, fileKey. - */ - private extractFileKey(content: string): string | null { - try { - const parsed = JSON.parse(content); - return parsed.image_key || parsed.file_key || parsed.imageKey || parsed.fileKey || null; - } catch { - return null; - } - } - - /** - * Parse rich text (post) content. - * Extracts plain text from text elements and image keys from img elements. - */ - private parsePostContent(content: string): { extractedText: string; imageKeys: string[] } { - const imageKeys: string[] = []; - const textParts: string[] = []; - - try { - const parsed = JSON.parse(content); - // Post content structure: { title, content: [[{tag, text/image_key}]] } - const title = parsed.title; - if (title) textParts.push(title); - - const paragraphs = parsed.content; - if (Array.isArray(paragraphs)) { - for (const paragraph of paragraphs) { - if (!Array.isArray(paragraph)) continue; - for (const element of paragraph) { - if (element.tag === 'text' && element.text) { - textParts.push(element.text); - } else if (element.tag === 'a' && element.text) { - textParts.push(element.text); - } else if (element.tag === 'at' && element.user_id) { - // Mention in post — handled by isBotMentioned for group policy - } else if (element.tag === 'img') { - const key = element.image_key || element.file_key || element.imageKey; - if (key) imageKeys.push(key); - } - } - textParts.push('\n'); - } - } - } catch { - // Failed to parse post content - } - - return { extractedText: textParts.join('').trim(), imageKeys }; - } - - // ── Bot identity ──────────────────────────────────────────── - - /** - * Resolve bot identity via the Feishu REST API /bot/v3/info/. - * Collects all available bot IDs for comprehensive mention matching. - */ - private async resolveBotIdentity( - appId: string, - appSecret: string, - domain: lark.Domain, - ): Promise { - try { - const baseUrl = domain === lark.Domain.Lark - ? 'https://open.larksuite.com' - : 'https://open.feishu.cn'; - - const tokenRes = await fetch(`${baseUrl}/open-apis/auth/v3/tenant_access_token/internal`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ app_id: appId, app_secret: appSecret }), - signal: AbortSignal.timeout(10_000), - }); - const tokenData = await tokenRes.json(); - if (!tokenData.tenant_access_token) { - console.warn('[feishu-adapter] Failed to get tenant access token'); - return; - } - - const botRes = await fetch(`${baseUrl}/open-apis/bot/v3/info/`, { - method: 'GET', - headers: { Authorization: `Bearer ${tokenData.tenant_access_token}` }, - signal: AbortSignal.timeout(10_000), - }); - const botData = await botRes.json(); - if (botData?.bot?.open_id) { - this.botOpenId = botData.bot.open_id; - this.botIds.add(botData.bot.open_id); - } - // Also record app_id-based IDs if available - if (botData?.bot?.bot_id) { - this.botIds.add(botData.bot.bot_id); - } - if (!this.botOpenId) { - console.warn('[feishu-adapter] Could not resolve bot open_id'); - } - } catch (err) { - console.warn( - '[feishu-adapter] Failed to resolve bot identity:', - err instanceof Error ? err.message : err, - ); - } - } - - // ── @Mention detection ────────────────────────────────────── - - /** - * [P2] Check if bot is mentioned — matches against open_id, user_id, union_id. - */ - private isBotMentioned( - mentions?: FeishuMessageEventData['message']['mentions'], - ): boolean { - if (!mentions || this.botIds.size === 0) return false; - return mentions.some((m) => { - const ids = [m.id.open_id, m.id.user_id, m.id.union_id].filter(Boolean) as string[]; - return ids.some((id) => this.botIds.has(id)); - }); - } - - private stripMentionMarkers(text: string): string { - // Feishu uses @_user_N placeholders for mentions - return text.replace(/@_user_\d+/g, '').trim(); - } - - // ── Resource download ─────────────────────────────────────── - - /** - * Download a message resource (image/file/audio/video) via SDK. - * Returns null on failure (caller decides fallback behavior). - */ - private async downloadResource( - messageId: string, - fileKey: string, - resourceType: string, - ): Promise { - if (!this.restClient) return null; - - try { - console.log(`[feishu-adapter] Downloading resource: type=${resourceType}, key=${fileKey}, msgId=${messageId}`); - - const res = await this.restClient.im.messageResource.get({ - path: { - message_id: messageId, - file_key: fileKey, - }, - params: { - type: resourceType === 'image' ? 'image' : 'file', - }, - }); - - if (!res) { - console.warn('[feishu-adapter] messageResource.get returned null/undefined'); - return null; - } - - // SDK returns { writeFile, getReadableStream, headers } - // Try stream approach first, fall back to writeFile + read if stream fails - let buffer: Buffer; - - try { - const readable = res.getReadableStream(); - const chunks: Buffer[] = []; - let totalSize = 0; - - for await (const chunk of readable) { - const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); - totalSize += buf.length; - if (totalSize > MAX_FILE_SIZE) { - console.warn(`[feishu-adapter] Resource too large (>${MAX_FILE_SIZE} bytes), key: ${fileKey}`); - return null; - } - chunks.push(buf); - } - buffer = Buffer.concat(chunks); - } catch (streamErr) { - // Stream approach failed — fall back to writeFile + read - console.warn('[feishu-adapter] Stream read failed, falling back to writeFile:', streamErr instanceof Error ? streamErr.message : streamErr); - - const fs = await import('fs'); - const os = await import('os'); - const path = await import('path'); - const tmpPath = path.join(os.tmpdir(), `feishu-dl-${crypto.randomUUID()}`); - try { - await res.writeFile(tmpPath); - buffer = fs.readFileSync(tmpPath); - if (buffer.length > MAX_FILE_SIZE) { - console.warn(`[feishu-adapter] Resource too large (>${MAX_FILE_SIZE} bytes), key: ${fileKey}`); - return null; - } - } finally { - try { fs.unlinkSync(tmpPath); } catch { /* ignore cleanup errors */ } - } - } - - if (!buffer || buffer.length === 0) { - console.warn('[feishu-adapter] Downloaded resource is empty, key:', fileKey); - return null; - } - - const base64 = buffer.toString('base64'); - const id = crypto.randomUUID(); - const mimeType = MIME_BY_TYPE[resourceType] || 'application/octet-stream'; - const ext = resourceType === 'image' ? 'png' - : resourceType === 'audio' ? 'ogg' - : resourceType === 'video' ? 'mp4' - : 'bin'; - - console.log(`[feishu-adapter] Resource downloaded: ${buffer.length} bytes, key=${fileKey}`); - - return { - id, - name: `${fileKey}.${ext}`, - type: mimeType, - size: buffer.length, - data: base64, - }; - } catch (err) { - console.error( - `[feishu-adapter] Resource download failed (type=${resourceType}, key=${fileKey}):`, - err instanceof Error ? err.stack || err.message : err, - ); - return null; - } - } - - // ── Utilities ─────────────────────────────────────────────── - - private addToDedup(messageId: string): void { - this.seenMessageIds.set(messageId, true); - - // LRU eviction: remove oldest entries when exceeding limit - if (this.seenMessageIds.size > DEDUP_MAX) { - const excess = this.seenMessageIds.size - DEDUP_MAX; - let removed = 0; - for (const key of this.seenMessageIds.keys()) { - if (removed >= excess) break; - this.seenMessageIds.delete(key); - removed++; - } - } - } -} +import { registerAdapterFactory } from '../channel-adapter'; +import { ChannelPluginAdapter } from '../../channels/channel-plugin-adapter'; +import { FeishuChannelPlugin } from '../../channels/feishu'; // Self-register so bridge-manager can create FeishuAdapter via the registry. -registerAdapterFactory('feishu', () => new FeishuAdapter()); +registerAdapterFactory('feishu', () => + new ChannelPluginAdapter(new FeishuChannelPlugin()) +); diff --git a/src/lib/bridge/bridge-manager.ts b/src/lib/bridge/bridge-manager.ts index 912b27b..dbeb673 100644 --- a/src/lib/bridge/bridge-manager.ts +++ b/src/lib/bridge/bridge-manager.ts @@ -29,6 +29,16 @@ import { sanitizeInput, validateMode, } from './security/validators'; +import { ChannelPluginAdapter } from '../channels/channel-plugin-adapter'; + +/** + * Extract the real platform chat_id from a potentially synthetic thread-session address. + * Thread-session mode encodes addresses as `{real_chat_id}:thread:{root_id}`. + */ +function extractRealChatId(chatId: string): string { + const threadIdx = chatId.indexOf(':thread:'); + return threadIdx >= 0 ? chatId.slice(0, threadIdx) : chatId; +} const GLOBAL_KEY = '__bridge_manager__'; @@ -369,6 +379,15 @@ export function registerAdapter(adapter: BaseChannelAdapter): void { state.adapters.set(adapter.channelType, adapter); } +/** + * Get a running adapter by channel type. + * Returns null if the adapter is not registered. + */ +export function getAdapter(channelType: string): BaseChannelAdapter | null { + const state = getState(); + return state.adapters.get(channelType) ?? null; +} + /** * Run the event loop for a single adapter. * Messages for different sessions are dispatched concurrently; @@ -513,51 +532,90 @@ async function handleMessage( }; } + // ── Card streaming setup (Feishu) ────────────────────────────── + let cardController: import('../channels/types').CardStreamController | null = null; + let cardMessageId: string | null = null; + let cardCreating = false; + let cardBufferedText = ''; + let cardFinalized = false; + + if (!previewState && adapter.getCardStreamController) { + cardController = adapter.getCardStreamController(); + } + const streamCfg = previewState ? getStreamConfig(adapter.channelType) : null; - // Build the onPartialText callback (or undefined if preview not supported) - const onPartialText = (previewState && streamCfg) ? (fullText: string) => { - const ps = previewState!; - const cfg = streamCfg!; - if (ps.degraded) return; + // Build the onPartialText callback — preview streaming OR card streaming + let onPartialText: ((fullText: string) => void) | undefined; - // Truncate to maxChars + ellipsis - ps.pendingText = fullText.length > cfg.maxChars - ? fullText.slice(0, cfg.maxChars) + '...' - : fullText; + if (previewState && streamCfg) { + // Preview-based streaming (Telegram, etc.) + const ps = previewState; + const cfg = streamCfg; + onPartialText = (fullText: string) => { + if (ps.degraded) return; - const delta = ps.pendingText.length - ps.lastSentText.length; - const elapsed = Date.now() - ps.lastSentAt; + ps.pendingText = fullText.length > cfg.maxChars + ? fullText.slice(0, cfg.maxChars) + '...' + : fullText; - if (delta < cfg.minDeltaChars && ps.lastSentAt > 0) { - // Not enough new content — schedule trailing-edge timer if not already set - if (!ps.throttleTimer) { - ps.throttleTimer = setTimeout(() => { - ps.throttleTimer = null; - if (!ps.degraded) flushPreview(adapter, ps, cfg); - }, cfg.intervalMs); + const delta = ps.pendingText.length - ps.lastSentText.length; + const elapsed = Date.now() - ps.lastSentAt; + + if (delta < cfg.minDeltaChars && ps.lastSentAt > 0) { + if (!ps.throttleTimer) { + ps.throttleTimer = setTimeout(() => { + ps.throttleTimer = null; + if (!ps.degraded) flushPreview(adapter, ps, cfg); + }, cfg.intervalMs); + } + return; } - return; - } - if (elapsed < cfg.intervalMs && ps.lastSentAt > 0) { - // Too soon — schedule trailing-edge timer to ensure latest text is sent - if (!ps.throttleTimer) { - ps.throttleTimer = setTimeout(() => { - ps.throttleTimer = null; - if (!ps.degraded) flushPreview(adapter, ps, cfg); - }, cfg.intervalMs - elapsed); + if (elapsed < cfg.intervalMs && ps.lastSentAt > 0) { + if (!ps.throttleTimer) { + ps.throttleTimer = setTimeout(() => { + ps.throttleTimer = null; + if (!ps.degraded) flushPreview(adapter, ps, cfg); + }, cfg.intervalMs - elapsed); + } + return; } - return; - } - // Clear any pending trailing-edge timer and flush immediately - if (ps.throttleTimer) { - clearTimeout(ps.throttleTimer); - ps.throttleTimer = null; - } - flushPreview(adapter, ps, cfg); - } : undefined; + if (ps.throttleTimer) { + clearTimeout(ps.throttleTimer); + ps.throttleTimer = null; + } + flushPreview(adapter, ps, cfg); + }; + } else if (cardController) { + // Card-based streaming (Feishu) + onPartialText = (fullText: string) => { + if (cardCreating) { + cardBufferedText = fullText; + return; + } + + if (!cardMessageId) { + // First call — create the card + cardCreating = true; + cardBufferedText = fullText; + cardController!.create(msg.address.chatId, fullText, msg.messageId).then((msgId) => { + cardCreating = false; + cardMessageId = msgId || null; + // Flush any buffered text that arrived during creation + if (cardMessageId && cardBufferedText && cardBufferedText !== fullText) { + cardController!.update(cardMessageId, cardBufferedText).catch(() => {}); + } + }).catch(() => { + cardCreating = false; + }); + return; + } + + cardController!.update(cardMessageId, fullText).catch(() => {}); + }; + } try { // Pass permission callback so requests are forwarded to IM immediately @@ -580,7 +638,13 @@ async function handleMessage( // Send response text — render via channel-appropriate format if (result.responseText) { - await deliverResponse(adapter, msg.address, result.responseText, binding.codepilotSessionId, msg.messageId); + if (cardController && cardMessageId) { + // Finalize streaming card with final content + await cardController.finalize(cardMessageId, result.responseText); + cardFinalized = true; + } else { + await deliverResponse(adapter, msg.address, result.responseText, binding.codepilotSessionId, msg.messageId); + } } else if (result.hasError) { const errorResponse: OutboundMessage = { address: msg.address, @@ -614,6 +678,11 @@ async function handleMessage( adapter.endPreview?.(msg.address.chatId, previewState.draftId); } + // Clean up card streaming state + if (cardController && cardMessageId && !cardFinalized) { + cardController.finalize(cardMessageId, '⚠️ Response interrupted.').catch(() => {}); + } + state.activeTasks.delete(binding.codepilotSessionId); // Notify adapter that message processing ended adapter.onMessageEnd?.(msg.address.chatId); @@ -674,6 +743,8 @@ async function handleCommand( '/sessions - List recent sessions', '/stop - Stop current session', '/perm allow|allow_session|deny <id> - Respond to permission', + '/history [count] - Show recent messages', + '/search <keyword> - Search messages', '/help - Show this help', ].join('\n'); break; @@ -800,6 +871,94 @@ async function handleCommand( break; } + case '/history': { + // Fetch recent messages from the current chat + if (!(adapter instanceof ChannelPluginAdapter)) { + response = 'History is not supported for this channel type.'; + break; + } + const plugin = adapter.getPlugin(); + if (!plugin.getCardStreamController && !(plugin as any).meta?.channelType) { + response = 'History is not available.'; + break; + } + // Use message-actions if the plugin has Feishu-type capabilities + try { + const { readMessages } = await import('../channels/feishu/message-actions'); + const restClient = (plugin as any).gateway?.getRestClient?.(); + if (!restClient) { + response = 'Channel not connected.'; + break; + } + const pageSize = parseInt(args, 10) || 10; + const realChatId = extractRealChatId(msg.address.chatId); + const result = await readMessages(restClient, realChatId, { pageSize }); + if (result.items.length === 0) { + response = 'No messages found.'; + } else { + const lines = [`Recent ${result.items.length} messages:`, '']; + for (const item of result.items) { + const time = item.createTime ? new Date(parseInt(item.createTime, 10) * 1000).toLocaleString() : '?'; + let content = ''; + try { + const parsed = JSON.parse(item.content); + content = (parsed.text ?? '').slice(0, 80); + } catch { + content = item.content.slice(0, 80); + } + lines.push(`[${time}] ${escapeHtml(content)}`); + } + if (result.hasMore) lines.push('\n(more messages available)'); + response = lines.join('\n'); + } + } catch (err) { + response = `Failed to fetch history: ${err instanceof Error ? escapeHtml(err.message) : 'unknown error'}`; + } + break; + } + + case '/search': { + if (!args) { + response = 'Usage: /search <keyword>'; + break; + } + if (!(adapter instanceof ChannelPluginAdapter)) { + response = 'Search is not supported for this channel type.'; + break; + } + try { + const { searchMessages } = await import('../channels/feishu/message-actions'); + const plugin = adapter.getPlugin(); + const restClient = (plugin as any).gateway?.getRestClient?.(); + if (!restClient) { + response = 'Channel not connected.'; + break; + } + const realChatId = extractRealChatId(msg.address.chatId); + const result = await searchMessages(restClient, realChatId, args, { pageSize: 10 }); + if (result.items.length === 0) { + response = `No messages matching "${escapeHtml(args)}".`; + } else { + const lines = [`${result.items.length} result(s) for "${escapeHtml(args)}":`, '']; + for (const item of result.items) { + const time = item.createTime ? new Date(parseInt(item.createTime, 10) * 1000).toLocaleString() : '?'; + let content = ''; + try { + const parsed = JSON.parse(item.content); + content = (parsed.text ?? '').slice(0, 100); + } catch { + content = item.content.slice(0, 100); + } + lines.push(`[${time}] ${escapeHtml(content)}`); + } + response = lines.join('\n'); + } + } catch (err) { + response = `Search failed: ${err instanceof Error ? escapeHtml(err.message) : 'unknown error'}`; + } + break; + } + case '/help': response = [ 'CodePilot Bridge Commands', @@ -812,6 +971,8 @@ async function handleCommand( '/sessions - List recent sessions', '/stop - Stop current session', '/perm allow|allow_session|deny <id> - Respond to permission request', + '/history [count] - Show recent messages', + '/search <keyword> - Search messages', '/help - Show this help', ].join('\n'); break; diff --git a/src/lib/bridge/channel-adapter.ts b/src/lib/bridge/channel-adapter.ts index 12cc752..2fe367f 100644 --- a/src/lib/bridge/channel-adapter.ts +++ b/src/lib/bridge/channel-adapter.ts @@ -96,6 +96,12 @@ export abstract class BaseChannelAdapter { * normal delivery path, so this is typically a no-op. */ endPreview?(_chatId: string, _draftId: number): void; + + /** + * Get a card stream controller for streaming card updates. + * Returns null if not supported by this adapter. + */ + getCardStreamController?(): import('../channels/types').CardStreamController | null; } // ── Adapter Registry ──────────────────────────────────────────── diff --git a/src/lib/bridge/conversation-engine.ts b/src/lib/bridge/conversation-engine.ts index f290c5b..689a06c 100644 --- a/src/lib/bridge/conversation-engine.ts +++ b/src/lib/bridge/conversation-engine.ts @@ -37,10 +37,25 @@ function loadMcpServers(): Record | undefined { }; const userConfig = readJson(path.join(os.homedir(), '.claude.json')); const settings = readJson(path.join(os.homedir(), '.claude', 'settings.json')); + // Also read project-level .mcp.json + const projectMcp = readJson(path.join(process.cwd(), '.mcp.json')); const merged = { ...((userConfig.mcpServers || {}) as Record), ...((settings.mcpServers || {}) as Record), + ...((projectMcp.mcpServers || {}) as Record), }; + // Resolve ${...} placeholders in env values against DB settings + for (const server of Object.values(merged)) { + if (server.env) { + for (const [key, value] of Object.entries(server.env)) { + if (typeof value === 'string' && value.startsWith('${') && value.endsWith('}')) { + const settingKey = value.slice(2, -1); + const resolved = getSetting(settingKey); + server.env[key] = resolved || ''; + } + } + } + } return Object.keys(merged).length > 0 ? merged : undefined; } catch { return undefined; diff --git a/src/lib/db.ts b/src/lib/db.ts index 73dfc6f..7329197 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -698,6 +698,19 @@ function migrateDb(db: Database.Database): void { if (permLinkColNames.length > 0 && !permLinkColNames.includes('resolved')) { db.exec("ALTER TABLE channel_permission_links ADD COLUMN resolved INTEGER NOT NULL DEFAULT 0"); } + + // Channel configs table (structured config for channel plugins) + db.exec(` + CREATE TABLE IF NOT EXISTS channel_configs ( + id TEXT PRIMARY KEY, + channel_type TEXT NOT NULL, + account_id TEXT NOT NULL DEFAULT 'default', + config_json TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')), + UNIQUE(channel_type, account_id) + ); + `); } // ==========================================