From 6a2efa541be1c5ba46045535110cd2c6d118d907 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 19:21:40 -0700 Subject: [PATCH] LINE: split setup adapter helpers --- extensions/line/src/channel.ts | 3 +- extensions/line/src/setup-core.ts | 162 ++++++++++++++++++++++++++ extensions/line/src/setup-surface.ts | 165 ++------------------------- src/plugin-sdk/line.ts | 3 +- 4 files changed, 175 insertions(+), 158 deletions(-) create mode 100644 extensions/line/src/setup-core.ts diff --git a/extensions/line/src/channel.ts b/extensions/line/src/channel.ts index 4c2b51cd6d0..b184ebe8482 100644 --- a/extensions/line/src/channel.ts +++ b/extensions/line/src/channel.ts @@ -20,7 +20,8 @@ import { type ResolvedLineAccount, } from "openclaw/plugin-sdk/line"; import { getLineRuntime } from "./runtime.js"; -import { lineSetupAdapter, lineSetupWizard } from "./setup-surface.js"; +import { lineSetupAdapter } from "./setup-core.js"; +import { lineSetupWizard } from "./setup-surface.js"; // LINE channel metadata const meta = { diff --git a/extensions/line/src/setup-core.ts b/extensions/line/src/setup-core.ts new file mode 100644 index 00000000000..324197c70af --- /dev/null +++ b/extensions/line/src/setup-core.ts @@ -0,0 +1,162 @@ +import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js"; +import type { OpenClawConfig } from "../../../src/config/config.js"; +import { + listLineAccountIds, + normalizeAccountId, + resolveLineAccount, +} from "../../../src/line/accounts.js"; +import type { LineConfig } from "../../../src/line/types.js"; +import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js"; + +const channel = "line" as const; + +export function patchLineAccountConfig(params: { + cfg: OpenClawConfig; + accountId: string; + patch: Record; + clearFields?: string[]; + enabled?: boolean; +}): OpenClawConfig { + const accountId = normalizeAccountId(params.accountId); + const lineConfig = ((params.cfg.channels?.line ?? {}) as LineConfig) ?? {}; + const clearFields = params.clearFields ?? []; + + if (accountId === DEFAULT_ACCOUNT_ID) { + const nextLine = { ...lineConfig } as Record; + for (const field of clearFields) { + delete nextLine[field]; + } + return { + ...params.cfg, + channels: { + ...params.cfg.channels, + line: { + ...nextLine, + ...(params.enabled ? { enabled: true } : {}), + ...params.patch, + }, + }, + }; + } + + const nextAccount = { + ...(lineConfig.accounts?.[accountId] ?? {}), + } as Record; + for (const field of clearFields) { + delete nextAccount[field]; + } + + return { + ...params.cfg, + channels: { + ...params.cfg.channels, + line: { + ...lineConfig, + ...(params.enabled ? { enabled: true } : {}), + accounts: { + ...lineConfig.accounts, + [accountId]: { + ...nextAccount, + ...(params.enabled ? { enabled: true } : {}), + ...params.patch, + }, + }, + }, + }, + }; +} + +export function isLineConfigured(cfg: OpenClawConfig, accountId: string): boolean { + const resolved = resolveLineAccount({ cfg, accountId }); + return Boolean(resolved.channelAccessToken.trim() && resolved.channelSecret.trim()); +} + +export function parseLineAllowFromId(raw: string): string | null { + const trimmed = raw.trim().replace(/^line:(?:user:)?/i, ""); + if (!/^U[a-f0-9]{32}$/i.test(trimmed)) { + return null; + } + return trimmed; +} + +export const lineSetupAdapter: ChannelSetupAdapter = { + resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), + applyAccountName: ({ cfg, accountId, name }) => + patchLineAccountConfig({ + cfg, + accountId, + patch: name?.trim() ? { name: name.trim() } : {}, + }), + validateInput: ({ accountId, input }) => { + const typedInput = input as { + useEnv?: boolean; + channelAccessToken?: string; + channelSecret?: string; + tokenFile?: string; + secretFile?: string; + }; + if (typedInput.useEnv && accountId !== DEFAULT_ACCOUNT_ID) { + return "LINE_CHANNEL_ACCESS_TOKEN can only be used for the default account."; + } + if (!typedInput.useEnv && !typedInput.channelAccessToken && !typedInput.tokenFile) { + return "LINE requires channelAccessToken or --token-file (or --use-env)."; + } + if (!typedInput.useEnv && !typedInput.channelSecret && !typedInput.secretFile) { + return "LINE requires channelSecret or --secret-file (or --use-env)."; + } + return null; + }, + applyAccountConfig: ({ cfg, accountId, input }) => { + const typedInput = input as { + useEnv?: boolean; + channelAccessToken?: string; + channelSecret?: string; + tokenFile?: string; + secretFile?: string; + }; + const normalizedAccountId = normalizeAccountId(accountId); + if (normalizedAccountId === DEFAULT_ACCOUNT_ID) { + return patchLineAccountConfig({ + cfg, + accountId: normalizedAccountId, + enabled: true, + clearFields: typedInput.useEnv + ? ["channelAccessToken", "channelSecret", "tokenFile", "secretFile"] + : undefined, + patch: typedInput.useEnv + ? {} + : { + ...(typedInput.tokenFile + ? { tokenFile: typedInput.tokenFile } + : typedInput.channelAccessToken + ? { channelAccessToken: typedInput.channelAccessToken } + : {}), + ...(typedInput.secretFile + ? { secretFile: typedInput.secretFile } + : typedInput.channelSecret + ? { channelSecret: typedInput.channelSecret } + : {}), + }, + }); + } + return patchLineAccountConfig({ + cfg, + accountId: normalizedAccountId, + enabled: true, + patch: { + ...(typedInput.tokenFile + ? { tokenFile: typedInput.tokenFile } + : typedInput.channelAccessToken + ? { channelAccessToken: typedInput.channelAccessToken } + : {}), + ...(typedInput.secretFile + ? { secretFile: typedInput.secretFile } + : typedInput.channelSecret + ? { channelSecret: typedInput.channelSecret } + : {}), + }, + }); + }, +}; + +export { listLineAccountIds }; diff --git a/extensions/line/src/setup-surface.ts b/extensions/line/src/setup-surface.ts index 1b7a22dfb11..8c1dca21562 100644 --- a/extensions/line/src/setup-surface.ts +++ b/extensions/line/src/setup-surface.ts @@ -5,16 +5,16 @@ import { splitOnboardingEntries, } from "../../../src/channels/plugins/onboarding/helpers.js"; import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js"; -import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js"; -import type { OpenClawConfig } from "../../../src/config/config.js"; -import { - listLineAccountIds, - normalizeAccountId, - resolveLineAccount, -} from "../../../src/line/accounts.js"; -import type { LineConfig } from "../../../src/line/types.js"; +import { resolveLineAccount } from "../../../src/line/accounts.js"; import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js"; import { formatDocsLink } from "../../../src/terminal/links.js"; +import { + isLineConfigured, + lineSetupAdapter, + listLineAccountIds, + parseLineAllowFromId, + patchLineAccountConfig, +} from "./setup-core.js"; const channel = "line" as const; @@ -36,75 +36,6 @@ const LINE_ALLOW_FROM_HELP_LINES = [ `Docs: ${formatDocsLink("/channels/line", "channels/line")}`, ]; -function patchLineAccountConfig(params: { - cfg: OpenClawConfig; - accountId: string; - patch: Record; - clearFields?: string[]; - enabled?: boolean; -}): OpenClawConfig { - const accountId = normalizeAccountId(params.accountId); - const lineConfig = ((params.cfg.channels?.line ?? {}) as LineConfig) ?? {}; - const clearFields = params.clearFields ?? []; - - if (accountId === DEFAULT_ACCOUNT_ID) { - const nextLine = { ...lineConfig } as Record; - for (const field of clearFields) { - delete nextLine[field]; - } - return { - ...params.cfg, - channels: { - ...params.cfg.channels, - line: { - ...nextLine, - ...(params.enabled ? { enabled: true } : {}), - ...params.patch, - }, - }, - }; - } - - const nextAccount = { - ...(lineConfig.accounts?.[accountId] ?? {}), - } as Record; - for (const field of clearFields) { - delete nextAccount[field]; - } - - return { - ...params.cfg, - channels: { - ...params.cfg.channels, - line: { - ...lineConfig, - ...(params.enabled ? { enabled: true } : {}), - accounts: { - ...lineConfig.accounts, - [accountId]: { - ...nextAccount, - ...(params.enabled ? { enabled: true } : {}), - ...params.patch, - }, - }, - }, - }, - }; -} - -function isLineConfigured(cfg: OpenClawConfig, accountId: string): boolean { - const resolved = resolveLineAccount({ cfg, accountId }); - return Boolean(resolved.channelAccessToken.trim() && resolved.channelSecret.trim()); -} - -function parseLineAllowFromId(raw: string): string | null { - const trimmed = raw.trim().replace(/^line:(?:user:)?/i, ""); - if (!/^U[a-f0-9]{32}$/i.test(trimmed)) { - return null; - } - return trimmed; -} - const lineDmPolicy: ChannelOnboardingDmPolicy = { label: "LINE", channel, @@ -119,85 +50,7 @@ const lineDmPolicy: ChannelOnboardingDmPolicy = { }), }; -export const lineSetupAdapter: ChannelSetupAdapter = { - resolveAccountId: ({ accountId }) => normalizeAccountId(accountId), - applyAccountName: ({ cfg, accountId, name }) => - patchLineAccountConfig({ - cfg, - accountId, - patch: name?.trim() ? { name: name.trim() } : {}, - }), - validateInput: ({ accountId, input }) => { - const typedInput = input as { - useEnv?: boolean; - channelAccessToken?: string; - channelSecret?: string; - tokenFile?: string; - secretFile?: string; - }; - if (typedInput.useEnv && accountId !== DEFAULT_ACCOUNT_ID) { - return "LINE_CHANNEL_ACCESS_TOKEN can only be used for the default account."; - } - if (!typedInput.useEnv && !typedInput.channelAccessToken && !typedInput.tokenFile) { - return "LINE requires channelAccessToken or --token-file (or --use-env)."; - } - if (!typedInput.useEnv && !typedInput.channelSecret && !typedInput.secretFile) { - return "LINE requires channelSecret or --secret-file (or --use-env)."; - } - return null; - }, - applyAccountConfig: ({ cfg, accountId, input }) => { - const typedInput = input as { - useEnv?: boolean; - channelAccessToken?: string; - channelSecret?: string; - tokenFile?: string; - secretFile?: string; - }; - const normalizedAccountId = normalizeAccountId(accountId); - if (normalizedAccountId === DEFAULT_ACCOUNT_ID) { - return patchLineAccountConfig({ - cfg, - accountId: normalizedAccountId, - enabled: true, - clearFields: typedInput.useEnv - ? ["channelAccessToken", "channelSecret", "tokenFile", "secretFile"] - : undefined, - patch: typedInput.useEnv - ? {} - : { - ...(typedInput.tokenFile - ? { tokenFile: typedInput.tokenFile } - : typedInput.channelAccessToken - ? { channelAccessToken: typedInput.channelAccessToken } - : {}), - ...(typedInput.secretFile - ? { secretFile: typedInput.secretFile } - : typedInput.channelSecret - ? { channelSecret: typedInput.channelSecret } - : {}), - }, - }); - } - return patchLineAccountConfig({ - cfg, - accountId: normalizedAccountId, - enabled: true, - patch: { - ...(typedInput.tokenFile - ? { tokenFile: typedInput.tokenFile } - : typedInput.channelAccessToken - ? { channelAccessToken: typedInput.channelAccessToken } - : {}), - ...(typedInput.secretFile - ? { secretFile: typedInput.secretFile } - : typedInput.channelSecret - ? { channelSecret: typedInput.channelSecret } - : {}), - }, - }); - }, -}; +export { lineSetupAdapter } from "./setup-core.js"; export const lineSetupWizard: ChannelSetupWizard = { channel, diff --git a/src/plugin-sdk/line.ts b/src/plugin-sdk/line.ts index d0c6ffcaf86..6022c2ea318 100644 --- a/src/plugin-sdk/line.ts +++ b/src/plugin-sdk/line.ts @@ -27,7 +27,8 @@ export { buildTokenChannelStatusSummary, } from "./status-helpers.js"; -export { lineSetupAdapter, lineSetupWizard } from "../../extensions/line/src/setup-surface.js"; +export { lineSetupAdapter } from "../../extensions/line/src/setup-core.js"; +export { lineSetupWizard } from "../../extensions/line/src/setup-surface.js"; export { LineConfigSchema } from "../line/config-schema.js"; export type { LineChannelData, LineConfig, ResolvedLineAccount } from "../line/types.js"; export {