diff --git a/packages/core/types/lark.ts b/packages/core/types/lark.ts index 4798adcc6..86245e558 100644 --- a/packages/core/types/lark.ts +++ b/packages/core/types/lark.ts @@ -13,6 +13,12 @@ export interface LarkInstallation { bot_open_id: string; installer_user_id: string; status: "active" | "revoked" | string; + /** Which Lark cloud the bot lives on: "feishu" (mainland) or "lark" + * (international). Auto-detected at install time. Optional so an older + * desktop build parsing a newer server — or a newer build hitting a + * server that predates the field — defaults to Feishu in the UI + * (see CLAUDE.md → API Response Compatibility). */ + region?: "feishu" | "lark" | string; installed_at: string; created_at: string; updated_at: string; diff --git a/packages/views/locales/en/settings.json b/packages/views/locales/en/settings.json index 8d1f9681a..4695c8b88 100644 --- a/packages/views/locales/en/settings.json +++ b/packages/views/locales/en/settings.json @@ -233,6 +233,8 @@ "empty_description_cta": "Bind to Lark", "empty_description_suffix": "to install a Bot for it.", "revoked_badge": "revoked", + "region_feishu": "Feishu", + "region_lark": "Lark", "installed_at_label": "Installed {{when}}", "disconnect": "Disconnect", "disconnecting": "Disconnecting…", diff --git a/packages/views/locales/ja/settings.json b/packages/views/locales/ja/settings.json index 7d7b34d2d..7ab4b02eb 100644 --- a/packages/views/locales/ja/settings.json +++ b/packages/views/locales/ja/settings.json @@ -233,6 +233,8 @@ "empty_description_cta": "Lark に接続", "empty_description_suffix": "をクリックすると、対応するボットを設置できます。", "revoked_badge": "取り消し済み", + "region_feishu": "Feishu", + "region_lark": "Lark", "installed_at_label": "{{when}} に設置", "disconnect": "切断", "disconnecting": "切断しています…", diff --git a/packages/views/locales/ko/settings.json b/packages/views/locales/ko/settings.json index a190e0a7f..acda5712e 100644 --- a/packages/views/locales/ko/settings.json +++ b/packages/views/locales/ko/settings.json @@ -310,6 +310,8 @@ "empty_description_cta": "Lark에 연결", "empty_description_suffix": "을(를) 클릭해 봇을 설치하세요.", "revoked_badge": "취소됨", + "region_feishu": "Feishu", + "region_lark": "Lark", "installed_at_label": "{{when}} 설치됨", "disconnect": "연결 해제", "disconnecting": "연결을 해제하는 중…", diff --git a/packages/views/locales/zh-Hans/settings.json b/packages/views/locales/zh-Hans/settings.json index 8e63ba9ed..e3489568f 100644 --- a/packages/views/locales/zh-Hans/settings.json +++ b/packages/views/locales/zh-Hans/settings.json @@ -233,6 +233,8 @@ "empty_description_cta": "绑定到飞书", "empty_description_suffix": "即可为它安装 Bot。", "revoked_badge": "已撤销", + "region_feishu": "飞书", + "region_lark": "Lark", "installed_at_label": "安装于 {{when}}", "disconnect": "断开连接", "disconnecting": "正在断开…", diff --git a/packages/views/settings/components/lark-tab.test.tsx b/packages/views/settings/components/lark-tab.test.tsx index cb6e8daa5..3278d90d2 100644 --- a/packages/views/settings/components/lark-tab.test.tsx +++ b/packages/views/settings/components/lark-tab.test.tsx @@ -251,6 +251,32 @@ describe("LarkAgentBindButton (CTA gate)", () => { expect(link.rel).toContain("noopener"); }); + it("points the Manage link at open.larksuite.com for a Lark-international (region=lark) installation", () => { + // Dual-region: a bot installed against the Lark international cloud + // must manage at open.larksuite.com, not the Feishu default. The + // region rides on the listings response, auto-detected at install. + installationsRef.current.installations = [ + { + id: "inst-lark", + workspace_id: "ws-1", + agent_id: "agent-1", + app_id: "cli_lark_app", + bot_open_id: "ou_lark_bot", + installer_user_id: "user-1", + status: "active", + region: "lark", + installed_at: "2026-06-03T00:00:00Z", + created_at: "2026-06-03T00:00:00Z", + updated_at: "2026-06-03T00:00:00Z", + }, + ]; + render(, { + wrapper: I18nWrapper, + }); + const link = screen.getByRole("link", { name: /Manage in Lark/i }) as HTMLAnchorElement; + expect(link.href).toBe("https://open.larksuite.com/app/cli_lark_app"); + }); + it("still shows the bind CTA when an installation exists for a DIFFERENT agent (per-agent scoping)", () => { installationsRef.current.installations = [ { diff --git a/packages/views/settings/components/lark-tab.tsx b/packages/views/settings/components/lark-tab.tsx index 882d3c4e5..5e957d113 100644 --- a/packages/views/settings/components/lark-tab.tsx +++ b/packages/views/settings/components/lark-tab.tsx @@ -232,6 +232,11 @@ function InstallationRow({

{agentName} + + {installation.region === "lark" + ? t(($) => $.lark.region_lark) + : t(($) => $.lark.region_feishu)} + {!isActive && ( {t(($) => $.lark.revoked_badge)} @@ -359,12 +364,17 @@ export function LarkAgentBindButton({ // a new tab so the user can manage scopes / display name / additional // permissions without re-scanning the QR. // -// The dev console URL host follows the same default as the backend's -// LARK_BASE_URL (open.feishu.cn for mainland Lark). Operators on the -// Lark international tenant currently see the wrong host; future- -// proofing requires the backend to surface a per-installation -// `dev_console_url` on the listings response. Tracked separately. -const LARK_DEV_CONSOLE_HOST = "https://open.feishu.cn"; +// The dev-console host depends on which Lark cloud the bot lives on: +// Feishu (mainland) bots are managed at open.feishu.cn, Lark +// (international) bots at open.larksuite.com. The region is auto-detected +// at install time and surfaced per installation on the listings +// response; an older server that omits `region` defaults to Feishu +// (API-compat — see CLAUDE.md). +function larkDevConsoleHost(region?: string): string { + return region === "lark" + ? "https://open.larksuite.com" + : "https://open.feishu.cn"; +} function LarkAgentBotConnectedBadge({ installation, @@ -374,7 +384,7 @@ function LarkAgentBotConnectedBadge({ className?: string; }) { const { t } = useT("settings"); - const manageHref = `${LARK_DEV_CONSOLE_HOST}/app/${encodeURIComponent(installation.app_id)}`; + const manageHref = `${larkDevConsoleHost(installation.region)}/app/${encodeURIComponent(installation.app_id)}`; return (