From 34a05353fc6215a0a2cd26c98a698675e5b3f319 Mon Sep 17 00:00:00 2001 From: song Date: Mon, 28 Sep 2026 23:19:18 +0800 Subject: [PATCH 1/4] fix(workspace): keep Goal context and disable Lark setup without lark-cli The workspace loaded Goal repository contexts and Lark connections with one Promise.all. Without lark-cli every Lark request returns 503, which also dropped every Goal's repository context, so the Goal drawer silently lost its repository card. Load the two optional sources independently. In Lark settings, a lark_cli_not_installed or lark_cli_not_executable answer now also disables New Lark App next to the existing explanation, instead of letting the setup dialog fail with the same error. Signed-off-by: song --- .../personal-workspace/lark-settings-page.tsx | 13 ++++++++++++- .../personal-workspace-page.tsx | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx b/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx index a461ffb1d3..dfe4ab673b 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx @@ -153,6 +153,14 @@ function larkGroupHistoryPermissionUrl(connection: LarkGoalConnection): string | return connection.history_permission_guidance?.api_document_url ?? null; } +// lark-cli is resolved once when the Chat service starts, so these codes stay +// true until the operator installs it and restarts LoopX. +const larkCliUnavailableCodes = new Set(["lark_cli_not_installed", "lark_cli_not_executable"]); + +function larkCliUnavailable(cause: unknown): boolean { + return cause instanceof ChatApiError && larkCliUnavailableCodes.has(String(cause.payload.error_code ?? "")); +} + function larkErrorMessage(cause: unknown, fallback: string, t: WorkspaceTranslate): string { if (cause instanceof ChatApiError) { const code = String(cause.payload.error_code ?? ""); @@ -219,6 +227,7 @@ export function LarkSettingsPage({ const [setupBrand, setSetupBrand] = useState<"feishu" | "lark">("feishu"); const [setupSnapshot, setSetupSnapshot] = useState(null); const [setupStarting, setSetupStarting] = useState(false); + const [cliUnavailable, setCliUnavailable] = useState(false); const [setupError, setSetupError] = useState(null); const setupPopup = useRef(null); const openedSetupUrl = useRef(null); @@ -234,8 +243,10 @@ export function LarkSettingsPage({ ]); setApps(nextApps); setConnections(nextConnections); + setCliUnavailable(false); setAppRef((current) => current || nextApps.find((app) => app.reply_ready)?.app_ref || nextApps.find((app) => app.ready)?.app_ref || nextApps[0]?.app_ref || ""); } catch (cause) { + setCliUnavailable(larkCliUnavailable(cause)); setError(larkErrorMessage(cause, t("lark.error.configuration"), t)); } finally { setLoading(false); @@ -517,7 +528,7 @@ export function LarkSettingsPage({ {!loading && tab === "apps" ? (
-
{t("lark.reusableApps", { count: apps.length })}
+
{t("lark.reusableApps", { count: apps.length })}
{apps.map((app) => (
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx index dab614eca8..908b295cfc 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-page.tsx @@ -1029,15 +1029,22 @@ export function PersonalWorkspacePage({ return; } let cancelled = false; - void Promise.all([fetchGoalContexts(), fetchLarkConnections()]) - .then(([contexts, connections]) => { - if (cancelled) return; - setGoalContexts(Object.fromEntries(contexts.map((row) => [row.goal_id, row.repository]))); - setLarkConnections(connections); + // Goal repositories and Lark connections are independent optional sources: + // a missing lark-cli must not also hide every Goal's repository context. + void fetchGoalContexts() + .then((contexts) => { + if (!cancelled) setGoalContexts(Object.fromEntries(contexts.map((row) => [row.goal_id, row.repository]))); }) .catch(() => { // Local context is optional; the Goal workspace stays usable without it. }); + void fetchLarkConnections() + .then((connections) => { + if (!cancelled) setLarkConnections(connections); + }) + .catch(() => { + // Lark is optional; Settings reports why it is unavailable. + }); return () => { cancelled = true; }; }, [readOnly]); From 714f2565a46677c34bda6da549fcea0e7d57b52e Mon Sep 17 00:00:00 2001 From: song Date: Mon, 28 Sep 2026 23:19:18 +0800 Subject: [PATCH 2/4] test(workspace): cover a workspace without lark-cli A new lark-cli-missing scenario answers Lark requests with the service's lark_cli_not_installed 503 and requires the Goal repository card to stay visible and New Lark App to be disabled. Signed-off-by: song --- examples/personal-workspace-browser-smoke.mjs | 3 +- .../lark-cli-missing.mjs | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 examples/personal-workspace-browser/lark-cli-missing.mjs diff --git a/examples/personal-workspace-browser-smoke.mjs b/examples/personal-workspace-browser-smoke.mjs index 593fcd654a..05d3bbf6d9 100644 --- a/examples/personal-workspace-browser-smoke.mjs +++ b/examples/personal-workspace-browser-smoke.mjs @@ -44,8 +44,9 @@ import { goalActivityScenario } from "./personal-workspace-browser/goal-activity import { stewardGroupTriggerScenario } from "./personal-workspace-browser/steward-group-trigger.mjs"; import { goalDraftScenario } from "./personal-workspace-browser/goal-draft.mjs"; +import { larkCliMissingScenario } from "./personal-workspace-browser/lark-cli-missing.mjs"; -const scenarioCatalog = [goalDraftScenario, capabilityScopeScenario, stewardGroupTriggerScenario, conversationInputScenario, goalActivityScenario, conversationActivityScenario, navigationSortingScenario, automationCadenceScenario, chatRecoveryScenario, conversationReturnContinuityScenario, answerPresentationScenario, loopxModeScenario, teamEvidenceScenario, managedGoalResultsScenario, typedActionsScenario, teamPlanScenario, stewardJourneyScenario, executionChipScenario, stewardModelSettingsScenario, progressiveLoadingScenario, workspaceLocaleScenario, newestDraftScenario]; +const scenarioCatalog = [goalDraftScenario, capabilityScopeScenario, stewardGroupTriggerScenario, conversationInputScenario, goalActivityScenario, conversationActivityScenario, navigationSortingScenario, automationCadenceScenario, chatRecoveryScenario, conversationReturnContinuityScenario, answerPresentationScenario, loopxModeScenario, teamEvidenceScenario, managedGoalResultsScenario, typedActionsScenario, teamPlanScenario, stewardJourneyScenario, executionChipScenario, stewardModelSettingsScenario, progressiveLoadingScenario, workspaceLocaleScenario, newestDraftScenario, larkCliMissingScenario]; const requestedScenario = process.env.LOOPX_PERSONAL_WORKSPACE_SCENARIO; const scenarios = requestedScenario ? scenarioCatalog.filter((scenario) => scenario.id === requestedScenario) diff --git a/examples/personal-workspace-browser/lark-cli-missing.mjs b/examples/personal-workspace-browser/lark-cli-missing.mjs new file mode 100644 index 0000000000..47cb0d72e8 --- /dev/null +++ b/examples/personal-workspace-browser/lark-cli-missing.mjs @@ -0,0 +1,48 @@ +import { openWorkspacePage } from "./scenario-context.mjs"; + +// The Chat service answers every Lark request with this 503 when lark-cli is +// not installed. Lark is optional: its absence must not hide unrelated Goal +// context, and Settings must not offer an App setup that can only fail. +const larkCliMissing = { + error: "Install lark-cli, then restart the LoopX Chat service.", + error_code: "lark_cli_not_installed", + ok: false, +}; + +export const larkCliMissingScenario = { + id: "lark-cli-missing", + async run({ browser, collectCoverage, url }) { + const context = await openWorkspacePage(browser, url, { + beforeGoto: async (_api, page) => { + await page.route(/\/api\/chat\/lark\/(apps|connections)(\?.*)?$/u, (route) => route.fulfill({ + contentType: "application/json", + json: larkCliMissing, + status: 503, + })); + }, + collectCoverage, + }); + const { page } = context; + try { + await page.locator(".personal-goal-link", { hasText: "LoopX meta" }).click(); + await page.getByRole("navigation", { name: "Goal 视图" }).getByRole("button", { name: "概览", exact: true }).click(); + await page.getByRole("button", { name: "Goal 信息", exact: true }).click(); + await page.locator(".personal-goal-repository").getByText("loopx-ai/loopx", { exact: true }).waitFor({ state: "visible", timeout: 10_000 }); + await page.keyboard.press("Escape"); + + await page.getByRole("button", { name: "设置", exact: true }).click(); + await page.getByRole("button", { name: "Lark", exact: true }).click(); + await page.getByText("未发现 lark-cli。请先安装 lark-cli,然后重新启动 LoopX。", { exact: true }).first().waitFor({ state: "visible", timeout: 10_000 }); + await page.locator(".personal-lark-tabs").getByRole("button", { name: /^Lark Apps/u }).click(); + if (!await page.getByRole("button", { name: "新建 Lark App" }).isDisabled()) { + throw new Error("New Lark App stayed available while lark-cli is missing"); + } + } finally { + await context.close(); + } + return { + coverageEntries: context.coverageEntries, + note: "A missing lark-cli keeps Goal repository context and disables Lark App setup with its reason.", + }; + }, +}; From c406395b95b159b75762929882bb1dff5c3b6dd5 Mon Sep 17 00:00:00 2001 From: song Date: Tue, 29 Sep 2026 17:28:45 +0800 Subject: [PATCH 3/4] fix(workspace): gate every Lark App setup entry on lark-cli The toolbar's New Lark App was disabled without lark-cli, but the Goal drawer's Connect Lark App dialog still offered "register another Lark App", which opened the same setup and failed with lark_cli_not_installed. openSetup, shared by both entries, now refuses while lark-cli is unavailable, and the dialog's register option is disabled. Signed-off-by: song --- .../src/features/personal-workspace/lark-settings-page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx b/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx index dfe4ab673b..f4fe5d7588 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/lark-settings-page.tsx @@ -416,6 +416,9 @@ export function LarkSettingsPage({ } function openSetup() { + // Every setup entry (toolbar, connection dialog) meets the same fact: + // without lark-cli the setup can only fail until LoopX restarts. + if (cliUnavailable) return; setSetupSnapshot(null); setSetupError(null); openedSetupUrl.current = null; @@ -594,7 +597,7 @@ export function LarkSettingsPage({ {t("lark.editPreservesIdentity")} : <> - + {selectedApp?.ready && !selectedApp.reply_ready ?
{t("lark.appPermissions")}
: null}