import { clearAccountEntryFields, DEFAULT_ACCOUNT_ID, type ChannelPlugin, type LineConfig, type OpenClawConfig, type ResolvedLineAccount, } from "../api.js"; import { getLineRuntime } from "./runtime.js"; export const lineGatewayAdapter: NonNullable["gateway"]> = { startAccount: async (ctx) => { const account = ctx.account; const token = account.channelAccessToken.trim(); const secret = account.channelSecret.trim(); if (!token) { throw new Error( `LINE webhook mode requires a non-empty channel access token for account "${account.accountId}".`, ); } if (!secret) { throw new Error( `LINE webhook mode requires a non-empty channel secret for account "${account.accountId}".`, ); } let lineBotLabel = ""; try { const probe = await getLineRuntime().channel.line.probeLineBot(token, 2500); const displayName = probe.ok ? probe.bot?.displayName?.trim() : null; if (displayName) { lineBotLabel = ` (${displayName})`; } } catch (err) { if (getLineRuntime().logging.shouldLogVerbose()) { ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`); } } ctx.log?.info(`[${account.accountId}] starting LINE provider${lineBotLabel}`); return await getLineRuntime().channel.line.monitorLineProvider({ channelAccessToken: token, channelSecret: secret, accountId: account.accountId, config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal, webhookPath: account.config.webhookPath, }); }, logoutAccount: async ({ accountId, cfg }) => { const envToken = process.env.LINE_CHANNEL_ACCESS_TOKEN?.trim() ?? ""; const nextCfg = { ...cfg } as OpenClawConfig; const lineConfig = (cfg.channels?.line ?? {}) as LineConfig; const nextLine = { ...lineConfig }; let cleared = false; let changed = false; if (accountId === DEFAULT_ACCOUNT_ID) { if ( nextLine.channelAccessToken || nextLine.channelSecret || nextLine.tokenFile || nextLine.secretFile ) { delete nextLine.channelAccessToken; delete nextLine.channelSecret; delete nextLine.tokenFile; delete nextLine.secretFile; cleared = true; changed = true; } } const accountCleanup = clearAccountEntryFields({ accounts: nextLine.accounts, accountId, fields: ["channelAccessToken", "channelSecret", "tokenFile", "secretFile"], markClearedOnFieldPresence: true, }); if (accountCleanup.changed) { changed = true; if (accountCleanup.cleared) { cleared = true; } if (accountCleanup.nextAccounts) { nextLine.accounts = accountCleanup.nextAccounts; } else { delete nextLine.accounts; } } if (changed) { if (Object.keys(nextLine).length > 0) { nextCfg.channels = { ...nextCfg.channels, line: nextLine }; } else { const nextChannels = { ...nextCfg.channels }; delete (nextChannels as Record).line; if (Object.keys(nextChannels).length > 0) { nextCfg.channels = nextChannels; } else { delete nextCfg.channels; } } await getLineRuntime().config.writeConfigFile(nextCfg); } const resolved = getLineRuntime().channel.line.resolveLineAccount({ cfg: changed ? nextCfg : cfg, accountId, }); const loggedOut = resolved.tokenSource === "none"; return { cleared, envToken: Boolean(envToken), loggedOut }; }, };