diff --git a/extensions/clickclack/src/setup-surface.ts b/extensions/clickclack/src/setup-surface.ts index 52610419c556..7d6ad679c2ec 100644 --- a/extensions/clickclack/src/setup-surface.ts +++ b/extensions/clickclack/src/setup-surface.ts @@ -1,5 +1,4 @@ // ClickClack plugin module implements guided setup behavior. -import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { createStandardChannelSetupStatus, createSetupTranslator, @@ -10,31 +9,17 @@ import { type ChannelSetupWizard, } from "openclaw/plugin-sdk/setup"; import { listClickClackAccountIds, resolveClickClackAccount } from "./accounts.js"; -import { createClickClackClient } from "./http-client.js"; -import { resolveWorkspaceId } from "./resolve.js"; import { applyClickClackCredentialConfig, applyClickClackSetupConfigPatch, normalizeClickClackBaseUrl, } from "./setup-core.js"; +import { checkClickClackSetupConnection } from "./setup-verify.js"; import type { CoreConfig, ResolvedClickClackAccount } from "./types.js"; const t = createSetupTranslator(); const channel = "clickclack" as const; -function isHttpStatus(error: unknown, status: number): boolean { - return ( - typeof error === "object" && - error !== null && - "status" in error && - (error as { status?: unknown }).status === status - ); -} - -function isWorkspaceNotFound(error: unknown): boolean { - return error instanceof Error && error.message.startsWith("ClickClack workspace not found:"); -} - function hasConfiguredClickClackCredential(account: ResolvedClickClackAccount): boolean { return ( hasConfiguredSecretInput(account.config.token) || Boolean(account.config.tokenFile?.trim()) @@ -166,39 +151,33 @@ export const clickClackSetupWizard: ChannelSetupWizard = { }, ], finalize: async ({ cfg, accountId, credentialValues, prompter }) => { - const account = resolveClickClackAccount({ + const result = await checkClickClackSetupConnection({ cfg: cfg as CoreConfig, accountId, + token: credentialValues.token, }); - try { - const client = createClickClackClient({ - baseUrl: account.baseUrl, - token: credentialValues.token || account.token, - }); - const me = await client.me(); - const workspaceId = await resolveWorkspaceId(client, account.workspace); - const workspaces = await client.workspaces(); - const workspace = workspaces.find((candidate) => candidate.id === workspaceId); - if (!workspace) { - throw new Error(`ClickClack workspace not found: ${account.workspace}`); - } + if (result.status === "connected") { await prompter.note( t("wizard.clickclack.connected", { - handle: me.handle, - workspace: workspace.name, + handle: result.handle, + workspace: result.workspaceName, }), t("wizard.clickclack.connectionTitle"), ); - } catch (error) { - const message = isHttpStatus(error, 401) - ? t("wizard.clickclack.invalidToken") - : isWorkspaceNotFound(error) - ? t("wizard.clickclack.workspaceNotFound", { workspace: account.workspace }) - : t("wizard.clickclack.connectionFailed", { - error: formatErrorMessage(error), - }); - await prompter.note(message, t("wizard.clickclack.validationWarningTitle")); + return; } + if (result.status === "skipped-env-token" || result.status === "skipped-unconfigured") { + return; + } + const message = + result.status === "invalid-token" + ? t("wizard.clickclack.invalidToken") + : result.status === "workspace-not-found" + ? t("wizard.clickclack.workspaceNotFound", { workspace: result.workspace }) + : t("wizard.clickclack.connectionFailed", { + error: result.error, + }); + await prompter.note(message, t("wizard.clickclack.validationWarningTitle")); }, disable: (cfg) => setSetupChannelEnabled(cfg, channel, false), }; diff --git a/extensions/clickclack/src/setup-verify.ts b/extensions/clickclack/src/setup-verify.ts new file mode 100644 index 000000000000..d477c558678a --- /dev/null +++ b/extensions/clickclack/src/setup-verify.ts @@ -0,0 +1,96 @@ +// ClickClack plugin module implements shared setup connection verification. +import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; +import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/setup"; +import { resolveClickClackAccount } from "./accounts.js"; +import { createClickClackClient } from "./http-client.js"; +import { resolveWorkspaceId } from "./resolve.js"; +import type { CoreConfig, ResolvedClickClackAccount } from "./types.js"; + +export type ClickClackSetupConnectionResult = + | { status: "connected"; handle: string; workspaceName: string } + | { status: "invalid-token" } + | { status: "workspace-not-found"; workspace: string } + | { status: "failed"; error: string } + | { status: "skipped-env-token" } + | { status: "skipped-unconfigured" }; + +function isHttpStatus(error: unknown, status: number): boolean { + return ( + typeof error === "object" && + error !== null && + "status" in error && + (error as { status?: unknown }).status === status + ); +} + +function isWorkspaceNotFound(error: unknown): boolean { + return error instanceof Error && error.message.startsWith("ClickClack workspace not found:"); +} + +function usesUnavailableImplicitEnvToken( + account: ResolvedClickClackAccount, + tokenOverride: string, +): boolean { + return ( + account.accountId === DEFAULT_ACCOUNT_ID && + Boolean(account.baseUrl && account.workspace) && + !tokenOverride && + !account.token && + !hasConfiguredSecretInput(account.config.token) && + !account.config.tokenFile?.trim() + ); +} + +export async function checkClickClackSetupConnection(params: { + cfg: CoreConfig; + accountId?: string; + token?: string; +}): Promise { + let workspaceInput = ""; + try { + const account = resolveClickClackAccount({ + cfg: params.cfg, + accountId: params.accountId, + }); + workspaceInput = account.workspace; + const token = params.token?.trim() || account.token; + if (usesUnavailableImplicitEnvToken(account, token)) { + return { status: "skipped-env-token" }; + } + if (!account.baseUrl || !account.workspace || !token) { + return { status: "skipped-unconfigured" }; + } + + const client = createClickClackClient({ + baseUrl: account.baseUrl, + token, + }); + const me = await client.me(); + const workspaceId = await resolveWorkspaceId(client, account.workspace); + const workspaces = await client.workspaces(); + const workspace = workspaces.find((candidate) => candidate.id === workspaceId); + if (!workspace) { + throw new Error(`ClickClack workspace not found: ${account.workspace}`); + } + return { + status: "connected", + handle: me.handle, + workspaceName: workspace.name, + }; + } catch (error) { + if (isHttpStatus(error, 401)) { + return { status: "invalid-token" }; + } + if (isWorkspaceNotFound(error)) { + return { + status: "workspace-not-found", + workspace: workspaceInput, + }; + } + return { + status: "failed", + error: formatErrorMessage(error), + }; + } +}