Add Feishu and Lark platform support for thread bridge#208
Conversation
- Bidirectional messaging via Feishu bot - Same command set as Telegram bridge - Interactive card buttons for thread picker - turn/completed notification forwarding
- Add feishuBridge to SharedBridgeState - Add /codex-api/feishu/configure-bot, /feishu/config, /feishu/status routes - Persist config to ~/.codex/feishu-bridge.json - Auto-start on middleware creation, stop on dispose
- FeishuStatus, FeishuConfig types - configureFeishuBot, getFeishuConfig, getFeishuStatus functions
- App ID, App Secret, allowed user open_ids fields - Status display and save functionality - Positioned alongside existing Telegram config
- Threads grouped under project folder headers - Divider between groups - Indigo card header style
- Each button in its own action row for vertical layout - Limit to 6 threads per project group - Show remaining count when truncated
- Fetch all threads with cursor pagination instead of limit 20 - Ensures no projects are missing from the thread picker
- Replace plain text messages with markdown card format - Add wathet header with Codex branding - Preserve code blocks and markdown formatting
- Replace plain text history with per-message interactive cards - User messages: wathet header with 👤 User - Assistant messages: grey header with 🤖 Assistant - Merge consecutive same-role messages into one card - Full text without truncation - Thread selection shows last 2 messages, /history shows all
- Pass cwd to thread/list API to only show project-local threads - Fixes thread not found error when selecting cross-project threads
- Show ⏳ Thinking... immediately when user sends a message - Edit same message with actual reply when turn completes - Reuse existing thinking message on consecutive sends - Update thinking message with error on failure
- Remove cwd filter to show threads from all projects - Show clear message when thread belongs to different project
- Store thread cwd from thread picker button value - Pass stored cwd to turn/start RPC call - Fixes thread not found error for cross-project threads
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR adds a Feishu/Lark bot bridge with server-side bridge logic, configuration/status APIs, a frontend settings panel, SDK integration, and README documentation. ChangesFeishu Bot Bridge
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant FeishuUser
participant FeishuThreadBridge
participant AppServer
participant CodexAppServerBridge
FeishuUser->>FeishuThreadBridge: send message or command
FeishuThreadBridge->>FeishuThreadBridge: validate allowlist and dedupe
FeishuThreadBridge->>AppServer: forward text to thread
AppServer-->>CodexAppServerBridge: turn/completed notification
CodexAppServerBridge-->>FeishuThreadBridge: notify completion
FeishuThreadBridge->>AppServer: read latest assistant reply
FeishuThreadBridge->>FeishuUser: send reply or update thinking message
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Summary by QodoAdd Feishu/Lark domain selection for Feishu thread bridge
AI Description
Diagram
High-Level Assessment
Files changed (6)
|
Code Review by Qodo
1. Feishu manual tests missing
|
| <button class="sidebar-settings-row" type="button" aria-live="polite" @click="isFeishuConfigOpen = !isFeishuConfigOpen"> | ||
| <span class="sidebar-settings-label">{{ t('Feishu') }}</span> | ||
| <span class="sidebar-settings-value">{{ feishuStatusText }}</span> | ||
| </button> | ||
| <div v-if="isFeishuConfigOpen" class="sidebar-settings-telegram-panel"> | ||
| <div class="sidebar-settings-row sidebar-settings-row--select" style="padding: 0"> | ||
| <span class="sidebar-settings-label">{{ t('Platform') }}</span> | ||
| <div class="sidebar-settings-segmented" role="group" :aria-label="t('Feishu platform')"> | ||
| <button | ||
| type="button" | ||
| class="sidebar-settings-segmented-option" | ||
| :class="{ 'is-active': feishuDomainDraft === 'feishu' }" | ||
| :disabled="isFeishuSaving" | ||
| @click="feishuDomainDraft = 'feishu'" | ||
| > | ||
| Feishu | ||
| </button> | ||
| <button | ||
| type="button" | ||
| class="sidebar-settings-segmented-option" | ||
| :class="{ 'is-active': feishuDomainDraft === 'lark' }" | ||
| :disabled="isFeishuSaving" | ||
| @click="feishuDomainDraft = 'lark'" | ||
| > | ||
| Lark | ||
| </button> | ||
| </div> | ||
| </div> | ||
| <label class="sidebar-settings-field"> | ||
| <span class="sidebar-settings-field-label">{{ t('App ID') }}</span> | ||
| <input | ||
| v-model="feishuAppIdDraft" | ||
| class="sidebar-settings-input" | ||
| type="text" | ||
| placeholder="cli_xxxxxxxxxx" | ||
| autocomplete="off" | ||
| spellcheck="false" | ||
| > | ||
| </label> | ||
| <label class="sidebar-settings-field"> | ||
| <span class="sidebar-settings-field-label">{{ t('App Secret') }}</span> | ||
| <input | ||
| v-model="feishuAppSecretDraft" | ||
| class="sidebar-settings-input" | ||
| type="password" | ||
| placeholder="xxxxxxxxxxxxxxxxxxxxxxxx" | ||
| autocomplete="off" | ||
| spellcheck="false" | ||
| > | ||
| </label> | ||
| <label class="sidebar-settings-field"> | ||
| <span class="sidebar-settings-field-label">{{ t('Allowed Feishu user open_ids') }}</span> | ||
| <textarea | ||
| v-model="feishuAllowedUserIdsDraft" | ||
| class="sidebar-settings-textarea" | ||
| rows="3" | ||
| placeholder="ou_xxxxxxxxxx ou_yyyyyyyyyy" | ||
| spellcheck="false" | ||
| /> | ||
| </label> | ||
| <div class="sidebar-settings-field-help"> | ||
| {{ t('Put one Feishu open_id per line or separate them with commas. Use `*` to allow all Feishu users. You can find open_id in the bot rejection message.') }} | ||
| </div> | ||
| <div v-if="feishuConfigError" class="sidebar-settings-telegram-error"> | ||
| <span>{{ feishuConfigError }}</span> | ||
| <a class="visible-error-feedback" :href="feedbackMailto" @click="prepareFeedbackLink($event, feishuConfigError)">{{ t('Send feedback') }}</a> | ||
| </div> | ||
| <div class="sidebar-settings-telegram-actions"> | ||
| <button | ||
| class="sidebar-settings-telegram-save" | ||
| type="button" | ||
| :disabled="isFeishuSaving" | ||
| @click="saveFeishuConfig" | ||
| > | ||
| {{ isFeishuSaving ? t('Saving…') : t('Save Feishu config') }} | ||
| </button> | ||
| </div> | ||
| </div> |
There was a problem hiding this comment.
1. Feishu manual tests missing 📘 Rule violation ⚙ Maintainability
The PR adds a new Feishu/Lark thread bridge feature and related settings, but does not add/update domain-scoped manual test documentation under tests/<domain>/ (and keeps tests.md as only an index). Without a new Feishu/Lark manual test section following the required template (prereqs, steps, expected results, rollback), the change is not reproducibly testable.
Agent Prompt
## Issue description
This PR introduces a Feishu/Lark platform selector + Feishu bot bridge configuration and server support, but no new/updated manual test documentation was added under `tests/<domain>/`, and therefore there is no reproducible manual test section for validating the new behavior.
## Issue Context
Repository policy requires feature work to be accompanied by domain-scoped manual test docs under `tests/<domain>/`, while keeping `tests.md` as an index. Manual test entries must include: feature name, prerequisites, exact steps, expected results, and rollback/cleanup notes.
## Fix Focus Areas
- tests/skills-plugins-integrations/index.md[7-14]
- tests/template.md[7-20]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| void readFeishuBridgeConfig() | ||
| .then((config) => { | ||
| if (!config.appId || !config.appSecret) return | ||
| feishuBridge.configureApp(config.appId, config.appSecret, config.domain) | ||
| feishuBridge.configureAllowedUserIds(config.allowedUserIds) | ||
| feishuBridge.start() | ||
| }) |
There was a problem hiding this comment.
2. Env config not started 🐞 Bug ≡ Correctness
FeishuThreadBridge reads FEISHU_* env vars, but createCodexBridgeMiddleware only calls feishuBridge.start() when feishu-bridge.json contains appId/appSecret, so env-only deployments never activate the Feishu WebSocket client. This breaks the documented “use environment variables” setup and leaves the bridge offline despite valid FEISHU_* values.
Agent Prompt
## Issue description
FeishuThreadBridge initializes credentials and defaults from environment variables, but the server only starts the Feishu bridge if the persisted `feishu-bridge.json` contains `appId/appSecret`. As a result, a user who follows the README and sets only `FEISHU_APP_ID/FEISHU_APP_SECRET/FEISHU_ALLOWED_USER_IDS` will have a configured-but-inactive bridge.
## Issue Context
- `FeishuThreadBridge` constructor reads `process.env.FEISHU_*`.
- `createCodexBridgeMiddleware()` only calls `feishuBridge.start()` inside the `readFeishuBridgeConfig().then(...)` block, and returns early when the config file lacks credentials.
## Fix Focus Areas
- src/server/codexAppServerBridge.ts[7545-7561]
- src/server/feishuThreadBridge.ts[203-257]
## Implementation notes
- After reading persisted config, if it’s missing credentials, fall back to the bridge’s current in-memory configuration (from env) and start when `getStatus().configured` is true.
- Optionally also require an allowlist to be present before starting: `status.allowAllUsers || status.allowedUsers > 0`, consistent with the security expectations in the README.
- Consider merging precedence: persisted config overrides env when present; otherwise env drives startup.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (req.method === 'POST' && url.pathname === '/codex-api/feishu/configure-bot') { | ||
| const payload = asRecord(await readJsonBody(req)) | ||
| const appId = typeof payload?.appId === 'string' ? payload.appId.trim() : '' | ||
| const appSecret = typeof payload?.appSecret === 'string' ? payload.appSecret.trim() : '' | ||
| const domain = normalizeFeishuDomain(payload?.domain) | ||
| const rawAllowedUserIds = Array.isArray(payload?.allowedUserIds) ? payload.allowedUserIds : [] | ||
| if (!appId) { | ||
| setJson(res, 400, { error: 'Missing appId' }) | ||
| return | ||
| } | ||
| if (!appSecret) { | ||
| setJson(res, 400, { error: 'Missing appSecret' }) | ||
| return | ||
| } | ||
| const config = normalizeFeishuBridgeConfig({ | ||
| appId, | ||
| appSecret, | ||
| domain, | ||
| allowedUserIds: rawAllowedUserIds, | ||
| }) | ||
| if (config.allowedUserIds.length === 0) { | ||
| setJson(res, 400, { error: 'At least one allowed Feishu user ID is required' }) | ||
| return | ||
| } | ||
|
|
||
| feishuBridge.configureApp(config.appId, config.appSecret, config.domain) | ||
| feishuBridge.configureAllowedUserIds(config.allowedUserIds) | ||
| feishuBridge.start() | ||
| const existingConfig = await readFeishuBridgeConfig() | ||
| await writeFeishuBridgeConfig({ | ||
| appId: config.appId, | ||
| appSecret: config.appSecret, | ||
| domain: config.domain, | ||
| chatIds: existingConfig.chatIds, | ||
| allowedUserIds: config.allowedUserIds, | ||
| }) | ||
| setJson(res, 200, { ok: true }) | ||
| return | ||
| } | ||
|
|
||
| if (req.method === 'GET' && url.pathname === '/codex-api/feishu/config') { | ||
| const config = await readFeishuBridgeConfig() | ||
| setJson(res, 200, { | ||
| data: { | ||
| appId: config.appId, | ||
| appSecret: config.appSecret, | ||
| domain: config.domain, | ||
| allowedUserIds: config.allowedUserIds, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
3. Feishu config api leaks secrets 🐞 Bug ⛨ Security
The new Feishu routes are unauthenticated and include a GET endpoint that returns appSecret, so any client that can reach the server can exfiltrate Feishu credentials and/or reconfigure the allowlist. This is especially risky because the app is explicitly LAN-accessible and these routes lack the localhost-only guard used elsewhere in the same middleware.
Agent Prompt
## Issue description
`/codex-api/feishu/config` returns `appSecret` and `/codex-api/feishu/configure-bot` allows arbitrary reconfiguration, but neither endpoint is protected by a localhost check or any authentication. Since the server is intended to be reachable on the LAN, this enables credential theft and bridge takeover.
## Issue Context
- The middleware already enforces a localhost-only check for some sensitive endpoints (e.g., zen proxy), but not for Feishu.
- Feishu credentials are also persisted to disk in plaintext JSON.
## Fix Focus Areas
- src/server/codexAppServerBridge.ts[9683-9733]
- src/server/codexAppServerBridge.ts[7613-7617]
- src/server/codexAppServerBridge.ts[6184-6193]
- src/App.vue[2474-2526]
- src/api/codexGateway.ts[3424-3475]
## Implementation notes
- Add an access-control guard for Feishu config routes at minimum (e.g., `isLoopbackRemoteAddress(req.socket.remoteAddress)`), consistent with other sensitive endpoints.
- Stop returning secrets from GET `/codex-api/feishu/config` (return a boolean like `hasAppSecret` and/or a masked placeholder).
- Update POST `/codex-api/feishu/configure-bot` to allow `appSecret` to be omitted/empty to mean “keep existing secret” (so the UI doesn’t need to re-fetch or re-send the secret).
- When writing `feishu-bridge.json`, consider restricting file permissions (e.g., mode `0o600`) since it contains an app secret.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
src/server/feishuThreadBridge.ts (2)
341-361: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMisleading name:
sendTextMessagesends an interactive card.
sendTextMessagebuilds aninteractivecard with a header, not a plain text message. It's used for the "⏳ Thinking..." placeholder and its returnedmessage_idis laterpatched. The behavior is fine, but the name obscures intent; consider renaming (e.g.sendCardTextMessage) for clarity.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/feishuThreadBridge.ts` around lines 341 - 361, The helper named sendTextMessage is misleading because it sends an interactive card, not plain text. Rename the method to reflect its actual behavior, such as sendCardTextMessage or similar, and update its call sites in FeishuThreadBridge so the purpose is clear while keeping the existing message creation and returned message_id behavior unchanged.
683-733: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUnbounded thread accumulation in the picker path.
listRecentThreadGroupsloopsthread/list(limit 100) untilnextCursoris exhausted, pushing every thread intoallRowsbefore grouping. On workspaces with many threads this loads the entire thread universe into memory on each/threadsor/startcommand, even though onlyMAX_THREADS_PER_GROUP(6) per group are rendered. Consider capping total pages/threads fetched (the picker only shows a small slice anyway).As per coding guidelines: "For startup, thread loading, ... explicitly check ... unbounded fanout, large payloads."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/server/feishuThreadBridge.ts` around lines 683 - 733, The picker path is fetching every thread page in listRecentThreadGroups, accumulating them all in allRows before grouping, which creates unbounded memory and RPC fanout on large workspaces. Update listRecentThreadGroups to stop after a bounded number of pages or threads, keeping only enough recent results for the picker and respecting the existing MAX_THREADS_PER_GROUP rendering limit. Make the cap explicit in the pagination loop that calls this.appServer.rpc('thread/list') and in the thread/group assembly logic so /threads and /start cannot load the entire thread universe.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@README.md`:
- Around line 184-188: The README setup steps are missing the interactive card
subscription needed by the handler in FeishuThreadBridge. Update the required
Feishu Event Subscription list to include card.action.trigger alongside
im.message.receive_v1, so users configuring the app know to enable both events
for the thread-picker/card flow to work.
- Around line 189-196: The startup environment variables section is missing the
FEISHU_DOMAIN setting even though feishuThreadBridge uses it to switch between
Feishu and Lark domains. Update the README env var block to document
FEISHU_DOMAIN alongside FEISHU_APP_ID, FEISHU_APP_SECRET, and
FEISHU_ALLOWED_USER_IDS, and note its purpose so users configuring the app via
CLI/bootstrap can discover the international-domain path.
In `@src/App.vue`:
- Line 1752: `feishuConfigError` is defined in `App.vue` but is missing from
`visibleFeedbackErrors`, so Feishu bridge failures are not picked up by the
failure watcher or `hasVisibleFeedbackError`. Update the `visibleFeedbackErrors`
array in the `App` component to include `feishuConfigError`, matching how
`telegramConfigError` is registered, so Feishu errors are auto-recorded
consistently.
In `@src/server/feishuThreadBridge.ts`:
- Around line 218-257: The start() flow in FeishuThreadBridge marks the bridge
active before WSClient startup, so a thrown error leaves the instance stuck in
an active state. Move the this.active = true assignment to after
this.wsClient.start({ eventDispatcher }) completes successfully, or clear
this.active inside the failure path; use start(), wsClient.start(), and active
as the key points to update.
---
Nitpick comments:
In `@src/server/feishuThreadBridge.ts`:
- Around line 341-361: The helper named sendTextMessage is misleading because it
sends an interactive card, not plain text. Rename the method to reflect its
actual behavior, such as sendCardTextMessage or similar, and update its call
sites in FeishuThreadBridge so the purpose is clear while keeping the existing
message creation and returned message_id behavior unchanged.
- Around line 683-733: The picker path is fetching every thread page in
listRecentThreadGroups, accumulating them all in allRows before grouping, which
creates unbounded memory and RPC fanout on large workspaces. Update
listRecentThreadGroups to stop after a bounded number of pages or threads,
keeping only enough recent results for the picker and respecting the existing
MAX_THREADS_PER_GROUP rendering limit. Make the cap explicit in the pagination
loop that calls this.appServer.rpc('thread/list') and in the thread/group
assembly logic so /threads and /start cannot load the entire thread universe.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 5a8ea43e-25db-48c1-975c-1d1f49cda202
📒 Files selected for processing (6)
README.mdpackage.jsonsrc/App.vuesrc/api/codexGateway.tssrc/server/codexAppServerBridge.tssrc/server/feishuThreadBridge.ts
| 1. Create a self-built app at [Feishu Open Platform](https://open.feishu.cn) | ||
| 2. Enable bot capability | ||
| 3. Under Event Subscription, select **Long Connection** mode | ||
| 4. Subscribe to `im.message.receive_v1` event | ||
| 5. Add permission `im:message:send_as_bot` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Add card.action.trigger to the required Feishu subscriptions.
The bridge also registers a handler for card.action.trigger in src/server/feishuThreadBridge.ts; documenting only im.message.receive_v1 leaves the interactive card/thread-picker path broken for users who follow the README.
Suggested doc fix
-4. Subscribe to `im.message.receive_v1` event
+4. Subscribe to `im.message.receive_v1` and `card.action.trigger`📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 1. Create a self-built app at [Feishu Open Platform](https://open.feishu.cn) | |
| 2. Enable bot capability | |
| 3. Under Event Subscription, select **Long Connection** mode | |
| 4. Subscribe to `im.message.receive_v1` event | |
| 5. Add permission `im:message:send_as_bot` | |
| 1. Create a self-built app at [Feishu Open Platform](https://open.feishu.cn) | |
| 2. Enable bot capability | |
| 3. Under Event Subscription, select **Long Connection** mode | |
| 4. Subscribe to `im.message.receive_v1` and `card.action.trigger` | |
| 5. Add permission `im:message:send_as_bot` |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 184 - 188, The README setup steps are missing the
interactive card subscription needed by the handler in FeishuThreadBridge.
Update the required Feishu Event Subscription list to include
card.action.trigger alongside im.message.receive_v1, so users configuring the
app know to enable both events for the thread-picker/card flow to work.
| 6. Configure in the codexapp sidebar settings (App ID, App Secret, allowed user open_ids), or use environment variables: | ||
|
|
||
| ```bash | ||
| export FEISHU_APP_ID="<your-feishu-app-id>" | ||
| export FEISHU_APP_SECRET="<your-feishu-app-secret>" | ||
| export FEISHU_ALLOWED_USER_IDS="<your-open-id>,<optional-second-id>" | ||
| export FEISHU_DEFAULT_CWD="$PWD" # optional, defaults to current working directory | ||
| npx codexapp |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Document FEISHU_DOMAIN in the startup env vars block.
src/server/feishuThreadBridge.ts reads process.env.FEISHU_DOMAIN to choose the Feishu vs. Lark domain, but this README section doesn’t mention it. That hides the new international-domain startup path from CLI/bootstrap users.
Suggested doc fix
export FEISHU_APP_ID="<your-feishu-app-id>"
export FEISHU_APP_SECRET="<your-feishu-app-secret>"
+export FEISHU_DOMAIN="feishu|lark"
export FEISHU_ALLOWED_USER_IDS="<your-open-id>,<optional-second-id>"
export FEISHU_DEFAULT_CWD="$PWD" # optional, defaults to current working directory📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 6. Configure in the codexapp sidebar settings (App ID, App Secret, allowed user open_ids), or use environment variables: | |
| ```bash | |
| export FEISHU_APP_ID="<your-feishu-app-id>" | |
| export FEISHU_APP_SECRET="<your-feishu-app-secret>" | |
| export FEISHU_ALLOWED_USER_IDS="<your-open-id>,<optional-second-id>" | |
| export FEISHU_DEFAULT_CWD="$PWD" # optional, defaults to current working directory | |
| npx codexapp | |
| 6. Configure in the codexapp sidebar settings (App ID, App Secret, allowed user open_ids), or use environment variables: | |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 189 - 196, The startup environment variables section
is missing the FEISHU_DOMAIN setting even though feishuThreadBridge uses it to
switch between Feishu and Lark domains. Update the README env var block to
document FEISHU_DOMAIN alongside FEISHU_APP_ID, FEISHU_APP_SECRET, and
FEISHU_ALLOWED_USER_IDS, and note its purpose so users configuring the app via
CLI/bootstrap can discover the international-domain path.
| const feishuAppIdDraft = ref('') | ||
| const feishuAppSecretDraft = ref('') | ||
| const feishuAllowedUserIdsDraft = ref('') | ||
| const feishuConfigError = ref('') |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Register feishuConfigError in visibleFeedbackErrors.
Unlike telegramConfigError (added at Line 1787), feishuConfigError is not included in the visibleFeedbackErrors array (Lines 1780-1791). As a result, Feishu bridge errors are not auto-recorded by the failure watcher (Line 2260) and don't contribute to hasVisibleFeedbackError, so they're only captured if the user clicks the inline "Send feedback" link.
🔧 Proposed fix
telegramConfigError,
+ feishuConfigError,
createFolderError,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const feishuConfigError = ref('') | |
| telegramConfigError, | |
| feishuConfigError, | |
| createFolderError, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/App.vue` at line 1752, `feishuConfigError` is defined in `App.vue` but is
missing from `visibleFeedbackErrors`, so Feishu bridge failures are not picked
up by the failure watcher or `hasVisibleFeedbackError`. Update the
`visibleFeedbackErrors` array in the `App` component to include
`feishuConfigError`, matching how `telegramConfigError` is registered, so Feishu
errors are auto-recorded consistently.
| start(): void { | ||
| if (!this.appId || !this.appSecret || this.active) return | ||
| this.active = true | ||
| const domain = getLarkSdkDomain(this.domain) | ||
|
|
||
| this.client = new lark.Client({ | ||
| appId: this.appId, | ||
| appSecret: this.appSecret, | ||
| appType: lark.AppType.SelfBuild, | ||
| domain, | ||
| }) | ||
|
|
||
| const eventDispatcher = new lark.EventDispatcher({}).register({ | ||
| 'im.message.receive_v1': async (data: unknown) => { | ||
| await this.handleMessageEvent(data).catch((error) => { | ||
| this.lastError = getErrorMessage(error, 'Failed to handle message event') | ||
| }) | ||
| }, | ||
| 'card.action.trigger': async (data: unknown) => { | ||
| await this.handleCardAction(data).catch((error) => { | ||
| this.lastError = getErrorMessage(error, 'Failed to handle card action') | ||
| }) | ||
| }, | ||
| }) | ||
|
|
||
| this.wsClient = new lark.WSClient({ | ||
| appId: this.appId, | ||
| appSecret: this.appSecret, | ||
| domain, | ||
| loggerLevel: lark.LoggerLevel.warn, | ||
| }) | ||
|
|
||
| this.wsClient.start({ eventDispatcher }) | ||
|
|
||
| this.unsubscribeNotifications = this.appServer.onNotification((notification) => { | ||
| void this.handleNotification(notification).catch(() => {}) | ||
| }) | ||
|
|
||
| void this.notifyOnlineForKnownChats().catch(() => {}) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
@larksuiteoapi/node-sdk WSClient close or stop method 1.70
💡 Result:
In the @larksuiteoapi/node-sdk, the WSClient does not expose a standard.stop or.close method for direct termination of the WebSocket connection as of recent versions [1]. For managing or closing a WebSocket connection, the recommended approach involves manually handling the underlying WebSocket instance or using the higher-level Channel abstraction [1][2]. 1. Manual Management via WSClient: To shut down a WSClient connection, you must prevent auto-reconnection and then close the underlying WebSocket instance directly [1]: // 1. Disable auto-reconnect to prevent the client from restarting the connection wsClient.wsConfig.updateWs({autoReconnect: false}); // 2. Retrieve the underlying WebSocket instance and close it const instance = wsClient.wsConfig.getWSInstance; instance.close; instance.terminate; 2. Using Channel Abstraction: If you are using the high-level Channel module, it provides a built-in.disconnect method, which is the preferred way to handle connection lifecycle management [2]: await channel.disconnect; Development history indicates that internal improvements (such as commit 965cdcb) have focused on ensuring proper resource cleanup, including timers, during connection close events [3]. Always check the specific version of the SDK you are using, as implementation details for internal cleanup continue to evolve [1][3].
Citations:
- 1: [Feature Request] 关闭Websocket连接 larksuite/node-sdk#173
- 2: https://github.com/larksuite/node-sdk/blob/main/docs/channel.md
- 3: larksuite/node-sdk@965cdcb
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the target file, then inspect the relevant sections around start/stop/status.
ast-grep outline src/server/feishuThreadBridge.ts --view expanded || true
printf '\n--- start/stop/status sections ---\n'
sed -n '200,310p' src/server/feishuThreadBridge.ts | cat -n
printf '\n--- getStatus / active references ---\n'
rg -n "active|start\\(|stop\\(|getStatus|wsClient\\.close|unsubscribeNotifications|lastError" src/server/feishuThreadBridge.tsRepository: friuns2/codex-mobile
Length of output: 11628
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect nearby methods to understand whether failures can leave the bridge active.
sed -n '200,310p' src/server/feishuThreadBridge.ts | cat -n
printf '\n--- surrounding status/teardown references ---\n'
sed -n '1,220p' src/server/feishuThreadBridge.ts | cat -n | sed -n '1,220p'Repository: friuns2/codex-mobile
Length of output: 14242
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check whether the SDK docs/types are present in the repo for direct evidence of WSClient lifecycle APIs.
rg -n "class WSClient|interface WSClient|close\\(|stop\\(" . -g '!node_modules' -g '!dist' -g '!build' -g '!coverage'Repository: friuns2/codex-mobile
Length of output: 2826
🌐 Web query:
@larksuiteoapi/node-sdk 1.70.0 WSClient close start lifecycle docs
💡 Result:
In the @larksuiteoapi/node-sdk, the WSClient lifecycle is managed primarily through the.start and.close methods. Lifecycle Overview: 1. Initialization: The WSClient is instantiated with configuration parameters, including credentials and optional settings like autoReconnect [1][2]. 2. Start: The.start method is called to initiate the WebSocket connection and register an event dispatcher to handle incoming messages [1][2]. 3. Operation: Once connected, the client maintains a full-duplex channel. If autoReconnect is enabled, the SDK attempts to automatically re-establish the connection if it drops [1]. 4. Close: The.close method is used to gracefully terminate the WebSocket connection [1][3]. Closing the Connection: While older versions of the SDK required manual intervention (such as accessing the underlying WebSocket instance via internal methods) to close the connection [3], modern versions of the @larksuiteoapi/node-sdk support the.close method directly on the WSClient instance [1]. To perform a force close, you can typically pass an options object to the.close method, such as wsClient.close({ force: true }) [1]. Note on Reliability: - If you are using the Channel module (a higher-level abstraction over WSClient), you should manage the lifecycle using channel.connect and channel.disconnect instead [4]. - For robust production deployments, ensure that wsConfig.pingTimeout is configured (e.g., set to 15 seconds) to handle scenarios where the underlying socket connection may become stale or dead, such as after system sleep/wake cycles [5]. - Be aware of known issues in older versions regarding timer leaks during reconnection loops; ensure you are using a stable, up-to-date version of the SDK [6].
Citations:
- 1: https://context7.com/larksuite/node-sdk/llms.txt
- 2: https://github.com/larksuite/node-sdk/blob/main/README.md
- 3: [Feature Request] 关闭Websocket连接 larksuite/node-sdk#173
- 4: https://github.com/larksuite/node-sdk/blob/main/docs/channel.md
- 5: WSClient missing pingTimeout causes permanent WebSocket disconnection after machine sleep/wake larksuite/openclaw-lark#543
- 6: WSClient.reConnect: timer leak causes unbounded reconnect loops and memory growth larksuite/node-sdk#177
🌐 Web query:
site:github.com/larksuite/node-sdk WSClient close force start 1.70.0
💡 Result:
In the context of the larksuite/node-sdk, the request to add a way to properly close a WebSocket connection was addressed in response to Issue #173 [1]. The feature, which allows for the explicit closing of a WSClient connection, was implemented around January 2026 [2]. Regarding the specific usage of closing a WSClient, official recommendations (and community workarounds prior to the feature release) emphasize ensuring that auto-reconnect functionality is disabled before calling the underlying WebSocket's close or terminate methods [1]. If you are encountering issues related to reconnect loops or resource cleanup, please note that recent updates, such as those in mid-2026 (e.g., commit 965cdcb), have focused on fixing timer leaks and ensuring proper cleanup of internal caches and timers when the client is closed [3]. If you are experiencing memory growth or unbound reconnect loops, ensure you are using the latest version of the SDK, as these issues have been documented and patched in subsequent releases [4][3]. For high-level implementations, the SDK's Channel module provides a built-in disconnect method which is the recommended way to handle graceful shutdowns [5].
Citations:
- 1: [Feature Request] 关闭Websocket连接 larksuite/node-sdk#173
- 2: larksuite/node-sdk@db295b7
- 3: larksuite/node-sdk@965cdcb
- 4: WSClient.reConnect: timer leak causes unbounded reconnect loops and memory growth larksuite/node-sdk#177
- 5: https://github.com/larksuite/node-sdk/blob/main/docs/channel.md
Move active = true after wsClient.start() succeeds
If wsClient.start() throws, the bridge stays marked active and later start() calls will no-op. Set the flag only after startup completes, or clear it in the failure path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/server/feishuThreadBridge.ts` around lines 218 - 257, The start() flow in
FeishuThreadBridge marks the bridge active before WSClient startup, so a thrown
error leaves the instance stuck in an active state. Move the this.active = true
assignment to after this.wsClient.start({ eventDispatcher }) completes
successfully, or clear this.active inside the failure path; use start(),
wsClient.start(), and active as the key points to update.
Source: Coding guidelines
Summary
Add Feishu Thread Bridge support for both domestic Feishu and international Lark.
This lets users choose which platform their bot belongs to, so the bridge can connect through either the Feishu API domain or the Lark international API domain.
What Changed
domainto the Feishu bridge config API, persisted config, and status response.feishufor backward compatibility.FEISHU_DOMAINenvironment support for startup configuration.Compatibility
Existing domestic Feishu setups continue to work without config changes because missing
domainvalues default tofeishu.Performance Notes
This change only adds a small config normalization path and passes the selected domain into SDK client construction. It does not add duplicate requests, polling, large payloads, or new fanout. The bridge only reconnects when credentials or platform change.
Summary by CodeRabbit