Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ Notes:
- ⚡ No global install required for quick experimentation
- 🎙️ Built-in hold-to-dictate voice input with transcription to composer draft
- 🤖 Optional Telegram bot bridge: send messages to bot, forward into mapped thread, send assistant reply back to Telegram
- 🐦 Optional Feishu bot bridge: same bidirectional messaging via Feishu using WebSocket long connection (no public IP needed)
- 💾 Project portability: export a project as a ZIP from project or thread menus, including matching Codex chat JSONL history under `.codex-project/chats/`
- 📦 Project import: restore exported project ZIPs from the browser via `Import Project`
- 🔁 Imported chats are rewritten for the destination `CODEX_HOME`, project path, and currently selected provider/model so they can be resumed in the new environment
Expand Down Expand Up @@ -174,6 +175,41 @@ Bot commands:

Outgoing assistant messages are sent with Telegram `parse_mode=HTML` for formatting, with automatic plain-text fallback if HTML delivery fails.

### Feishu Bot Bridge (Optional)

Same bidirectional messaging as Telegram, using Feishu's WebSocket long connection mode (no public IP or domain required).

**Setup:**

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`
Comment on lines +184 to +188

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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
Comment on lines +189 to +196

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

```

`FEISHU_ALLOWED_USER_IDS` is required for safe access. Use `*` to allow all users. To find your open_id, send a message to the bot — it will show in the rejection message.

Bot commands (same as Telegram):

- `/start` show quick help and thread picker
- `/threads` list recent threads grouped by project
- `/newthread` create and map a new Codex thread
- `/thread <threadId>` map current chat to an existing thread
- `/current` show currently connected thread
- `/history` show full history for current thread
- `/status` show bridge/mapping status
- `/whoami` show your Feishu IDs and authorization state
- `/help` show command reference

---

