mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 23:21:40 +00:00
* feat(channels): add channel-owned setup contracts * test(channels): align legacy setup fixtures * chore(channels): regenerate config and SDK baselines after rebase * fix(update): run fresh doctor after current-process core changes * fix(channels): align add pre-scan with execution precedence * style(cli): format channels-cli test additions * fix(channels): restore option-before-positional channel resolution via metadata arity scan * fix(channels): keep help flags out of metadata arity escalation * test(update): mock fresh post-update doctor in current-process suites * style: format review fixes and correct entrypoint mock type * fix(channels): register only modern contract options for dual-publishing plugins * test(update): align downgrade suites with fresh-doctor child invocation * docs(channels): record empty-contract and input-forwarding invariants * fix(line): keep the shipped --token switch as a channel access token alias * fix(signal): stop treating exact cross-family loopback endpoints as bind-aligned * chore(config): regenerate docs config baselines after second rebase * style: format rebased channels add tests * fix(channels): enforce field-key and flag-name agreement in setup contracts * fix(signal): detect container endpoints for bare --http-url setup * fix(signal): ignore unconfigured accounts in transport collision checks * fix(channels): validate negated setup flags in contract and normalizer * fix(signal): preserve existing transport kind when setup detection is unreachable * style(signal): use direct boolean check in collision guard * style(signal): type test config literals * docs(update): record two-read design of fresh-doctor validation gate * fix(channels): satisfy post-rebase architecture gates * docs: refresh channel setup map --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
195 lines
5.8 KiB
TypeScript
195 lines
5.8 KiB
TypeScript
import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
|
|
// Line plugin module implements setup core behavior.
|
|
import type {
|
|
ChannelSetupAdapter,
|
|
ChannelSetupInput,
|
|
OpenClawConfig,
|
|
} from "openclaw/plugin-sdk/setup";
|
|
import { createSetupInputPresenceValidator } from "openclaw/plugin-sdk/setup";
|
|
import { hasLineCredentials, parseLineAllowFromId } from "./account-helpers.js";
|
|
import {
|
|
DEFAULT_ACCOUNT_ID,
|
|
listLineAccountIds,
|
|
normalizeAccountId,
|
|
resolveLineAccount,
|
|
type LineConfig,
|
|
} from "./setup-runtime-api.js";
|
|
|
|
type LineSetupInput = ChannelSetupInput & {
|
|
channelAccessToken?: string;
|
|
channelSecret?: string;
|
|
secretFile?: string;
|
|
};
|
|
|
|
export function patchLineAccountConfig(params: {
|
|
cfg: OpenClawConfig;
|
|
accountId: string;
|
|
patch: Record<string, unknown>;
|
|
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<string, unknown>;
|
|
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<string, unknown>;
|
|
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 {
|
|
return hasLineCredentials(resolveLineAccount({ cfg, accountId }));
|
|
}
|
|
|
|
export { parseLineAllowFromId };
|
|
|
|
export const lineSetupAdapter: ChannelSetupAdapter = {
|
|
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
|
applyAccountName: ({ cfg, accountId, name }) =>
|
|
patchLineAccountConfig({
|
|
cfg,
|
|
accountId,
|
|
patch: name?.trim() ? { name: name.trim() } : {},
|
|
}),
|
|
validateInput: createSetupInputPresenceValidator({
|
|
defaultAccountOnlyEnvError:
|
|
"LINE_CHANNEL_ACCESS_TOKEN can only be used for the default account.",
|
|
whenNotUseEnv: [
|
|
{
|
|
someOf: ["channelAccessToken", "token", "tokenFile"],
|
|
message: "LINE requires channelAccessToken or --token-file (or --use-env).",
|
|
},
|
|
{
|
|
someOf: ["channelSecret", "secretFile"],
|
|
message: "LINE requires channelSecret or --secret-file (or --use-env).",
|
|
},
|
|
],
|
|
}),
|
|
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
const typedInput = input as LineSetupInput;
|
|
// Shipped alias: `--token` writes channelAccessToken; the explicit switch wins.
|
|
const accessToken = typedInput.channelAccessToken ?? typedInput.token;
|
|
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 }
|
|
: accessToken
|
|
? { channelAccessToken: accessToken }
|
|
: {}),
|
|
...(typedInput.secretFile
|
|
? { secretFile: typedInput.secretFile }
|
|
: typedInput.channelSecret
|
|
? { channelSecret: typedInput.channelSecret }
|
|
: {}),
|
|
},
|
|
});
|
|
}
|
|
return patchLineAccountConfig({
|
|
cfg,
|
|
accountId: normalizedAccountId,
|
|
enabled: true,
|
|
patch: {
|
|
...(typedInput.tokenFile
|
|
? { tokenFile: typedInput.tokenFile }
|
|
: accessToken
|
|
? { channelAccessToken: accessToken }
|
|
: {}),
|
|
...(typedInput.secretFile
|
|
? { secretFile: typedInput.secretFile }
|
|
: typedInput.channelSecret
|
|
? { channelSecret: typedInput.channelSecret }
|
|
: {}),
|
|
},
|
|
});
|
|
},
|
|
};
|
|
|
|
export const lineSetupContract = defineChannelSetupContract({
|
|
fields: {
|
|
channelAccessToken: {
|
|
kind: "string",
|
|
sensitive: true,
|
|
cli: { flags: "--channel-access-token <token>", description: "LINE channel access token" },
|
|
},
|
|
// Shipped alias: released CLIs configured LINE via the shared `--token`
|
|
// envelope switch; the adapter maps it onto channelAccessToken.
|
|
token: {
|
|
kind: "string",
|
|
sensitive: true,
|
|
cli: { flags: "--token <token>", description: "LINE channel access token (alias)" },
|
|
},
|
|
channelSecret: {
|
|
kind: "string",
|
|
sensitive: true,
|
|
cli: { flags: "--channel-secret <secret>", description: "LINE channel secret" },
|
|
},
|
|
tokenFile: {
|
|
kind: "string",
|
|
sensitive: true,
|
|
cli: { flags: "--token-file <path>", description: "LINE access token file" },
|
|
},
|
|
secretFile: {
|
|
kind: "string",
|
|
sensitive: true,
|
|
cli: { flags: "--secret-file <path>", description: "LINE channel secret file" },
|
|
},
|
|
useEnv: {
|
|
kind: "boolean",
|
|
cli: { flags: "--use-env", description: "Use LINE environment credentials" },
|
|
},
|
|
},
|
|
legacyAdapter: lineSetupAdapter,
|
|
});
|
|
|
|
export { listLineAccountIds };
|