Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* Channel Configuration Utilities
|
||||
* Manages channel configuration in OpenClaw config files
|
||||
* Manages channel configuration in OpenClaw config files.
|
||||
*
|
||||
* All file I/O uses async fs/promises to avoid blocking the main thread.
|
||||
*/
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, rmSync } from 'fs';
|
||||
import { access, mkdir, readFile, writeFile, readdir, stat, rm } from 'fs/promises';
|
||||
import { constants } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { getOpenClawResolvedDir } from './paths';
|
||||
@@ -14,6 +17,14 @@ const CONFIG_FILE = join(OPENCLAW_DIR, 'openclaw.json');
|
||||
// Channels that are managed as plugins (config goes under plugins.entries, not channels)
|
||||
const PLUGIN_CHANNELS = ['whatsapp'];
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
async function fileExists(p: string): Promise<boolean> {
|
||||
try { await access(p, constants.F_OK); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────
|
||||
|
||||
export interface ChannelConfigData {
|
||||
enabled?: boolean;
|
||||
[key: string]: unknown;
|
||||
@@ -30,27 +41,23 @@ export interface OpenClawConfig {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure OpenClaw config directory exists
|
||||
*/
|
||||
function ensureConfigDir(): void {
|
||||
if (!existsSync(OPENCLAW_DIR)) {
|
||||
mkdirSync(OPENCLAW_DIR, { recursive: true });
|
||||
// ── Config I/O ───────────────────────────────────────────────────
|
||||
|
||||
async function ensureConfigDir(): Promise<void> {
|
||||
if (!(await fileExists(OPENCLAW_DIR))) {
|
||||
await mkdir(OPENCLAW_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read OpenClaw configuration
|
||||
*/
|
||||
export function readOpenClawConfig(): OpenClawConfig {
|
||||
ensureConfigDir();
|
||||
export async function readOpenClawConfig(): Promise<OpenClawConfig> {
|
||||
await ensureConfigDir();
|
||||
|
||||
if (!existsSync(CONFIG_FILE)) {
|
||||
if (!(await fileExists(CONFIG_FILE))) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const content = readFileSync(CONFIG_FILE, 'utf-8');
|
||||
const content = await readFile(CONFIG_FILE, 'utf-8');
|
||||
return JSON.parse(content) as OpenClawConfig;
|
||||
} catch (error) {
|
||||
logger.error('Failed to read OpenClaw config', error);
|
||||
@@ -59,14 +66,11 @@ export function readOpenClawConfig(): OpenClawConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write OpenClaw configuration
|
||||
*/
|
||||
export function writeOpenClawConfig(config: OpenClawConfig): void {
|
||||
ensureConfigDir();
|
||||
export async function writeOpenClawConfig(config: OpenClawConfig): Promise<void> {
|
||||
await ensureConfigDir();
|
||||
|
||||
try {
|
||||
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
||||
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
||||
} catch (error) {
|
||||
logger.error('Failed to write OpenClaw config', error);
|
||||
console.error('Failed to write OpenClaw config:', error);
|
||||
@@ -74,16 +78,13 @@ export function writeOpenClawConfig(config: OpenClawConfig): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save channel configuration
|
||||
* @param channelType - The channel type (e.g., 'telegram', 'discord')
|
||||
* @param config - The channel configuration object
|
||||
*/
|
||||
export function saveChannelConfig(
|
||||
// ── Channel operations ───────────────────────────────────────────
|
||||
|
||||
export async function saveChannelConfig(
|
||||
channelType: string,
|
||||
config: ChannelConfigData
|
||||
): void {
|
||||
const currentConfig = readOpenClawConfig();
|
||||
): Promise<void> {
|
||||
const currentConfig = await readOpenClawConfig();
|
||||
|
||||
// Plugin-based channels (e.g. WhatsApp) go under plugins.entries, not channels
|
||||
if (PLUGIN_CHANNELS.includes(channelType)) {
|
||||
@@ -97,7 +98,7 @@ export function saveChannelConfig(
|
||||
...currentConfig.plugins.entries[channelType],
|
||||
enabled: config.enabled ?? true,
|
||||
};
|
||||
writeOpenClawConfig(currentConfig);
|
||||
await writeOpenClawConfig(currentConfig);
|
||||
logger.info('Plugin channel config saved', {
|
||||
channelType,
|
||||
configFile: CONFIG_FILE,
|
||||
@@ -119,7 +120,6 @@ export function saveChannelConfig(
|
||||
const { guildId, channelId, ...restConfig } = config;
|
||||
transformedConfig = { ...restConfig };
|
||||
|
||||
// Add standard Discord config
|
||||
transformedConfig.groupPolicy = 'allowlist';
|
||||
transformedConfig.dm = { enabled: false };
|
||||
transformedConfig.retry = {
|
||||
@@ -129,21 +129,17 @@ export function saveChannelConfig(
|
||||
jitter: 0.1,
|
||||
};
|
||||
|
||||
// Build guilds structure
|
||||
if (guildId && typeof guildId === 'string' && guildId.trim()) {
|
||||
const guildConfig: Record<string, unknown> = {
|
||||
users: ['*'],
|
||||
requireMention: true,
|
||||
};
|
||||
|
||||
// Add channels config
|
||||
if (channelId && typeof channelId === 'string' && channelId.trim()) {
|
||||
// Specific channel
|
||||
guildConfig.channels = {
|
||||
[channelId.trim()]: { allow: true, requireMention: true }
|
||||
};
|
||||
} else {
|
||||
// All channels
|
||||
guildConfig.channels = {
|
||||
'*': { allow: true, requireMention: true }
|
||||
};
|
||||
@@ -166,8 +162,7 @@ export function saveChannelConfig(
|
||||
.filter(u => u.length > 0);
|
||||
|
||||
if (users.length > 0) {
|
||||
transformedConfig.allowFrom = users; // Use 'allowFrom' (correct key)
|
||||
// transformedConfig.groupPolicy = 'allowlist'; // Default is allowlist
|
||||
transformedConfig.allowFrom = users;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -176,17 +171,16 @@ export function saveChannelConfig(
|
||||
if (channelType === 'feishu') {
|
||||
const existingConfig = currentConfig.channels[channelType] || {};
|
||||
transformedConfig.dmPolicy = transformedConfig.dmPolicy ?? existingConfig.dmPolicy ?? 'open';
|
||||
|
||||
|
||||
let allowFrom = transformedConfig.allowFrom ?? existingConfig.allowFrom ?? ['*'];
|
||||
if (!Array.isArray(allowFrom)) {
|
||||
allowFrom = [allowFrom];
|
||||
}
|
||||
|
||||
// If dmPolicy is open, OpenClaw schema requires '*' in allowFrom
|
||||
|
||||
if (transformedConfig.dmPolicy === 'open' && !allowFrom.includes('*')) {
|
||||
allowFrom = [...allowFrom, '*'];
|
||||
}
|
||||
|
||||
|
||||
transformedConfig.allowFrom = allowFrom;
|
||||
}
|
||||
|
||||
@@ -197,7 +191,7 @@ export function saveChannelConfig(
|
||||
enabled: transformedConfig.enabled ?? true,
|
||||
};
|
||||
|
||||
writeOpenClawConfig(currentConfig);
|
||||
await writeOpenClawConfig(currentConfig);
|
||||
logger.info('Channel config saved', {
|
||||
channelType,
|
||||
configFile: CONFIG_FILE,
|
||||
@@ -208,42 +202,26 @@ export function saveChannelConfig(
|
||||
console.log(`Saved channel config for ${channelType}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channel configuration
|
||||
* @param channelType - The channel type
|
||||
*/
|
||||
export function getChannelConfig(channelType: string): ChannelConfigData | undefined {
|
||||
const config = readOpenClawConfig();
|
||||
export async function getChannelConfig(channelType: string): Promise<ChannelConfigData | undefined> {
|
||||
const config = await readOpenClawConfig();
|
||||
return config.channels?.[channelType];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get channel configuration as form-friendly values.
|
||||
* Reverses the transformation done in saveChannelConfig so the
|
||||
* values can be fed back into the UI form fields.
|
||||
*
|
||||
* @param channelType - The channel type
|
||||
* @returns A flat Record<string, string> matching the form field keys, or undefined
|
||||
*/
|
||||
export function getChannelFormValues(channelType: string): Record<string, string> | undefined {
|
||||
const saved = getChannelConfig(channelType);
|
||||
export async function getChannelFormValues(channelType: string): Promise<Record<string, string> | undefined> {
|
||||
const saved = await getChannelConfig(channelType);
|
||||
if (!saved) return undefined;
|
||||
|
||||
const values: Record<string, string> = {};
|
||||
|
||||
if (channelType === 'discord') {
|
||||
// token is stored at top level
|
||||
if (saved.token && typeof saved.token === 'string') {
|
||||
values.token = saved.token;
|
||||
}
|
||||
|
||||
// Extract guildId and channelId from the nested guilds structure
|
||||
const guilds = saved.guilds as Record<string, Record<string, unknown>> | undefined;
|
||||
if (guilds) {
|
||||
const guildIds = Object.keys(guilds);
|
||||
if (guildIds.length > 0) {
|
||||
values.guildId = guildIds[0];
|
||||
|
||||
const guildConfig = guilds[guildIds[0]];
|
||||
const channels = guildConfig?.channels as Record<string, unknown> | undefined;
|
||||
if (channels) {
|
||||
@@ -255,19 +233,15 @@ export function getChannelFormValues(channelType: string): Record<string, string
|
||||
}
|
||||
}
|
||||
} else if (channelType === 'telegram') {
|
||||
// Special handling for Telegram: convert allowFrom array to allowedUsers string
|
||||
if (Array.isArray(saved.allowFrom)) {
|
||||
values.allowedUsers = saved.allowFrom.join(', ');
|
||||
}
|
||||
|
||||
// Also extract other string values
|
||||
for (const [key, value] of Object.entries(saved)) {
|
||||
if (typeof value === 'string' && key !== 'enabled') {
|
||||
values[key] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For other channel types, extract all string values directly
|
||||
for (const [key, value] of Object.entries(saved)) {
|
||||
if (typeof value === 'string' && key !== 'enabled') {
|
||||
values[key] = value;
|
||||
@@ -278,31 +252,23 @@ export function getChannelFormValues(channelType: string): Record<string, string
|
||||
return Object.keys(values).length > 0 ? values : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete channel configuration
|
||||
* @param channelType - The channel type
|
||||
*/
|
||||
export function deleteChannelConfig(channelType: string): void {
|
||||
const currentConfig = readOpenClawConfig();
|
||||
export async function deleteChannelConfig(channelType: string): Promise<void> {
|
||||
const currentConfig = await readOpenClawConfig();
|
||||
|
||||
if (currentConfig.channels?.[channelType]) {
|
||||
delete currentConfig.channels[channelType];
|
||||
writeOpenClawConfig(currentConfig);
|
||||
await writeOpenClawConfig(currentConfig);
|
||||
console.log(`Deleted channel config for ${channelType}`);
|
||||
} else if (PLUGIN_CHANNELS.includes(channelType)) {
|
||||
// Handle plugin channels (like whatsapp)
|
||||
if (currentConfig.plugins?.entries?.[channelType]) {
|
||||
delete currentConfig.plugins.entries[channelType];
|
||||
|
||||
// Cleanup empty objects
|
||||
if (Object.keys(currentConfig.plugins.entries).length === 0) {
|
||||
delete currentConfig.plugins.entries;
|
||||
}
|
||||
if (currentConfig.plugins && Object.keys(currentConfig.plugins).length === 0) {
|
||||
delete currentConfig.plugins;
|
||||
}
|
||||
|
||||
writeOpenClawConfig(currentConfig);
|
||||
await writeOpenClawConfig(currentConfig);
|
||||
console.log(`Deleted plugin channel config for ${channelType}`);
|
||||
}
|
||||
}
|
||||
@@ -310,10 +276,9 @@ export function deleteChannelConfig(channelType: string): void {
|
||||
// Special handling for WhatsApp credentials
|
||||
if (channelType === 'whatsapp') {
|
||||
try {
|
||||
|
||||
const whatsappDir = join(homedir(), '.openclaw', 'credentials', 'whatsapp');
|
||||
if (existsSync(whatsappDir)) {
|
||||
rmSync(whatsappDir, { recursive: true, force: true });
|
||||
if (await fileExists(whatsappDir)) {
|
||||
await rm(whatsappDir, { recursive: true, force: true });
|
||||
console.log('Deleted WhatsApp credentials directory');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -322,11 +287,8 @@ export function deleteChannelConfig(channelType: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all configured channels
|
||||
*/
|
||||
export function listConfiguredChannels(): string[] {
|
||||
const config = readOpenClawConfig();
|
||||
export async function listConfiguredChannels(): Promise<string[]> {
|
||||
const config = await readOpenClawConfig();
|
||||
const channels: string[] = [];
|
||||
|
||||
if (config.channels) {
|
||||
@@ -338,14 +300,17 @@ export function listConfiguredChannels(): string[] {
|
||||
// Check for WhatsApp credentials directory
|
||||
try {
|
||||
const whatsappDir = join(homedir(), '.openclaw', 'credentials', 'whatsapp');
|
||||
if (existsSync(whatsappDir)) {
|
||||
const entries = readdirSync(whatsappDir);
|
||||
// Check if there's at least one directory (session)
|
||||
const hasSession = entries.some((entry: string) => {
|
||||
try {
|
||||
return statSync(join(whatsappDir, entry)).isDirectory();
|
||||
} catch { return false; }
|
||||
});
|
||||
if (await fileExists(whatsappDir)) {
|
||||
const entries = await readdir(whatsappDir);
|
||||
const hasSession = await (async () => {
|
||||
for (const entry of entries) {
|
||||
try {
|
||||
const s = await stat(join(whatsappDir, entry));
|
||||
if (s.isDirectory()) return true;
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
if (hasSession && !channels.includes('whatsapp')) {
|
||||
channels.push('whatsapp');
|
||||
@@ -358,42 +323,28 @@ export function listConfiguredChannels(): string[] {
|
||||
return channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable a channel
|
||||
*/
|
||||
export function setChannelEnabled(channelType: string, enabled: boolean): void {
|
||||
const currentConfig = readOpenClawConfig();
|
||||
export async function setChannelEnabled(channelType: string, enabled: boolean): Promise<void> {
|
||||
const currentConfig = await readOpenClawConfig();
|
||||
|
||||
// Plugin-based channels go under plugins.entries
|
||||
if (PLUGIN_CHANNELS.includes(channelType)) {
|
||||
if (!currentConfig.plugins) {
|
||||
currentConfig.plugins = {};
|
||||
}
|
||||
if (!currentConfig.plugins.entries) {
|
||||
currentConfig.plugins.entries = {};
|
||||
}
|
||||
if (!currentConfig.plugins.entries[channelType]) {
|
||||
currentConfig.plugins.entries[channelType] = {};
|
||||
}
|
||||
if (!currentConfig.plugins) currentConfig.plugins = {};
|
||||
if (!currentConfig.plugins.entries) currentConfig.plugins.entries = {};
|
||||
if (!currentConfig.plugins.entries[channelType]) currentConfig.plugins.entries[channelType] = {};
|
||||
currentConfig.plugins.entries[channelType].enabled = enabled;
|
||||
writeOpenClawConfig(currentConfig);
|
||||
await writeOpenClawConfig(currentConfig);
|
||||
console.log(`Set plugin channel ${channelType} enabled: ${enabled}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentConfig.channels) {
|
||||
currentConfig.channels = {};
|
||||
}
|
||||
|
||||
if (!currentConfig.channels[channelType]) {
|
||||
currentConfig.channels[channelType] = {};
|
||||
}
|
||||
|
||||
if (!currentConfig.channels) currentConfig.channels = {};
|
||||
if (!currentConfig.channels[channelType]) currentConfig.channels[channelType] = {};
|
||||
currentConfig.channels[channelType].enabled = enabled;
|
||||
writeOpenClawConfig(currentConfig);
|
||||
await writeOpenClawConfig(currentConfig);
|
||||
console.log(`Set channel ${channelType} enabled: ${enabled}`);
|
||||
}
|
||||
|
||||
// ── Validation ───────────────────────────────────────────────────
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
@@ -404,17 +355,9 @@ export interface CredentialValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
/** Extra info returned from the API (e.g. bot username, guild name) */
|
||||
details?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate channel credentials by calling the actual service APIs
|
||||
* This validates the raw config values BEFORE saving them.
|
||||
*
|
||||
* @param channelType - The channel type (e.g., 'discord', 'telegram')
|
||||
* @param config - The raw config values from the form
|
||||
*/
|
||||
export async function validateChannelCredentials(
|
||||
channelType: string,
|
||||
config: Record<string, string>
|
||||
@@ -425,14 +368,10 @@ export async function validateChannelCredentials(
|
||||
case 'telegram':
|
||||
return validateTelegramCredentials(config);
|
||||
default:
|
||||
// For channels without specific validation, just check required fields are present
|
||||
return { valid: true, errors: [], warnings: ['No online validation available for this channel type.'] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Discord bot token and optional guild/channel IDs
|
||||
*/
|
||||
async function validateDiscordCredentials(
|
||||
config: Record<string, string>
|
||||
): Promise<CredentialValidationResult> {
|
||||
@@ -443,12 +382,10 @@ async function validateDiscordCredentials(
|
||||
return { valid: false, errors: ['Bot token is required'], warnings: [] };
|
||||
}
|
||||
|
||||
// 1) Validate bot token by calling GET /users/@me
|
||||
try {
|
||||
const meResponse = await fetch('https://discord.com/api/v10/users/@me', {
|
||||
headers: { Authorization: `Bot ${token}` },
|
||||
});
|
||||
|
||||
if (!meResponse.ok) {
|
||||
if (meResponse.status === 401) {
|
||||
return { valid: false, errors: ['Invalid bot token. Please check and try again.'], warnings: [] };
|
||||
@@ -457,38 +394,25 @@ async function validateDiscordCredentials(
|
||||
const msg = (errorData as { message?: string }).message || `Discord API error: ${meResponse.status}`;
|
||||
return { valid: false, errors: [msg], warnings: [] };
|
||||
}
|
||||
|
||||
const meData = (await meResponse.json()) as { username?: string; id?: string; bot?: boolean };
|
||||
if (!meData.bot) {
|
||||
return {
|
||||
valid: false,
|
||||
errors: ['The provided token belongs to a user account, not a bot. Please use a bot token.'],
|
||||
warnings: [],
|
||||
};
|
||||
return { valid: false, errors: ['The provided token belongs to a user account, not a bot. Please use a bot token.'], warnings: [] };
|
||||
}
|
||||
result.details!.botUsername = meData.username || 'Unknown';
|
||||
result.details!.botId = meData.id || '';
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
errors: [`Connection error when validating bot token: ${error instanceof Error ? error.message : String(error)}`],
|
||||
warnings: [],
|
||||
};
|
||||
return { valid: false, errors: [`Connection error when validating bot token: ${error instanceof Error ? error.message : String(error)}`], warnings: [] };
|
||||
}
|
||||
|
||||
// 2) Validate guild ID (optional)
|
||||
const guildId = config.guildId?.trim();
|
||||
if (guildId) {
|
||||
try {
|
||||
const guildResponse = await fetch(`https://discord.com/api/v10/guilds/${guildId}`, {
|
||||
headers: { Authorization: `Bot ${token}` },
|
||||
});
|
||||
|
||||
if (!guildResponse.ok) {
|
||||
if (guildResponse.status === 403 || guildResponse.status === 404) {
|
||||
result.errors.push(
|
||||
`Cannot access guild (server) with ID "${guildId}". Make sure the bot has been invited to this server.`
|
||||
);
|
||||
result.errors.push(`Cannot access guild (server) with ID "${guildId}". Make sure the bot has been invited to this server.`);
|
||||
result.valid = false;
|
||||
} else {
|
||||
result.errors.push(`Failed to verify guild ID: Discord API returned ${guildResponse.status}`);
|
||||
@@ -503,19 +427,15 @@ async function validateDiscordCredentials(
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Validate channel ID (optional)
|
||||
const channelId = config.channelId?.trim();
|
||||
if (channelId) {
|
||||
try {
|
||||
const channelResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}`, {
|
||||
headers: { Authorization: `Bot ${token}` },
|
||||
});
|
||||
|
||||
if (!channelResponse.ok) {
|
||||
if (channelResponse.status === 403 || channelResponse.status === 404) {
|
||||
result.errors.push(
|
||||
`Cannot access channel with ID "${channelId}". Make sure the bot has permission to view this channel.`
|
||||
);
|
||||
result.errors.push(`Cannot access channel with ID "${channelId}". Make sure the bot has permission to view this channel.`);
|
||||
result.valid = false;
|
||||
} else {
|
||||
result.errors.push(`Failed to verify channel ID: Discord API returned ${channelResponse.status}`);
|
||||
@@ -524,12 +444,8 @@ async function validateDiscordCredentials(
|
||||
} else {
|
||||
const channelData = (await channelResponse.json()) as { name?: string; guild_id?: string };
|
||||
result.details!.channelName = channelData.name || 'Unknown';
|
||||
|
||||
// Cross-check: if both guild and channel are provided, make sure channel belongs to the guild
|
||||
if (guildId && channelData.guild_id && channelData.guild_id !== guildId) {
|
||||
result.errors.push(
|
||||
`Channel "${channelData.name}" does not belong to the specified guild. It belongs to a different server.`
|
||||
);
|
||||
result.errors.push(`Channel "${channelData.name}" does not belong to the specified guild. It belongs to a different server.`);
|
||||
result.valid = false;
|
||||
}
|
||||
}
|
||||
@@ -541,80 +457,52 @@ async function validateDiscordCredentials(
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate Telegram bot token
|
||||
*/
|
||||
async function validateTelegramCredentials(
|
||||
config: Record<string, string>
|
||||
): Promise<CredentialValidationResult> {
|
||||
const botToken = config.botToken?.trim();
|
||||
|
||||
const allowedUsers = config.allowedUsers?.trim();
|
||||
|
||||
if (!botToken) {
|
||||
return { valid: false, errors: ['Bot token is required'], warnings: [] };
|
||||
}
|
||||
|
||||
if (!allowedUsers) {
|
||||
return { valid: false, errors: ['At least one allowed user ID is required'], warnings: [] };
|
||||
}
|
||||
if (!botToken) return { valid: false, errors: ['Bot token is required'], warnings: [] };
|
||||
if (!allowedUsers) return { valid: false, errors: ['At least one allowed user ID is required'], warnings: [] };
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
|
||||
const data = (await response.json()) as { ok?: boolean; description?: string; result?: { username?: string } };
|
||||
|
||||
if (data.ok) {
|
||||
return {
|
||||
valid: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
details: { botUsername: data.result?.username || 'Unknown' },
|
||||
};
|
||||
return { valid: true, errors: [], warnings: [], details: { botUsername: data.result?.username || 'Unknown' } };
|
||||
}
|
||||
|
||||
return {
|
||||
valid: false,
|
||||
errors: [data.description || 'Invalid bot token'],
|
||||
warnings: [],
|
||||
};
|
||||
return { valid: false, errors: [data.description || 'Invalid bot token'], warnings: [] };
|
||||
} catch (error) {
|
||||
return {
|
||||
valid: false,
|
||||
errors: [`Connection error: ${error instanceof Error ? error.message : String(error)}`],
|
||||
warnings: [],
|
||||
};
|
||||
return { valid: false, errors: [`Connection error: ${error instanceof Error ? error.message : String(error)}`], warnings: [] };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Validate channel configuration using OpenClaw doctor
|
||||
*/
|
||||
export async function validateChannelConfig(channelType: string): Promise<ValidationResult> {
|
||||
const { execSync } = await import('child_process');
|
||||
const { exec } = await import('child_process');
|
||||
|
||||
const result: ValidationResult = {
|
||||
valid: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
};
|
||||
const result: ValidationResult = { valid: true, errors: [], warnings: [] };
|
||||
|
||||
try {
|
||||
// Get OpenClaw path
|
||||
const openclawPath = getOpenClawResolvedDir();
|
||||
|
||||
// Run openclaw doctor command to validate config
|
||||
const output = execSync(
|
||||
`node openclaw.mjs doctor --json 2>&1`,
|
||||
{
|
||||
cwd: openclawPath,
|
||||
encoding: 'utf-8',
|
||||
timeout: 30000,
|
||||
}
|
||||
);
|
||||
// Run openclaw doctor command to validate config (async to avoid
|
||||
// blocking the main thread).
|
||||
const output = await new Promise<string>((resolve, reject) => {
|
||||
exec(
|
||||
`node openclaw.mjs doctor --json 2>&1`,
|
||||
{
|
||||
cwd: openclawPath,
|
||||
encoding: 'utf-8',
|
||||
timeout: 30000,
|
||||
},
|
||||
(err, stdout) => {
|
||||
if (err) reject(err);
|
||||
else resolve(stdout);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Parse output for errors related to the channel
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
const lowerLine = line.toLowerCase();
|
||||
@@ -629,8 +517,7 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
|
||||
}
|
||||
}
|
||||
|
||||
// If no specific errors found, check if config exists and is valid
|
||||
const config = readOpenClawConfig();
|
||||
const config = await readOpenClawConfig();
|
||||
if (!config.channels?.[channelType]) {
|
||||
result.errors.push(`Channel ${channelType} is not configured`);
|
||||
result.valid = false;
|
||||
@@ -638,7 +525,6 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
|
||||
result.warnings.push(`Channel ${channelType} is disabled`);
|
||||
}
|
||||
|
||||
// Channel-specific validation
|
||||
if (channelType === 'discord') {
|
||||
const discordConfig = config.channels?.discord;
|
||||
if (!discordConfig?.token) {
|
||||
@@ -651,7 +537,6 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
|
||||
result.errors.push('Telegram: Bot token is required');
|
||||
result.valid = false;
|
||||
}
|
||||
// Check allowed users (stored as allowFrom array)
|
||||
const allowedUsers = telegramConfig?.allowFrom as string[] | undefined;
|
||||
if (!allowedUsers || allowedUsers.length === 0) {
|
||||
result.errors.push('Telegram: Allowed User IDs are required');
|
||||
@@ -666,7 +551,6 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Check for config errors in the error message
|
||||
if (errorMessage.includes('Unrecognized key') || errorMessage.includes('invalid config')) {
|
||||
result.errors.push(errorMessage);
|
||||
result.valid = false;
|
||||
@@ -674,11 +558,8 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
|
||||
result.errors.push('OpenClaw not found. Please ensure OpenClaw is installed.');
|
||||
result.valid = false;
|
||||
} else {
|
||||
// Doctor command might fail but config could still be valid
|
||||
// Just log it and do basic validation
|
||||
console.warn('Doctor command failed:', errorMessage);
|
||||
|
||||
const config = readOpenClawConfig();
|
||||
const config = await readOpenClawConfig();
|
||||
if (config.channels?.[channelType]) {
|
||||
result.valid = true;
|
||||
} else {
|
||||
@@ -689,4 +570,4 @@ export async function validateChannelConfig(channelType: string): Promise<Valida
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user