Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion electron/api/routes/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import {
ensureDingTalkPluginInstalled,
ensureFeishuPluginInstalled,
ensureLinePluginInstalled,
ensureQQBotPluginInstalled,
ensureWeComPluginInstalled,
} from '../../utils/plugin-install';
Expand Down Expand Up @@ -363,6 +364,13 @@ export async function handleChannelRoutes(
return true;
}
}
let linePluginWarning: string | undefined;
if (body.channelType === 'line') {
const installResult = await ensureLinePluginInstalled();
if (!installResult.installed) {
linePluginWarning = installResult.warning || 'LINE plugin not found locally. Install the plugin via OpenClaw CLI if needed.';
}
}
const existingValues = await getChannelFormValues(body.channelType, body.accountId);
if (isSameConfigValues(existingValues, body.config)) {
await ensureScopedChannelBinding(body.channelType, body.accountId);
Expand All @@ -372,7 +380,7 @@ export async function handleChannelRoutes(
await saveChannelConfig(body.channelType, body.config, body.accountId);
await ensureScopedChannelBinding(body.channelType, body.accountId);
scheduleGatewayChannelSaveRefresh(ctx, body.channelType, `channel:saveConfig:${body.channelType}`);
sendJson(res, 200, { success: true });
sendJson(res, 200, { success: true, ...(linePluginWarning ? { warning: linePluginWarning } : {}) });
} catch (error) {
sendJson(res, 500, { success: false, error: String(error) });
}
Expand Down
60 changes: 60 additions & 0 deletions electron/utils/channel-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,19 @@ async function ensurePluginAllowlist(currentConfig: OpenClawConfig, channelType:
currentConfig.plugins.allow = [...allow, 'qqbot'];
}
}

if (channelType === 'line') {
if (!currentConfig.plugins) {
currentConfig.plugins = {};
}
currentConfig.plugins.enabled = true;
const allow = Array.isArray(currentConfig.plugins.allow)
? currentConfig.plugins.allow as string[]
: [];
if (!allow.includes('line')) {
currentConfig.plugins.allow = [...allow, 'line'];
}
}
}

function transformChannelConfig(
Expand Down Expand Up @@ -959,6 +972,8 @@ export async function validateChannelCredentials(
return validateDiscordCredentials(config);
case 'telegram':
return validateTelegramCredentials(config);
case 'line':
return validateLineCredentials(config);
default:
return { valid: true, errors: [], warnings: ['No online validation available for this channel type.'] };
}
Expand Down Expand Up @@ -1070,6 +1085,42 @@ async function validateTelegramCredentials(
}
}

async function validateLineCredentials(
config: Record<string, string>
): Promise<CredentialValidationResult> {
const channelAccessToken = config.channelAccessToken?.trim();
const channelSecret = config.channelSecret?.trim();

if (!channelAccessToken) return { valid: false, errors: ['Channel Access Token is required'], warnings: [] };
if (!channelSecret) return { valid: false, errors: ['Channel Secret is required'], warnings: [] };

try {
const response = await proxyAwareFetch('https://api.line.me/v2/bot/info', {
headers: { Authorization: `Bearer ${channelAccessToken}` },
});
if (!response.ok) {
if (response.status === 401) {
return { valid: false, errors: ['Invalid Channel Access Token. Please check and try again.'], warnings: [] };
}
const errorData = await response.json().catch(() => ({}));
const msg = (errorData as { message?: string }).message || `LINE API error: ${response.status}`;
return { valid: false, errors: [msg], warnings: [] };
}
const data = (await response.json()) as { displayName?: string; userId?: string; basicId?: string };
return {
valid: true,
errors: [],
warnings: [],
details: {
botUsername: data.displayName || 'Unknown',
...(data.basicId ? { basicId: data.basicId } : {}),
},
};
} catch (error) {
return { valid: false, errors: [`Connection error: ${error instanceof Error ? error.message : String(error)}`], warnings: [] };
}
}

