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..8520396 100644 --- a/docs/handover/bridge-system.md +++ b/docs/handover/bridge-system.md @@ -19,42 +19,70 @@ src/lib/bridge/ │ ├── ir.ts # Markdown → IR 中间表示解析器(基于 markdown-it) │ ├── render.ts # IR → 格式化输出的通用标记渲染器 │ └── telegram.ts # Telegram HTML 渲染 + 文件引用保护 + render-first 分片 -├── markdown/ -│ ├── ... # (见上) -│ └── feishu.ts # 飞书 Markdown 处理:hasComplexMarkdown / buildCardContent / buildPostContent / htmlToFeishuMarkdown ├── adapters/ │ ├── index.ts # Adapter 目录文件(side-effect import 自注册所有 adapter) │ ├── telegram-adapter.ts # Telegram 长轮询 + offset 安全水位 + 图片/相册处理 + 自注册 │ ├── telegram-media.ts # Telegram 图片下载、尺寸选择、base64 转换 │ ├── telegram-utils.ts # callTelegramApi / sendMessageDraft / escapeHtml / splitMessage -│ ├── feishu-adapter.ts # 飞书 WSClient + REST 消息收发 + typing 指示器 + 自注册 +│ ├── feishu-adapter.ts # 薄代理 → ChannelPluginAdapter(FeishuChannelPlugin) │ └── discord-adapter.ts # Discord.js Client + Gateway intents + 按钮交互 + 流式预览 + 自注册 ├── markdown/ │ └── discord.ts # Discord 消息分片(2000 字符限制)+ 代码围栏平衡 └── security/ ├── rate-limiter.ts # 按 chat 滑动窗口限流(20 条/分钟) └── validators.ts # 路径/SessionID/危险输入校验 + +src/lib/channels/ +├── types.ts # ChannelPlugin / ChannelCapabilities / CardStreamController / ToolCallInfo 接口 +├── channel-plugin-adapter.ts # ChannelPlugin → BaseChannelAdapter 桥接 +└── feishu/ + ├── index.ts # FeishuChannelPlugin 组合入口 + ├── types.ts # FeishuConfig / CardStreamConfig / FeishuBotInfo 等内部类型 + ├── config.ts # 从 settings DB 加载配置 + 校验 + ├── gateway.ts # WSClient 生命周期 + card.action.trigger monkey-patch + 超时保护 + ├── inbound.ts # 入站消息解析 + 内容提取 + 资源下载 + ├── outbound.ts # 出站消息渲染(post md / interactive card / reaction)+ Markdown 优化 + ├── policy.ts # 用户授权 + DM/群聊策略 + ├── identity.ts # Bot 身份解析 + @mention 检测 + └── card-controller.ts # CardKit v2 流式卡片(create/update/finalize/thinking/toolCalls) ``` ## 数据流 -### 飞书 +### 飞书(V2 — ChannelPlugin 架构 + 流式卡片) ``` 飞书消息 → WSClient(WebSocket) → EventDispatcher - → im.message.receive_v1 → handleIncomingEvent() - → 去重(message_id LRU 1000) → 授权检查 → 群策略过滤 → @提及检查 - → text → parseTextContent() → enqueue() - → image → downloadResource(stream/writeFile) → base64 FileAttachment → enqueue() - → post → parsePostContent() 提取文本+图片 → enqueue() - → /perm 文本命令 → 构造 callbackData → enqueue() + → im.message.receive_v1 → FeishuGateway → messageHandler() + → parseInboundMessage() → 去重(message_id) → 授权检查(policy.ts) → 群策略过滤 → @提及检查 + → text/image/post → enqueue() + → card.action.trigger → FeishuGateway.safeCardActionHandler() (2.5s 超时保护) + → FeishuChannelPlugin.cardActionHandler() + → callback_data (perm:allow/deny) → enqueue(callbackMsg) + → callback_data (cwd:/path) → enqueue(callbackMsg) + → action/operation_id → enqueue(syntheticCallback) + → 返回 toast 给飞书客户端 → BridgeManager.runAdapterLoop() → handleMessage() - → deliverResponse(): - → hasComplexMarkdown(代码块/表格)? → sendAsCard() [schema 2.0 markdown] - → 纯文本? → sendAsPost() [msg_type: post, md tag] - → 权限请求 → sendPermissionCard() [schema 2.0 卡片 + /perm 文本命令] + → 普通消息 → processMessage(): + → CardStreamController.create() 创建流式卡片 + → consumeStream() 服务端消费 SSE: + → text → onPartialText → CardStreamController.update() 流式推送 + → tool_use/tool_result → onToolEvent → cardToolCalls 追踪 → updateToolCalls() 渲染 🔄/✅/❌ + → permission_request → PermissionBroker 转发 → 内联按钮卡片(Schema V2 column_set) + → CardStreamController.finalize() 最终渲染 + 页脚(状态+耗时) + → 回调消息 → handlePermissionCallback() / handleCwdCallback() + → 命令 → handleCommand(): + → /cwd 无参 → 项目选择器卡片(内联按钮, turquoise header) + → /new → 继承当前 binding 的 workingDirectory ``` +**飞书 V2 关键变化(相比初版 adapter):** +- **ChannelPlugin 架构**:`FeishuChannelPlugin` 实现 `ChannelPlugin` 接口,通过 `ChannelPluginAdapter` 桥接为 `BaseChannelAdapter`,bridge-manager 无感知 +- **流式卡片**:使用 CardKit v2 API(`cardkit.v2.card.create/streamContent/setStreamingMode/update`),替代旧的 card/post 分流渲染 +- **WSClient 卡片回调**:通过 monkey-patch `handleEventData()` 将 `type:"card"` 重写为 `type:"event"`,使 SDK 的 EventDispatcher 能处理卡片交互事件 +- **配置简化**:移除 encryptKey/verificationToken(WSClient 不需要)、renderMode/blockStreaming(流式始终开启)、footer 开关(始终显示) +- **卡片创建竞态保护**:`cardCreatePromise` 确保 finalize 路径不会在 create 完成前执行 + ### Discord ``` @@ -170,30 +198,31 @@ Claude 的回复是 Markdown 格式,Telegram 仅支持有限 HTML 标签(b/i - **降级**:`sendPreview` 返回 `'sent'|'skip'|'degrade'` 三态。400/404(API 不支持)→ 永久降级该 chatId;429/网络错误 → 仅跳过本次。`previewDegraded` Set 在 adapter `stop()` 时清空。 - **线程安全**:`processWithSessionLock` 保证同 session 串行 → 同时刻只有一个 `previewState`。多个 in-flight `sendMessageDraft` 安全:Telegram 对同 `draft_id` last-write-wins。 -**13. 飞书适配器 — WSClient + 渲染分流** -飞书使用 `@larksuiteoapi/node-sdk` 的 `WSClient`(长连接 WebSocket)接收事件,`Client`(REST)发送消息和下载资源。与 Telegram 的 HTTP 长轮询不同,WSClient 由 SDK 管理重连。消息去重使用内存 Map LRU(上限 1000),无需持久化 offset。 +**13. 飞书 ChannelPlugin 架构** +飞书从原 `BaseChannelAdapter` 子类迁移为 `ChannelPlugin` 实现。`src/lib/channels/feishu/` 拆分为独立模块:`gateway.ts`(WSClient 生命周期)、`inbound.ts`(消息解析)、`outbound.ts`(消息发送 + Markdown 优化)、`policy.ts`(授权策略)、`card-controller.ts`(流式卡片)、`config.ts`(配置加载)。通过 `ChannelPluginAdapter` 桥接为 `BaseChannelAdapter`,bridge-manager 无需修改。WSClient 由 SDK 管理重连,消息去重使用内存 LRU。 -**14. 飞书渲染策略 — Card vs Post** -Claude 回复按内容分流渲染(对齐 Openclaw 方案): -- 含代码块(` ``` `)或表格 → `msg_type: 'interactive'`,schema 2.0 卡片(`{ tag: 'markdown', content }` 元素),代码高亮和表格正常渲染。 -- 纯文本 → `msg_type: 'post'`,`{ tag: 'md', text }` 格式,渲染粗体、斜体、行内代码、链接。 -- 每层发送失败自动降级:card → post → text。 +**14. 飞书流式卡片渲染** +所有 Claude 回复通过 CardKit v2 流式卡片输出(替代旧的 card/post 分流)。流程:`cardController.create()` 创建卡片 → `update()` 节流推送文本(200ms)→ `finalize()` 停止流式 + 渲染最终内容 + 页脚。卡片支持: +- **Thinking 状态**:文本到达前显示 `💭 Thinking...` +- **Tool 进度**:`🔄 Running` / `✅ Complete` / `❌ Error` 实时显示 +- **Markdown 优化**:标题降级(H1→H4, H2-6→H5)、表格间距、代码块填充、无效图片 key 剥离 +- **页脚**:状态 emoji(✅/⚠️/❌)+ 耗时,始终显示 -`markdown/feishu.ts` 的 `hasComplexMarkdown()` 负责路由判断,`buildCardContent()` / `buildPostContent()` 构建消息体。 +非卡片消息(命令响应等)使用 `post` 格式 + `md` tag。注意:post md tag 不支持 HTML `
`(会渲染为字面文本),必须用空行 `\n\n` 代替。 -**15. 飞书权限交互 — 无按钮,文本命令兜底** -**关键限制**:飞书卡片交互回调(card.action.trigger)需要 HTTP webhook 端点,不支持通过 WSClient 长连接接收。Openclaw 通过 `http.createServer()` + `Lark.adaptDefault()` 暴露公网 webhook 解决。CodePilot 是桌面应用无公网端点,因此: -- Schema 2.0 不支持 `action` 标签(错误码 200861) -- Schema 1.0 的 `action` 标签可渲染按钮,但点击报 200340(无 webhook 端点接收回调) -- **最终方案**:权限卡片使用 schema 2.0 markdown 展示信息 + `/perm` 文本命令。用户复制命令发送即可审批。`processIncomingEvent()` 检测 `/perm` 前缀并构造 `callbackData`,走 `permission-broker.handlePermissionCallback()` 标准流程。 +**15. 飞书权限交互 — Schema V2 内联按钮** +通过 monkey-patch WSClient 的 `handleEventData()` 方法,将 `type:"card"` 事件重写为 `type:"event"`,使 SDK 的 EventDispatcher 能接收 `card.action.trigger` 回调。这解决了之前的 200340 错误(无 webhook 端点)。Schema V2 卡片不支持 `action` tag(错误码 200861),按钮使用 `column_set` + `column` + `button` 布局。按钮 value 中嵌入 `chatId` 作为兜底(WSClient 回调的 context 字段可能缺失)。Gateway 层提供 2.5s 超时保护,确保 3s 内必定返回 toast 响应。 **16. 飞书 Typing 指示器 — Emoji Reaction** -飞书无 typing indicator API。使用 Openclaw 方案:`onMessageStart()` 在用户消息上添加 "Typing" emoji reaction(`im.messageReaction.create`),`onMessageEnd()` 删除。`lastIncomingMessageId` Map 追踪每个 chat 的最新消息 ID。非关键路径,fire-and-forget。 +`FeishuChannelPlugin.onMessageStart()` 在用户消息上添加 "Typing" emoji reaction(`im.messageReaction.create`),`onMessageEnd()` 删除。`lastMessageIdByChat` Map 追踪每个 chat 的最新消息 ID,`activeReactions` Map 追踪活跃 reaction ID。非关键路径,fire-and-forget。 **17. 飞书 @提及检测** -通过 `/bot/v3/info/` REST API 获取 bot 的 `open_id`/`bot_id`,存入 `botIds` Set。群聊消息检查 `event.message.mentions` 数组中是否有匹配的 ID。文本中的 `@_user_N` 占位符由 `stripMentionMarkers()` 清理。 +`inbound.ts` 解析 `event.message.mentions` 数组检测 bot 是否被 @。bot 身份通过 `identity.ts` 的 `/bot/v3/info/` REST API 获取(`open_id`/`bot_id`)。文本中的 `@_user_N` 占位符由 `stripMentionMarkers()` 清理。 -**18. Telegram 通知模式互斥** +**18. 飞书 Bridge 单操作者模型** +当前飞书 bridge 按「单操作者桌面应用」模型设计。虽然有 dmPolicy/groupPolicy/allowFrom 等多入口访问控制,但所有飞书聊天绑定共享同一操作者身份。`/cwd` 项目选择器展示同一 Feishu 渠道下所有活跃项目目录,作为「最近项目快捷切换」使用,不做 chat-level 隔离。如果未来需要多用户/多租户隔离,`/cwd` picker 应按 userId 或 chatId 进一步收窄数据源。 + +**19. Telegram 通知模式互斥** `telegram-bot.ts` 的通知功能(UI 会话通知)与 bridge 模式互斥。通过 `globalThis.__codepilot_bridge_mode_active` 标志协调(存 globalThis 防 HMR 重置)。Bridge 启动时设 `true`,4 个 notify 函数检查此标志后提前返回。 ## 设置项(settings 表) @@ -218,10 +247,12 @@ Claude 回复按内容分流渲染(对齐 Openclaw 方案): | bridge_feishu_app_id | 飞书应用 App ID | | bridge_feishu_app_secret | 飞书应用 App Secret(API 返回脱敏) | | bridge_feishu_domain | 平台域名:`feishu`(默认)或 `lark` | -| bridge_feishu_allowed_users | 允许的 open_id/chat_id(逗号分隔,空=不限) | +| bridge_feishu_allow_from | 允许的 open_id(逗号分隔,`*`=不限) | +| bridge_feishu_dm_policy | 私信策略:`open`(默认)/ `pairing` / `allowlist` / `disabled` | +| bridge_feishu_thread_session | 每话题独立上下文(默认 false) | | bridge_feishu_group_policy | 群消息策略:`open`(默认)/ `allowlist` / `disabled` | | bridge_feishu_group_allow_from | 群聊白名单 chat_id(逗号分隔) | -| bridge_feishu_require_mention | 群聊需要 @bot 才触发(默认 true) | +| bridge_feishu_require_mention | 群聊需要 @bot 才触发(默认 false) | ## API 路由 @@ -253,9 +284,10 @@ Claude 回复按内容分流渲染(对齐 Openclaw 方案): - `src/components/bridge/BridgeSection.tsx` — Bridge 设置 UI(一级导航 /bridge),含 Telegram/飞书通道开关 - `src/components/bridge/BridgeLayout.tsx` — 侧边栏导航(Telegram + Feishu 入口) - `src/components/bridge/TelegramBridgeSection.tsx` — Telegram 凭据 + 白名单设置 UI(/bridge#telegram) -- `src/components/bridge/FeishuBridgeSection.tsx` — 飞书凭据 + 群聊策略 + 域名选择 UI(/bridge#feishu) -- `src/app/api/settings/feishu/route.ts` — 飞书设置读写 API +- `src/components/bridge/FeishuBridgeSection.tsx` — 飞书设置 UI:凭据 + 访问与行为(2 卡片 2 保存按钮 + 脏状态追踪) +- `src/app/api/settings/feishu/route.ts` — 飞书设置读写 API(简化后 10 个 key) - `src/app/api/settings/feishu/verify/route.ts` — 飞书凭据验证 API(测试 token 获取 + bot info) +- `src/lib/channels/` — V2 ChannelPlugin 架构(见目录结构) - `electron/main.ts` — 窗口关闭时 bridge 活跃则保持后台运行;启动时通过 POST `auto-start` 触发桥接恢复 - `src/app/api/settings/telegram/verify/route.ts` — 支持 `register_commands` action 注册 Telegram 命令菜单 @@ -281,3 +313,51 @@ 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、threadReply、search、history、reactions 能力声明 + - `CardStreamController`:流式卡片接口(create/update/finalize/setThinking/updateToolCalls) + - `ToolCallInfo`:工具调用进度追踪(id/name/status) + +2. **ChannelPluginAdapter** (`src/lib/channels/channel-plugin-adapter.ts`) + - 将 `ChannelPlugin` 桥接为 `BaseChannelAdapter` + - 自动代理 `getCardStreamController()`、`onMessageStart/End()` 等 + - bridge-manager 无需修改即可使用新插件 + +3. **飞书模块拆分** (`src/lib/channels/feishu/`) + - `types.ts` — FeishuConfig、CardStreamConfig(简化后无 renderMode/blockStreaming/footer 开关) + - `config.ts` — 从 settings DB 加载配置,cardStreamConfig 始终启用(footer 始终显示) + - `gateway.ts` — WSClient 生命周期 + card.action.trigger monkey-patch + 2.5s 超时保护 + - `inbound.ts` — 入站消息处理 + 内容解析 + 资源下载 + - `outbound.ts` — 出站渲染(post md + interactive card)+ optimizeMarkdown() + 权限/CWD 卡片 + - `identity.ts` — Bot 身份解析 + @mention 检测 + - `policy.ts` — 用户授权 + DM/群聊策略 + - `card-controller.ts` — CardKit v2 流式卡片(thinking/streaming/tool progress/footer) + - `index.ts` — FeishuChannelPlugin 组合入口 + Typing reaction 管理 + +4. **流式卡片 + 工具进度** (`bridge-manager.ts` + `card-controller.ts`) + - `onPartialText` 回调 → CardStreamController.update() 节流推送 + - `onToolEvent` 回调 → cardToolCalls[] 追踪 → updateToolCalls() 实时渲染 + - Tool-first 回合(无文本直接调工具):onToolEvent 自动 bootstrap 卡片 + - `cardCreatePromise` 竞态保护:finalize 路径 await 创建完成后再执行 + +5. **权限内联按钮** + - Schema V2 `column_set` + `column` + `button` 布局(`action` tag 已废弃 → 200861) + - 权限卡片:蓝色 header + lock icon + Allow(primary)/Deny(danger) 按钮 + 5 分钟过期提示 + - CWD 选择器卡片:turquoise header + folder icon + 垂直堆叠按钮 + 📍 当前项目高亮 + - 按钮 value 嵌入 chatId 兜底(WSClient 回调 context 可能缺失) + +6. **MCP 残留剥离** + - 移除 `.mcp.json` 中 feishu MCP 入口 + - 移除 `@codepilot/feishu-mcp` workspace 依赖 + - 原 feishu-adapter.ts 改为薄代理(~15 行) + +7. **设置 UI 简化** (`FeishuBridgeSection.tsx`) + - 移除:encryptKey、verificationToken、renderMode、blockStreaming、footer 开关 + - 合并为 2 个卡片:凭据 + 访问与行为 + - 保存按钮脏状态追踪:修改后显示"保存",保存后显示"已保存" 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 79571fc..0830fb3 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", @@ -7955,6 +8005,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 +9547,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 +10301,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 +10381,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 +10655,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 +11578,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 +11614,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 +12257,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 +13828,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 +14763,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 +15298,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 +15421,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 +15435,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 +16651,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 +17633,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 +18195,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 +18477,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 +20659,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 +21677,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 +23667,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 +25333,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 +25596,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 +25800,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 +25868,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 +26826,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", 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/app/api/settings/feishu/route.ts b/src/app/api/settings/feishu/route.ts index b0254e5..2e3e8bd 100644 --- a/src/app/api/settings/feishu/route.ts +++ b/src/app/api/settings/feishu/route.ts @@ -11,7 +11,9 @@ const FEISHU_KEYS = [ 'bridge_feishu_app_id', 'bridge_feishu_app_secret', 'bridge_feishu_domain', - 'bridge_feishu_allowed_users', + 'bridge_feishu_allow_from', + 'bridge_feishu_dm_policy', + 'bridge_feishu_thread_session', 'bridge_feishu_group_policy', 'bridge_feishu_group_allow_from', 'bridge_feishu_require_mention', @@ -23,7 +25,7 @@ export async function GET() { for (const key of FEISHU_KEYS) { const value = getSetting(key); if (value !== undefined) { - // Mask app secret for security + // Mask sensitive fields for security if (key === 'bridge_feishu_app_secret' && value.length > 8) { result[key] = '***' + value.slice(-8); } else { @@ -52,7 +54,7 @@ export async function PUT(request: NextRequest) { if (!FEISHU_KEYS.includes(key as typeof FEISHU_KEYS[number])) continue; const strValue = String(value ?? '').trim(); - // Don't overwrite secret if user sent the masked version back + // Don't overwrite secrets if user sent the masked version back if (key === 'bridge_feishu_app_secret' && strValue.startsWith('***')) { continue; } diff --git a/src/components/bridge/FeishuBridgeSection.tsx b/src/components/bridge/FeishuBridgeSection.tsx index 7cdd170..4870e6f 100644 --- a/src/components/bridge/FeishuBridgeSection.tsx +++ b/src/components/bridge/FeishuBridgeSection.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useEffect } from "react"; +import { useState, useCallback, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -21,7 +21,9 @@ interface FeishuBridgeSettings { bridge_feishu_app_id: string; bridge_feishu_app_secret: string; bridge_feishu_domain: string; - bridge_feishu_allowed_users: string; + bridge_feishu_allow_from: string; + bridge_feishu_dm_policy: string; + bridge_feishu_thread_session: string; bridge_feishu_group_policy: string; bridge_feishu_group_allow_from: string; bridge_feishu_require_mention: string; @@ -31,23 +33,68 @@ const DEFAULT_SETTINGS: FeishuBridgeSettings = { bridge_feishu_app_id: "", bridge_feishu_app_secret: "", bridge_feishu_domain: "feishu", - bridge_feishu_allowed_users: "", + bridge_feishu_allow_from: "", + bridge_feishu_dm_policy: "open", + bridge_feishu_thread_session: "false", bridge_feishu_group_policy: "open", bridge_feishu_group_allow_from: "", bridge_feishu_require_mention: "false", }; +/** SaveButton: shows Save / Saving / Saved based on dirty + saving state. */ +function SaveButton({ + dirty, + saving, + onClick, + label, + savedLabel, +}: { + dirty: boolean; + saving: boolean; + onClick: () => void; + label: string; + savedLabel: string; +}) { + return ( + + ); +} + export function FeishuBridgeSection() { - const [, setSettings] = - useState(DEFAULT_SETTINGS); + // ── Credentials state ── const [appId, setAppId] = useState(""); const [appSecret, setAppSecret] = useState(""); const [domain, setDomain] = useState("feishu"); - const [allowedUsers, setAllowedUsers] = useState(""); + const [credentialsSaving, setCredentialsSaving] = useState(false); + const [credentialsDirty, setCredentialsDirty] = useState(false); + const savedCredentials = useRef({ appId: "", appSecret: "", domain: "feishu" }); + + // ── Access & Behavior state ── + const [allowFrom, setAllowFrom] = useState(""); + const [dmPolicy, setDmPolicy] = useState("open"); + const [threadSession, setThreadSession] = useState(false); const [groupPolicy, setGroupPolicy] = useState("open"); const [groupAllowFrom, setGroupAllowFrom] = useState(""); const [requireMention, setRequireMention] = useState(false); - const [saving, setSaving] = useState(false); + const [behaviorSaving, setBehaviorSaving] = useState(false); + const [behaviorDirty, setBehaviorDirty] = useState(false); + const savedBehavior = useRef({ + allowFrom: "", dmPolicy: "open", threadSession: false, + groupPolicy: "open", groupAllowFrom: "", requireMention: false, + }); + + // ── Verify state ── const [verifying, setVerifying] = useState(false); const [verifyResult, setVerifyResult] = useState<{ ok: boolean; @@ -55,20 +102,59 @@ export function FeishuBridgeSection() { } | null>(null); const { t } = useTranslation(); + // ── Dirty tracking ── + useEffect(() => { + const s = savedCredentials.current; + setCredentialsDirty( + appId !== s.appId || appSecret !== s.appSecret || domain !== s.domain + ); + }, [appId, appSecret, domain]); + + useEffect(() => { + const s = savedBehavior.current; + setBehaviorDirty( + allowFrom !== s.allowFrom || + dmPolicy !== s.dmPolicy || + threadSession !== s.threadSession || + groupPolicy !== s.groupPolicy || + groupAllowFrom !== s.groupAllowFrom || + requireMention !== s.requireMention + ); + }, [allowFrom, dmPolicy, threadSession, groupPolicy, groupAllowFrom, requireMention]); + + // ── Fetch ── const fetchSettings = useCallback(async () => { try { const res = await fetch("/api/settings/feishu"); if (res.ok) { const data = await res.json(); const s = { ...DEFAULT_SETTINGS, ...data.settings }; - setSettings(s); setAppId(s.bridge_feishu_app_id); setAppSecret(s.bridge_feishu_app_secret); setDomain(s.bridge_feishu_domain || "feishu"); - setAllowedUsers(s.bridge_feishu_allowed_users); + setAllowFrom(s.bridge_feishu_allow_from); + setDmPolicy(s.bridge_feishu_dm_policy || "open"); + setThreadSession(s.bridge_feishu_thread_session === "true"); setGroupPolicy(s.bridge_feishu_group_policy || "open"); setGroupAllowFrom(s.bridge_feishu_group_allow_from); setRequireMention(s.bridge_feishu_require_mention === "true"); + + // Snapshot as "saved" baseline + savedCredentials.current = { + appId: s.bridge_feishu_app_id, + appSecret: s.bridge_feishu_app_secret, + domain: s.bridge_feishu_domain || "feishu", + }; + savedBehavior.current = { + allowFrom: s.bridge_feishu_allow_from, + dmPolicy: s.bridge_feishu_dm_policy || "open", + threadSession: s.bridge_feishu_thread_session === "true", + groupPolicy: s.bridge_feishu_group_policy || "open", + groupAllowFrom: s.bridge_feishu_group_allow_from, + requireMention: s.bridge_feishu_require_mention === "true", + }; + setCredentialsDirty(false); + setBehaviorDirty(false); } } catch { // ignore @@ -79,42 +165,60 @@ export function FeishuBridgeSection() { fetchSettings(); }, [fetchSettings]); - const saveSettings = async (updates: Partial) => { - setSaving(true); + // ── Save helpers ── + const saveToApi = async (updates: Partial) => { + const res = await fetch("/api/settings/feishu", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ settings: updates }), + }); + return res.ok; + }; + + const handleSaveCredentials = async () => { + setCredentialsSaving(true); try { - const res = await fetch("/api/settings/feishu", { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ settings: updates }), - }); - if (res.ok) { - setSettings((prev) => ({ ...prev, ...updates })); + const updates: Partial = { + bridge_feishu_app_id: appId, + bridge_feishu_domain: domain, + }; + if (appSecret && !appSecret.startsWith("***")) { + updates.bridge_feishu_app_secret = appSecret; + } + if (await saveToApi(updates)) { + savedCredentials.current = { appId, appSecret, domain }; + setCredentialsDirty(false); } } catch { // ignore } finally { - setSaving(false); + setCredentialsSaving(false); } }; - const handleSaveCredentials = () => { - const updates: Partial = { - bridge_feishu_app_id: appId, - bridge_feishu_domain: domain, - }; - if (appSecret && !appSecret.startsWith("***")) { - updates.bridge_feishu_app_secret = appSecret; + const handleSaveBehavior = async () => { + setBehaviorSaving(true); + try { + const ok = await saveToApi({ + bridge_feishu_allow_from: allowFrom, + bridge_feishu_dm_policy: dmPolicy, + bridge_feishu_thread_session: threadSession ? "true" : "false", + bridge_feishu_group_policy: groupPolicy, + bridge_feishu_group_allow_from: groupAllowFrom, + bridge_feishu_require_mention: requireMention ? "true" : "false", + }); + if (ok) { + savedBehavior.current = { + allowFrom, dmPolicy, threadSession, + groupPolicy, groupAllowFrom, requireMention, + }; + setBehaviorDirty(false); + } + } catch { + // ignore + } finally { + setBehaviorSaving(false); } - saveSettings(updates); - }; - - const handleSaveGroupSettings = () => { - saveSettings({ - bridge_feishu_allowed_users: allowedUsers, - bridge_feishu_group_policy: groupPolicy, - bridge_feishu_group_allow_from: groupAllowFrom, - bridge_feishu_require_mention: requireMention ? "true" : "false", - }); }; const handleVerify = async () => { @@ -162,7 +266,7 @@ export function FeishuBridgeSection() { return (
- {/* App Credentials */} + {/* ── App Credentials ── */}
- +