## 🧩 Recent Product Features (from main commits)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
},
"dependencies": {
"@composio/client": "0.1.0-alpha.66",
"@larksuiteoapi/node-sdk": "^1.70.0",
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"commander": "^13.1.0",
Expand Down
189 changes: 188 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,84 @@
</button>
</div>
</div>
<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&#10;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>
Comment on lines +478 to +555

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

<div
v-if="showThreadContextBadge"
class="sidebar-settings-row sidebar-settings-context-row"
Expand Down Expand Up @@ -1193,6 +1271,7 @@ import {
checkoutGitBranch,
cloneGithubRepository,
configureTelegramBot,
configureFeishuBot,
createPermanentWorktree,
createWorktree,
createProjectlessThreadDirectory,
Expand All @@ -1209,8 +1288,10 @@ import {
getFirstLaunchPluginsCardPreference,
getHomeDirectory,
getTelegramConfig,
getFeishuConfig,
getProjectRootSuggestion,
getTelegramStatus,
getFeishuStatus,
getThreadTerminalQuickCommands,
getThreadTerminalStatus,
getWorkspaceRootsState,
Expand All @@ -1227,7 +1308,7 @@ import {
} from './api/codexGateway'
import type { ReasoningEffort, SpeedMode, UiAccountEntry, UiRateLimitWindow, UiServerRequest, UiServerRequestReply, UiThreadAutomation, UiThreadTokenUsage } from './types/codex'
import type { ComposerDraftPayload, ThreadComposerExposed } from './components/content/ThreadComposer.vue'
import type { GitCommitFileChange, GitCommitOption, LocalDirectoryEntry, TelegramStatus, ThreadTerminalQuickCommand, WorktreeBranchOption } from './api/codexGateway'
import type { GitCommitFileChange, GitCommitOption, LocalDirectoryEntry, TelegramStatus, FeishuStatus, FeishuDomain, ThreadTerminalQuickCommand, WorktreeBranchOption } from './api/codexGateway'
import { getFreeModeStatus, setFreeMode, setFreeModeCustomKey, setCustomProvider } from './api/codexGateway'
import { getPathLeafName, getPathParent, isProjectlessChatPath, normalizePathForUi } from './pathUtils.js'
import { copyTextToClipboard } from './utils/clipboard'
Expand Down Expand Up @@ -1663,6 +1744,13 @@ const telegramBotTokenDraft = ref('')
const telegramAllowedUserIdsDraft = ref('')
const telegramConfigError = ref('')
const isTelegramSaving = ref(false)
const isFeishuConfigOpen = ref(false)
const feishuDomainDraft = ref<FeishuDomain>('feishu')
const feishuAppIdDraft = ref('')
const feishuAppSecretDraft = ref('')
const feishuAllowedUserIdsDraft = ref('')
const feishuConfigError = ref('')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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.

const isFeishuSaving = ref(false)
const isCreateFolderOpen = ref(false)
const createFolderDraft = ref('')
const createFolderError = ref('')
Expand Down Expand Up @@ -1711,6 +1799,16 @@ const telegramStatus = ref<TelegramStatus>({
allowAllUsers: false,
lastError: '',
})
const feishuStatus = ref<FeishuStatus>({
configured: false,
active: false,
domain: 'feishu',
mappedChats: 0,
mappedThreads: 0,
allowedUsers: 0,
allowAllUsers: false,
lastError: '',
})
const mobileHiddenAtMs = ref<number | null>(null)
const mobileResumeReloadTriggered = ref(false)
const mobileResumeSyncInProgress = ref(false)
Expand Down Expand Up @@ -2121,6 +2219,17 @@ const telegramStatusText = computed(() => {
const error = telegramStatus.value.lastError ? `, ${t('error')}: ${telegramStatus.value.lastError}` : ''
return `${base}, ${mapped}${error}`
})
const feishuStatusText = computed(() => {
if (!feishuStatus.value.configured) return t('Not configured')
const base = feishuStatus.value.active ? t('Online') : t('Configured (offline)')
const platform = feishuStatus.value.domain === 'lark' ? 'Lark' : 'Feishu'
const allowlist = feishuStatus.value.allowAllUsers
? t('allow all users')
: `${feishuStatus.value.allowedUsers} ${t('allowed user(s)')}`
const mapped = `${feishuStatus.value.mappedChats} ${t('chat(s)')}, ${feishuStatus.value.mappedThreads} ${t('thread(s)')}, ${allowlist}`
const error = feishuStatus.value.lastError ? `, ${t('error')}: ${feishuStatus.value.lastError}` : ''
return `${base}, ${platform}, ${mapped}${error}`
})

onMounted(() => {
document.addEventListener('pointerdown', onDocumentPointerDown)
Expand All @@ -2141,6 +2250,8 @@ onMounted(() => {
void refreshDefaultProjectName()
void refreshTelegramConfig()
void refreshTelegramStatus()
void refreshFeishuConfig()
void refreshFeishuStatus()
void loadFreeModeStatus()
void refreshThreadTerminalStatus()
void refreshTerminalQuickCommands()
Expand Down Expand Up @@ -2342,6 +2453,82 @@ async function saveTelegramConfig(): Promise<void> {
}
}

async function refreshFeishuStatus(): Promise<void> {
try {
feishuStatus.value = await getFeishuStatus()
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to load Feishu status'
feishuStatus.value = {
configured: false,
active: false,
mappedChats: 0,
mappedThreads: 0,
allowedUsers: 0,
allowAllUsers: false,
domain: 'feishu',
lastError: message,
}
}
}

async function refreshFeishuConfig(): Promise<void> {
try {
const config = await getFeishuConfig()
feishuDomainDraft.value = config.domain
feishuAppIdDraft.value = config.appId
feishuAppSecretDraft.value = config.appSecret
feishuAllowedUserIdsDraft.value = config.allowedUserIds.map((value) => String(value)).join('\n')
feishuConfigError.value = ''
} catch (error) {
feishuConfigError.value = error instanceof Error ? error.message : 'Failed to load Feishu configuration'
}
}

function parseFeishuAllowedUserIdsInput(value: string): Array<string | '*'> {
const rawEntries = value
.split(/[\n,]/)
.map((entry) => entry.trim())
.filter(Boolean)
const allowAllUsers = rawEntries.includes('*')
const normalizedUserIds = Array.from(new Set(rawEntries.filter((entry) => entry !== '*')))
return allowAllUsers ? ['*', ...normalizedUserIds] : normalizedUserIds
}

async function saveFeishuConfig(): Promise<void> {
const appId = feishuAppIdDraft.value.trim()
const appSecret = feishuAppSecretDraft.value.trim()
const allowedUserIds = parseFeishuAllowedUserIdsInput(feishuAllowedUserIdsDraft.value)
if (!appId) {
feishuConfigError.value = t('Feishu App ID is required.')
return
}
if (!appSecret) {
feishuConfigError.value = t('Feishu App Secret is required.')
return
}
if (allowedUserIds.length === 0) {
feishuConfigError.value = t('At least one allowed Feishu user open_id or * is required.')
return
}

isFeishuSaving.value = true
feishuConfigError.value = ''
try {
await configureFeishuBot(appId, appSecret, feishuDomainDraft.value, allowedUserIds)
feishuAllowedUserIdsDraft.value = allowedUserIds.map((value) => String(value)).join('\n')
await Promise.all([
refreshFeishuConfig(),
refreshFeishuStatus(),
])
window.alert(t('Feishu bot configured. Only allowlisted Feishu users can use the bridge.'))
} catch (error) {
feishuConfigError.value = error instanceof Error ? error.message : t('Failed to connect Feishu bot')
void refreshFeishuStatus()
} finally {
isFeishuSaving.value = false
}
}

function toggleSidebarSearch(): void {
isSidebarSearchVisible.value = !isSidebarSearchVisible.value
if (isSidebarSearchVisible.value) {
Expand Down
Loading