export async function validateChannelConfig(channelType: string): Promise<ValidationResult> {
const { exec } = await import('child_process');

Expand Down Expand Up @@ -1143,6 +1194,15 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
result.errors.push('Telegram: Allowed User IDs are required');
result.valid = false;
}
} else if (channelType === 'line') {
if (!savedChannelConfig?.channelAccessToken) {
result.errors.push('LINE: Channel Access Token is required');
result.valid = false;
}
if (!savedChannelConfig?.channelSecret) {
result.errors.push('LINE: Channel Secret is required');
result.valid = false;
}
}

if (result.errors.length === 0 && result.warnings.length === 0) {
Expand Down
5 changes: 5 additions & 0 deletions electron/utils/plugin-install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,10 @@ export function ensureQQBotPluginInstalled(): { installed: boolean; warning?: st
return ensurePluginInstalled('qqbot', buildCandidateSources('qqbot'), 'QQ Bot');
}

export function ensureLinePluginInstalled(): { installed: boolean; warning?: string } {
return ensurePluginInstalled('line', buildCandidateSources('line'), 'LINE');
}

// ── Bulk startup installer ───────────────────────────────────────────────────

/**
Expand All @@ -384,6 +388,7 @@ const ALL_BUNDLED_PLUGINS = [
{ fn: ensureWeComPluginInstalled, label: 'WeCom' },
{ fn: ensureQQBotPluginInstalled, label: 'QQ Bot' },
{ fn: ensureFeishuPluginInstalled, label: 'Feishu' },
{ fn: ensureLinePluginInstalled, label: 'LINE' },
] as const;

/**
Expand Down
3 changes: 3 additions & 0 deletions src/assets/channels/line.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/components/channels/ChannelConfigModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import dingtalkIcon from '@/assets/channels/dingtalk.svg';
import feishuIcon from '@/assets/channels/feishu.svg';
import wecomIcon from '@/assets/channels/wecom.svg';
import qqIcon from '@/assets/channels/qq.svg';
import lineIcon from '@/assets/channels/line.svg';

interface ChannelConfigModalProps {
initialSelectedType?: ChannelType | null;
Expand Down Expand Up @@ -744,6 +745,8 @@ function ChannelLogo({ type }: { type: ChannelType }) {
return <img src={wecomIcon} alt="WeCom" className="w-[22px] h-[22px] dark:invert" />;
case 'qqbot':
return <img src={qqIcon} alt="QQ" className="w-[22px] h-[22px] dark:invert" />;
case 'line':
return <img src={lineIcon} alt="LINE" className="w-[22px] h-[22px] dark:invert" />;
default:
return <span className="text-[22px]">{CHANNEL_ICONS[type] || '💬'}</span>;
}
Expand Down
3 changes: 3 additions & 0 deletions src/pages/Channels/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import dingtalkIcon from '@/assets/channels/dingtalk.svg';
import feishuIcon from '@/assets/channels/feishu.svg';
import wecomIcon from '@/assets/channels/wecom.svg';
import qqIcon from '@/assets/channels/qq.svg';
import lineIcon from '@/assets/channels/line.svg';

interface ChannelAccountItem {
accountId: string;
Expand Down Expand Up @@ -527,6 +528,8 @@ function ChannelLogo({ type }: { type: ChannelType }) {
return <img src={wecomIcon} alt="WeCom" className="w-[22px] h-[22px] dark:invert" />;
case 'qqbot':
return <img src={qqIcon} alt="QQ" className="w-[22px] h-[22px] dark:invert" />;
case 'line':
return <img src={lineIcon} alt="LINE" className="w-[22px] h-[22px] dark:invert" />;
default:
return <span className="text-[22px]">{CHANNEL_ICONS[type] || '💬'}</span>;
}
Expand Down
2 changes: 1 addition & 1 deletion src/types/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,7 +561,7 @@ export const CHANNEL_META: Record<ChannelType, ChannelMeta> = {
* Get primary supported channels (non-plugin, commonly used)
*/
export function getPrimaryChannels(): ChannelType[] {
return ['telegram', 'discord', 'whatsapp', 'dingtalk', 'feishu', 'wecom', 'qqbot'];
return ['telegram', 'discord', 'whatsapp', 'dingtalk', 'feishu', 'wecom', 'qqbot', 'line'];
}

/**
Expand Down