diff --git a/package.json b/package.json index c7a6825..044a338 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,11 @@ "npm": "@wecom/wecom-openclaw-plugin", "version": "1.0.12" }, + { + "id": "openclaw-weixin", + "npm": "@tencent-weixin/openclaw-weixin", + "version": "1.0.2" + }, { "id": "moltbot-popo", "npm": "moltbot-popo", diff --git a/public/weixin.png b/public/weixin.png new file mode 100644 index 0000000..7b6a56a Binary files /dev/null and b/public/weixin.png differ diff --git a/scripts/ensure-openclaw-plugins.cjs b/scripts/ensure-openclaw-plugins.cjs index cdb0e48..a4b16a6 100644 --- a/scripts/ensure-openclaw-plugins.cjs +++ b/scripts/ensure-openclaw-plugins.cjs @@ -298,3 +298,26 @@ for (const plugin of plugins) { } log(`All ${plugins.length} plugin(s) installed successfully.`); + +// --- Post-install patch: openclaw-weixin gatewayMethods --- +// The openclaw-weixin plugin defines loginWithQrStart/loginWithQrWait in its +// gateway adapter but does not declare gatewayMethods on the channel plugin +// object. Without this declaration, the gateway's resolveWebLoginProvider() +// cannot discover the plugin for web.login.start/web.login.wait RPC calls. +// Patch channel.ts to inject the missing property. +const weixinChannelPath = path.join(runtimeExtensionsDir, 'openclaw-weixin', 'src', 'channel.ts'); +if (fs.existsSync(weixinChannelPath)) { + let src = fs.readFileSync(weixinChannelPath, 'utf8'); + if (!src.includes('gatewayMethods')) { + // Insert gatewayMethods right after the configSchema block in weixinPlugin + const marker = 'configSchema: {'; + const idx = src.indexOf(marker); + if (idx !== -1) { + src = src.slice(0, idx) + 'gatewayMethods: ["web.login.start", "web.login.wait"],\n ' + src.slice(idx); + fs.writeFileSync(weixinChannelPath, src); + log('Patched openclaw-weixin/src/channel.ts: added gatewayMethods declaration'); + } + } else { + log('openclaw-weixin/src/channel.ts already has gatewayMethods, skipping patch'); + } +} diff --git a/src/main/im/imGatewayManager.ts b/src/main/im/imGatewayManager.ts index 4c2443a..9fe0052 100644 --- a/src/main/im/imGatewayManager.ts +++ b/src/main/im/imGatewayManager.ts @@ -1,7 +1,7 @@ /** * IM Gateway Manager * Unified manager for DingTalk, Feishu, NIM, Xiaomifeng gateways - * and Telegram, Discord, QQ, WeCom, POPO via OpenClaw + * and Telegram, Discord, QQ, WeCom, Weixin, POPO via OpenClaw */ import { EventEmitter } from 'events'; @@ -170,6 +170,8 @@ export class IMGatewayManager extends EventEmitter { // WeCom runs via OpenClaw; no direct gateway events to forward + // Weixin runs via OpenClaw; no direct gateway events to forward + // POPO runs via OpenClaw; no direct gateway events to forward } @@ -193,6 +195,8 @@ export class IMGatewayManager extends EventEmitter { // WeCom runs via OpenClaw; no direct reconnection needed + // Weixin runs via OpenClaw; no direct reconnection needed + // POPO runs via OpenClaw; no direct reconnection needed } @@ -276,6 +280,7 @@ export class IMGatewayManager extends EventEmitter { target = this.nimGateway.getNotificationTarget(); } // WeCom runs via OpenClaw; notification target not managed locally + // Weixin runs via OpenClaw; notification target not managed locally // POPO runs via OpenClaw; notification target not managed locally if (target != null) { this.imStore.setNotificationTarget(platform, target); @@ -297,6 +302,7 @@ export class IMGatewayManager extends EventEmitter { this.nimGateway.setNotificationTarget(target); } // WeCom runs via OpenClaw; notification target not managed locally + // Weixin runs via OpenClaw; notification target not managed locally // POPO runs via OpenClaw; notification target not managed locally console.log(`[IMGatewayManager] Restored notification target for ${platform}`); } catch (err: any) { @@ -410,6 +416,8 @@ export class IMGatewayManager extends EventEmitter { // WeCom runs via OpenClaw; config changes are synced via OpenClawConfigSync + // Weixin runs via OpenClaw; config changes are synced via OpenClawConfigSync + // POPO runs via OpenClaw; config changes are synced via OpenClawConfigSync } @@ -496,6 +504,13 @@ export class IMGatewayManager extends EventEmitter { lastInboundAt: null as number | null, lastOutboundAt: null as number | null, }, + weixin: { + connected: Boolean(config.weixin?.enabled), + startedAt: null as number | null, + lastError: null as string | null, + lastInboundAt: null as number | null, + lastOutboundAt: null as number | null, + }, popo: { connected: Boolean(config.popo?.enabled && config.popo.appKey && config.popo.appSecret && config.popo.aesKey && (config.popo.connectionMode === 'websocket' || config.popo.token)), startedAt: null as number | null, @@ -539,6 +554,11 @@ export class IMGatewayManager extends EventEmitter { return this.testWecomOpenClawConnectivity(configOverride); } + // Weixin always uses OpenClaw mode + if (platform === 'weixin') { + return this.testWeixinOpenClawConnectivity(configOverride); + } + // POPO always uses OpenClaw mode if (platform === 'popo') { return this.testPopoOpenClawConnectivity(configOverride); @@ -746,6 +766,12 @@ export class IMGatewayManager extends EventEmitter { await this.syncOpenClawConfig?.(); await this.ensureOpenClawGatewayConnected?.(); return; + } else if (platform === 'weixin') { + // Weixin runs via OpenClaw gateway (weixin-openclaw-plugin) + console.log('[IMGatewayManager] Weixin in OpenClaw mode, syncing config instead of starting direct gateway'); + await this.syncOpenClawConfig?.(); + await this.ensureOpenClawGatewayConnected?.(); + return; } else if (platform === 'popo') { // POPO runs via OpenClaw gateway (moltbot-popo plugin) console.log('[IMGatewayManager] POPO in OpenClaw mode, syncing config instead of starting direct gateway'); @@ -796,6 +822,11 @@ export class IMGatewayManager extends EventEmitter { console.log('[IMGatewayManager] WeCom in OpenClaw mode, syncing disabled config'); await this.syncOpenClawConfig?.(); return; + } else if (platform === 'weixin') { + // Weixin runs via OpenClaw gateway + console.log('[IMGatewayManager] Weixin in OpenClaw mode, syncing disabled config'); + await this.syncOpenClawConfig?.(); + return; } else if (platform === 'popo') { // POPO runs via OpenClaw gateway console.log('[IMGatewayManager] POPO in OpenClaw mode, syncing disabled config'); @@ -807,7 +838,7 @@ export class IMGatewayManager extends EventEmitter { /** * Start all enabled gateways. * - * OpenClaw platforms (dingtalk/feishu/telegram/discord/qq/wecom/popo/nim) are batched + * OpenClaw platforms (dingtalk/feishu/telegram/discord/qq/wecom/weixin/popo/nim) are batched * so that `syncOpenClawConfig` + `ensureOpenClawGatewayConnected` are called * only **once** regardless of how many OpenClaw platforms are enabled. * This avoids N serial gateway restarts which cause message loss, Telegram @@ -851,6 +882,9 @@ export class IMGatewayManager extends EventEmitter { if (config.wecom?.enabled && config.wecom?.botId && config.wecom?.secret) { openClawPlatformsToStart.push('wecom'); } + if (config.weixin?.enabled) { + openClawPlatformsToStart.push('weixin'); + } if (config.popo?.enabled && config.popo?.appKey && config.popo?.appSecret && config.popo?.aesKey && (config.popo.connectionMode === 'websocket' || config.popo.token)) { openClawPlatformsToStart.push('popo'); } @@ -913,6 +947,11 @@ export class IMGatewayManager extends EventEmitter { const config = this.getConfig(); return Boolean(config.wecom?.enabled && config.wecom.botId && config.wecom.secret); } + if (platform === 'weixin') { + // Weixin runs via OpenClaw; consider it connected when enabled + const config = this.getConfig(); + return Boolean(config.weixin?.enabled); + } if (platform === 'popo') { // POPO runs via OpenClaw; consider it connected when enabled and configured const config = this.getConfig(); @@ -937,6 +976,9 @@ export class IMGatewayManager extends EventEmitter { } else if (platform === 'wecom') { // WeCom runs via OpenClaw; notifications are handled by the wecom-openclaw-plugin console.log('[IMGatewayManager] WeCom notification via OpenClaw not yet supported'); + } else if (platform === 'weixin') { + // Weixin runs via OpenClaw; notifications are handled by the weixin-openclaw-plugin + console.log('[IMGatewayManager] Weixin notification via OpenClaw not yet supported'); } else if (platform === 'popo') { // POPO runs via OpenClaw; notifications are handled by the moltbot-popo plugin console.log('[IMGatewayManager] POPO notification via OpenClaw not yet supported'); @@ -966,6 +1008,9 @@ export class IMGatewayManager extends EventEmitter { } else if (platform === 'wecom') { // WeCom runs via OpenClaw; notifications are handled by the wecom-openclaw-plugin console.log('[IMGatewayManager] WeCom notification with media via OpenClaw not yet supported'); + } else if (platform === 'weixin') { + // Weixin runs via OpenClaw; notifications are handled by the weixin-openclaw-plugin + console.log('[IMGatewayManager] Weixin notification with media via OpenClaw not yet supported'); } else if (platform === 'popo') { // POPO runs via OpenClaw; notifications are handled by the moltbot-popo plugin console.log('[IMGatewayManager] POPO notification with media via OpenClaw not yet supported'); @@ -1338,6 +1383,106 @@ export class IMGatewayManager extends EventEmitter { return { platform, testedAt, verdict, checks }; } + private async testWeixinOpenClawConnectivity( + configOverride?: Partial + ): Promise { + const checks: IMConnectivityCheck[] = []; + const testedAt = Date.now(); + const platform: IMPlatform = 'weixin'; + + const mergedConfig = this.buildMergedConfig(configOverride); + const wxConfig = mergedConfig.weixin; + + // Weixin has no credentials; just check if enabled + if (!wxConfig?.enabled) { + checks.push({ + code: 'gateway_running', + level: 'info', + message: '微信渠道当前未启用。', + suggestion: '请启用微信渠道后重新测试连通性。', + }); + return { platform, testedAt, verdict: 'pass', checks }; + } + + // Config completeness passes (no credentials needed) + checks.push({ + code: 'auth_check', + level: 'pass', + message: '微信配置已就绪。', + }); + + // OpenClaw Gateway running info + checks.push({ + code: 'gateway_running', + level: 'info', + message: '微信通过 OpenClaw 运行时运行,Bot 将在 OpenClaw Gateway 启动后自动连接。', + }); + + const verdict: IMConnectivityVerdict = checks.some(c => c.level === 'fail') + ? 'fail' + : checks.some(c => c.level === 'warn') + ? 'warn' + : 'pass'; + + return { platform, testedAt, verdict, checks }; + } + + /** + * Start Weixin QR code login via OpenClaw Gateway RPC. + * Returns the QR code data URL and a session key for polling. + */ + async weixinQrLoginStart(): Promise<{ qrDataUrl?: string; message: string; sessionKey?: string }> { + const client = this.getOpenClawGatewayClient?.(); + if (!client) { + await this.ensureOpenClawGatewayReady?.(); + const retryClient = this.getOpenClawGatewayClient?.(); + if (!retryClient) { + return { message: 'OpenClaw Gateway is not running. Please start OpenClaw engine first.' }; + } + return this.doWeixinQrLoginStart(retryClient); + } + return this.doWeixinQrLoginStart(client); + } + + private async doWeixinQrLoginStart(client: GatewayClientLike): Promise<{ qrDataUrl?: string; message: string; sessionKey?: string }> { + try { + const result = await client.request<{ qrDataUrl?: string; message: string; sessionKey?: string }>( + 'web.login.start', + { force: true, timeoutMs: 300000, verbose: true }, + ); + console.log('[IMGatewayManager] Weixin QR login start result:', result.message); + return result; + } catch (err) { + console.error('[IMGatewayManager] Weixin QR login start failed:', err); + return { message: `Failed to start Weixin login: ${String(err)}` }; + } + } + + /** + * Wait for Weixin QR code scan completion via OpenClaw Gateway RPC. + */ + async weixinQrLoginWait(accountId?: string): Promise<{ connected: boolean; message: string; accountId?: string }> { + const client = this.getOpenClawGatewayClient?.(); + if (!client) { + return { connected: false, message: 'OpenClaw Gateway is not connected.' }; + } + try { + const result = await client.request<{ connected: boolean; message: string; accountId?: string }>( + 'web.login.wait', + { timeoutMs: 480000, ...(accountId ? { accountId } : {}) }, + ); + console.log('[IMGatewayManager] Weixin QR login wait result:', result.message, 'connected:', result.connected); + if (result.connected) { + // Sync config to pick up new account + await this.syncOpenClawConfig?.(); + } + return result; + } catch (err) { + console.error('[IMGatewayManager] Weixin QR login wait failed:', err); + return { connected: false, message: `Login failed: ${String(err)}` }; + } + } + private async testNimOpenClawConnectivity( configOverride?: Partial ): Promise { @@ -1464,6 +1609,7 @@ export class IMGatewayManager extends EventEmitter { nim: { ...current.nim, ...(configOverride.nim || {}) }, xiaomifeng: { ...current.xiaomifeng, ...(configOverride.xiaomifeng || {}) }, wecom: { ...current.wecom, ...(configOverride.wecom || {}) }, + weixin: { ...current.weixin, ...(configOverride.weixin || {}) }, popo: { ...current.popo, ...(configOverride.popo || {}) }, settings: { ...current.settings, ...(configOverride.settings || {}) }, }; @@ -1510,6 +1656,10 @@ export class IMGatewayManager extends EventEmitter { if (!config.wecom?.secret) fields.push('secret'); return fields; } + if (platform === 'weixin') { + // Weixin has no credentials; nothing to check + return []; + } if (platform === 'popo') { const fields: string[] = []; if (!config.popo?.appKey) fields.push('appKey'); @@ -1578,6 +1728,11 @@ export class IMGatewayManager extends EventEmitter { } + if (platform === 'weixin') { + // Weixin has no credentials to probe; just confirm enabled + return '微信配置已就绪,通过 OpenClaw 运行。'; + } + if (platform === 'popo') { const { appKey, appSecret, token, aesKey, connectionMode } = config.popo; const isWebhook = (connectionMode ?? 'websocket') === 'webhook'; @@ -2081,6 +2236,7 @@ export class IMGatewayManager extends EventEmitter { if (platform === 'xiaomifeng') return status.xiaomifeng.startedAt; if (platform === 'qq') return status.qq.startedAt; if (platform === 'wecom') return status.wecom.startedAt; + if (platform === 'weixin') return status.weixin.startedAt; if (platform === 'popo') return status.popo.startedAt; return status.discord.startedAt; } @@ -2093,6 +2249,7 @@ export class IMGatewayManager extends EventEmitter { if (platform === 'xiaomifeng') return status.xiaomifeng.lastInboundAt; if (platform === 'qq') return status.qq.lastInboundAt; if (platform === 'wecom') return status.wecom.lastInboundAt; + if (platform === 'weixin') return status.weixin.lastInboundAt; if (platform === 'popo') return status.popo.lastInboundAt; return status.discord.lastInboundAt; } @@ -2105,6 +2262,7 @@ export class IMGatewayManager extends EventEmitter { if (platform === 'xiaomifeng') return status.xiaomifeng.lastOutboundAt; if (platform === 'qq') return status.qq.lastOutboundAt; if (platform === 'wecom') return status.wecom.lastOutboundAt; + if (platform === 'weixin') return status.weixin.lastOutboundAt; if (platform === 'popo') return status.popo.lastOutboundAt; return status.discord.lastOutboundAt; } @@ -2117,6 +2275,7 @@ export class IMGatewayManager extends EventEmitter { if (platform === 'xiaomifeng') return status.xiaomifeng.lastError; if (platform === 'qq') return status.qq.lastError; if (platform === 'wecom') return status.wecom.lastError; + if (platform === 'weixin') return status.weixin.lastError; if (platform === 'popo') return status.popo.lastError; return status.discord.lastError; } diff --git a/src/main/im/imStore.ts b/src/main/im/imStore.ts index f316f4d..18b0f72 100644 --- a/src/main/im/imStore.ts +++ b/src/main/im/imStore.ts @@ -16,6 +16,7 @@ import { WecomConfig, WecomOpenClawConfig, PopoOpenClawConfig, + WeixinOpenClawConfig, IMSettings, IMPlatform, IMSessionMapping, @@ -28,6 +29,7 @@ import { DEFAULT_XIAOMIFENG_CONFIG, DEFAULT_WECOM_CONFIG, DEFAULT_POPO_CONFIG, + DEFAULT_WEIXIN_CONFIG, DEFAULT_IM_SETTINGS, } from './types'; @@ -76,7 +78,7 @@ export class IMStore { * Migrate existing IM configs to ensure stable defaults. */ private migrateDefaults(): void { - const platforms = ['dingtalk', 'feishu', 'telegram', 'discord', 'nim', 'xiaomifeng', 'qq', 'wecom', 'popo'] as const; + const platforms = ['dingtalk', 'feishu', 'telegram', 'discord', 'nim', 'xiaomifeng', 'qq', 'wecom', 'popo', 'weixin'] as const; let changed = false; for (const platform of platforms) { @@ -354,6 +356,7 @@ export class IMStore { const qq = this.getConfigValue('qq') ?? DEFAULT_QQ_CONFIG; const wecom = this.getConfigValue('wecomOpenClaw') ?? DEFAULT_WECOM_CONFIG; const popo = this.getConfigValue('popo') ?? DEFAULT_POPO_CONFIG; + const weixin = this.getConfigValue('weixin') ?? DEFAULT_WEIXIN_CONFIG; const settings = this.getConfigValue('settings') ?? DEFAULT_IM_SETTINGS; // Resolve enabled field: default to false for safety @@ -377,6 +380,7 @@ export class IMStore { qq: resolveEnabled(qq, DEFAULT_QQ_CONFIG), wecom: resolveEnabled(wecom, DEFAULT_WECOM_CONFIG), popo: resolveEnabled(popo, DEFAULT_POPO_CONFIG), + weixin: resolveEnabled(weixin, DEFAULT_WEIXIN_CONFIG), settings: { ...DEFAULT_IM_SETTINGS, ...settings }, }; } @@ -409,6 +413,9 @@ export class IMStore { if (config.popo) { this.setPopoConfig(config.popo); } + if (config.weixin) { + this.setWeixinConfig(config.weixin); + } if (config.settings) { this.setIMSettings(config.settings); } @@ -522,6 +529,18 @@ export class IMStore { this.setConfigValue('popo', { ...current, ...config }); } + // ==================== Weixin (微信) ==================== + + getWeixinConfig(): WeixinOpenClawConfig { + const stored = this.getConfigValue('weixin'); + return { ...DEFAULT_WEIXIN_CONFIG, ...stored }; + } + + setWeixinConfig(config: Partial): void { + const current = this.getWeixinConfig(); + this.setConfigValue('weixin', { ...current, ...config }); + } + // ==================== IM Settings ==================== getIMSettings(): IMSettings { diff --git a/src/main/im/types.ts b/src/main/im/types.ts index 2a54ff3..7d79771 100644 --- a/src/main/im/types.ts +++ b/src/main/im/types.ts @@ -277,9 +277,28 @@ export interface PopoGatewayStatus { lastOutboundAt: number | null; } +// ==================== Weixin (微信) Types ==================== + +export interface WeixinOpenClawConfig { + enabled: boolean; + dmPolicy: 'open' | 'pairing' | 'allowlist' | 'disabled'; + allowFrom: string[]; + groupPolicy: 'open' | 'allowlist' | 'disabled'; + groupAllowFrom: string[]; + debug: boolean; +} + +export interface WeixinGatewayStatus { + connected: boolean; + startedAt: number | null; + lastError: string | null; + lastInboundAt: number | null; + lastOutboundAt: number | null; +} + // ==================== Common IM Types ==================== -export type IMPlatform = 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo'; +export type IMPlatform = 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo' | 'weixin'; export interface IMGatewayConfig { dingtalk: DingTalkOpenClawConfig; @@ -291,6 +310,7 @@ export interface IMGatewayConfig { xiaomifeng: XiaomifengConfig; wecom: WecomOpenClawConfig; popo: PopoOpenClawConfig; + weixin: WeixinOpenClawConfig; settings: IMSettings; } @@ -309,6 +329,7 @@ export interface IMGatewayStatus { xiaomifeng: XiaomifengGatewayStatus; wecom: WecomGatewayStatus; popo: PopoGatewayStatus; + weixin: WeixinGatewayStatus; } // ==================== Media Attachment Types ==================== @@ -549,6 +570,15 @@ export const DEFAULT_POPO_CONFIG: PopoOpenClawConfig = { debug: true, }; +export const DEFAULT_WEIXIN_CONFIG: WeixinOpenClawConfig = { + enabled: false, + dmPolicy: 'open', + allowFrom: [], + groupPolicy: 'open', + groupAllowFrom: [], + debug: true, +}; + export const DEFAULT_IM_SETTINGS: IMSettings = { systemPrompt: '', skillsEnabled: true, @@ -564,6 +594,7 @@ export const DEFAULT_IM_CONFIG: IMGatewayConfig = { xiaomifeng: DEFAULT_XIAOMIFENG_CONFIG, wecom: DEFAULT_WECOM_CONFIG, popo: DEFAULT_POPO_CONFIG, + weixin: DEFAULT_WEIXIN_CONFIG, settings: DEFAULT_IM_SETTINGS, }; @@ -637,6 +668,14 @@ export const DEFAULT_POPO_STATUS: PopoGatewayStatus = { lastOutboundAt: null, }; +export const DEFAULT_WEIXIN_STATUS: WeixinGatewayStatus = { + connected: false, + startedAt: null, + lastError: null, + lastInboundAt: null, + lastOutboundAt: null, +}; + export const DEFAULT_IM_STATUS: IMGatewayStatus = { dingtalk: DEFAULT_DINGTALK_STATUS, feishu: DEFAULT_FEISHU_STATUS, @@ -654,6 +693,7 @@ export const DEFAULT_IM_STATUS: IMGatewayStatus = { xiaomifeng: DEFAULT_XIAOMIFENG_STATUS, wecom: DEFAULT_WECOM_STATUS, popo: DEFAULT_POPO_STATUS, + weixin: DEFAULT_WEIXIN_STATUS, }; // ==================== Media Marker Types ==================== diff --git a/src/main/libs/openclawConfigSync.ts b/src/main/libs/openclawConfigSync.ts index 417c9f8..bee96ff 100644 --- a/src/main/libs/openclawConfigSync.ts +++ b/src/main/libs/openclawConfigSync.ts @@ -3,7 +3,7 @@ import fs from 'fs'; import path from 'path'; import type { CoworkConfig, CoworkExecutionMode } from '../coworkStore'; import type { TelegramOpenClawConfig, DiscordOpenClawConfig } from '../im/types'; -import type { DingTalkOpenClawConfig, FeishuOpenClawConfig, QQOpenClawConfig, WecomOpenClawConfig, PopoOpenClawConfig, NimConfig } from '../im/types'; +import type { DingTalkOpenClawConfig, FeishuOpenClawConfig, QQOpenClawConfig, WecomOpenClawConfig, PopoOpenClawConfig, NimConfig, WeixinOpenClawConfig } from '../im/types'; import { resolveRawApiConfig } from './claudeSettings'; import type { OpenClawEngineManager } from './openclawEngineManager'; import { parseChannelSessionKey } from './openclawChannelSessionSync'; @@ -430,6 +430,7 @@ type OpenClawConfigSyncDeps = { getWecomConfig: () => WecomOpenClawConfig | null; getPopoConfig: () => PopoOpenClawConfig | null; getNimConfig: () => NimConfig | null; + getWeixinConfig: () => WeixinOpenClawConfig | null; getMcpBridgeConfig?: () => McpBridgeConfig | null; getSkillsList?: () => Array<{ id: string; enabled: boolean }>; }; @@ -445,6 +446,7 @@ export class OpenClawConfigSync { private readonly getWecomConfig: () => WecomOpenClawConfig | null; private readonly getPopoConfig: () => PopoOpenClawConfig | null; private readonly getNimConfig: () => NimConfig | null; + private readonly getWeixinConfig: () => WeixinOpenClawConfig | null; private readonly getMcpBridgeConfig?: () => McpBridgeConfig | null; private readonly getSkillsList?: () => Array<{ id: string; enabled: boolean }>; @@ -459,6 +461,7 @@ export class OpenClawConfigSync { this.getWecomConfig = deps.getWecomConfig; this.getPopoConfig = deps.getPopoConfig; this.getNimConfig = deps.getNimConfig; + this.getWeixinConfig = deps.getWeixinConfig; this.getMcpBridgeConfig = deps.getMcpBridgeConfig; this.getSkillsList = deps.getSkillsList; } @@ -525,9 +528,11 @@ export class OpenClawConfigSync { const wecomConfig = this.getWecomConfig(); const popoConfig = this.getPopoConfig(); - + const nimConfig = this.getNimConfig(); + const weixinConfig = this.getWeixinConfig(); + const hasAnyChannel = hasDingTalkOpenClaw; const managedConfig: Record = { @@ -605,6 +610,7 @@ export class OpenClawConfigSync { if (id === 'wecom-openclaw-plugin') return !!(wecomConfig?.enabled && wecomConfig.botId); if (id === 'moltbot-popo') return !!(popoConfig?.enabled && popoConfig.appKey); if (id === 'nim') return !!(nimConfig?.enabled && nimConfig.appKey && nimConfig.account && nimConfig.token); + if (id === 'openclaw-weixin') return true; // Always keep enabled for QR login discovery return true; // other plugins stay enabled })(); return [id, { enabled: pluginEnabled }]; @@ -895,6 +901,26 @@ export class OpenClawConfigSync { managedConfig.channels = { ...(managedConfig.channels as Record || {}), 'nim': nimChannel }; } + // Sync Weixin OpenClaw channel config (via openclaw-weixin-cli plugin) + if (weixinConfig?.enabled) { + const weixinChannel: Record = { + enabled: true, + dmPolicy: weixinConfig.dmPolicy || 'open', + allowFrom: (() => { + const ids = weixinConfig.allowFrom?.length ? [...weixinConfig.allowFrom] : []; + if (weixinConfig.dmPolicy === 'open' && !ids.includes('*')) ids.push('*'); + return ids; + })(), + groupPolicy: weixinConfig.groupPolicy || 'open', + groupAllowFrom: (() => { + const ids = weixinConfig.groupAllowFrom?.length ? [...weixinConfig.groupAllowFrom] : []; + if (weixinConfig.groupPolicy === 'open' && !ids.includes('*')) ids.push('*'); + return ids; + })(), + }; + managedConfig.channels = { ...(managedConfig.channels as Record || {}), 'openclaw-weixin': weixinChannel }; + } + const nextContent = `${JSON.stringify(managedConfig, null, 2)}\n`; let currentContent = ''; try { diff --git a/src/main/main.ts b/src/main/main.ts index fdbf610..f49176b 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -792,6 +792,13 @@ const getOpenClawConfigSync = (): OpenClawConfigSync => { return null; } }, + getWeixinConfig: () => { + try { + return getIMGatewayManager().getConfig().weixin; + } catch { + return null; + } + }, getDiscordOpenClawConfig: () => { try { return getIMGatewayManager()?.getConfig()?.discord ?? null; @@ -2952,6 +2959,25 @@ if (!gotTheLock) { } }); + // Weixin QR login + ipcMain.handle('im:weixin:qr-login-start', async () => { + try { + const result = await getIMGatewayManager().weixinQrLoginStart(); + return { success: true, ...result }; + } catch (error) { + return { success: false, message: error instanceof Error ? error.message : 'Failed to start Weixin QR login' }; + } + }); + + ipcMain.handle('im:weixin:qr-login-wait', async (_event, accountId?: string) => { + try { + const result = await getIMGatewayManager().weixinQrLoginWait(accountId); + return { success: true, ...result }; + } catch (error) { + return { success: false, connected: false, message: error instanceof Error ? error.message : 'Weixin QR login failed' }; + } + }); + ipcMain.handle('im:status:get', async () => { try { const status = getIMGatewayManager().getStatus(); diff --git a/src/main/preload.ts b/src/main/preload.ts index c5a5e9c..80d3f2f 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -288,10 +288,10 @@ contextBridge.exposeInMainWorld('electron', { syncConfig: () => ipcRenderer.invoke('im:config:sync'), // Gateway control - startGateway: (platform: 'dingtalk' | 'feishu' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo') => ipcRenderer.invoke('im:gateway:start', platform), - stopGateway: (platform: 'dingtalk' | 'feishu' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo') => ipcRenderer.invoke('im:gateway:stop', platform), + startGateway: (platform: 'dingtalk' | 'feishu' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo' | 'weixin') => ipcRenderer.invoke('im:gateway:start', platform), + stopGateway: (platform: 'dingtalk' | 'feishu' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo' | 'weixin') => ipcRenderer.invoke('im:gateway:stop', platform), testGateway: ( - platform: 'dingtalk' | 'feishu' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo', + platform: 'dingtalk' | 'feishu' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo' | 'weixin', configOverride?: any ) => ipcRenderer.invoke('im:gateway:test', platform, configOverride), @@ -302,6 +302,10 @@ contextBridge.exposeInMainWorld('electron', { getOpenClawConfigSchema: () => ipcRenderer.invoke('im:openclaw:config-schema'), + // Weixin QR login + weixinQrLoginStart: () => ipcRenderer.invoke('im:weixin:qr-login-start'), + weixinQrLoginWait: (accountId?: string) => ipcRenderer.invoke('im:weixin:qr-login-wait', accountId), + // Pairing listPairingRequests: (platform: string) => ipcRenderer.invoke('im:pairing:list', platform), approvePairingCode: (platform: string, code: string) => ipcRenderer.invoke('im:pairing:approve', platform, code), diff --git a/src/renderer/components/im/IMSettings.tsx b/src/renderer/components/im/IMSettings.tsx index db7cf7c..aca0564 100644 --- a/src/renderer/components/im/IMSettings.tsx +++ b/src/renderer/components/im/IMSettings.tsx @@ -9,9 +9,9 @@ import { SignalIcon, XMarkIcon, CheckCircleIcon, XCircleIcon, ExclamationTriangl import { EyeIcon, EyeSlashIcon, XCircleIcon as XCircleIconSolid } from '@heroicons/react/20/solid'; import { RootState } from '../../store'; import { imService } from '../../services/im'; -import { setDingTalkConfig, setFeishuConfig, setTelegramOpenClawConfig, setQQConfig, setDiscordConfig, setNimConfig, setXiaomifengConfig, setWecomConfig, setPopoConfig, clearError } from '../../store/slices/imSlice'; +import { setDingTalkConfig, setFeishuConfig, setTelegramOpenClawConfig, setQQConfig, setDiscordConfig, setNimConfig, setXiaomifengConfig, setWecomConfig, setWeixinConfig, setPopoConfig, clearError } from '../../store/slices/imSlice'; import { i18nService } from '../../services/i18n'; -import type { IMPlatform, IMConnectivityCheck, IMConnectivityTestResult, IMGatewayConfig, TelegramOpenClawConfig, DiscordOpenClawConfig, FeishuOpenClawConfig, DingTalkOpenClawConfig, QQOpenClawConfig, WecomOpenClawConfig, PopoOpenClawConfig } from '../../types/im'; +import type { IMPlatform, IMConnectivityCheck, IMConnectivityTestResult, IMGatewayConfig, TelegramOpenClawConfig, DiscordOpenClawConfig, FeishuOpenClawConfig, DingTalkOpenClawConfig, QQOpenClawConfig, WecomOpenClawConfig, WeixinOpenClawConfig, PopoOpenClawConfig } from '../../types/im'; import { getVisibleIMPlatforms } from '../../utils/regionFilter'; import WecomAIBotSDK from '@wecom/wecom-aibot-sdk'; import { QRCodeSVG } from 'qrcode.react'; @@ -28,6 +28,7 @@ const platformLogos: Record = { discord: 'discord.svg', nim: 'nim.png', xiaomifeng: 'xiaomifeng.png', + weixin: 'weixin.png', wecom: 'wecom.png', popo: 'popo.png', }; @@ -40,6 +41,7 @@ const IM_GUIDE_URLS: Partial> = { qq: 'https://lobsterai.youdao.com/#/docs/lobsterai_im_bot_config_guide/qqqq-bot', telegram: 'https://lobsterai.youdao.com/#/en/docs/lobsterai_im_bot_config_guide/telegram-bot-configuration', discord: 'https://lobsterai.youdao.com/#/en/docs/lobsterai_im_bot_config_guide/discord-bot-configuration', + weixin: '', popo: '', }; @@ -133,6 +135,11 @@ const IMSettings: React.FC = () => { // WeCom quick setup state const [wecomQuickSetupStatus, setWecomQuickSetupStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle'); const [wecomQuickSetupError, setWecomQuickSetupError] = useState(''); + // Weixin QR login state + const [weixinQrStatus, setWeixinQrStatus] = useState<'idle' | 'loading' | 'showing' | 'waiting' | 'success' | 'error'>('idle'); + const [weixinQrUrl, setWeixinQrUrl] = useState(''); + const [weixinQrError, setWeixinQrError] = useState(''); + const [, setWeixinQrSessionKey] = useState(''); const [localIp, setLocalIp] = useState(''); const isMountedRef = useRef(true); @@ -244,6 +251,16 @@ const IMSettings: React.FC = () => { } }, [activePlatform]); + // Reset weixin QR login state when switching away from weixin + useEffect(() => { + if (activePlatform !== 'weixin') { + setWeixinQrStatus('idle'); + setWeixinQrUrl(''); + setWeixinQrError(''); + setWeixinQrSessionKey(''); + } + }, [activePlatform]); + // Reset password visibility when switching platforms useEffect(() => { setShowSecrets({}); @@ -393,6 +410,10 @@ const IMSettings: React.FC = () => { const [popoAllowedUserIdInput, setPopoAllowedUserIdInput] = useState(''); const [popoGroupAllowIdInput, setPopoGroupAllowIdInput] = useState(''); + // State for Weixin allow-from inputs + const [weixinAllowedUserIdInput, setWeixinAllowedUserIdInput] = useState(''); + const [weixinGroupAllowIdInput, setWeixinGroupAllowIdInput] = useState(''); + // Handle Xiaomifeng config change const handleXiaomifengChange = (field: 'clientId' | 'secret', value: string) => { dispatch(setXiaomifengConfig({ [field]: value })); @@ -411,6 +432,19 @@ const IMSettings: React.FC = () => { await imService.persistConfig({ wecom: configToSave }); }; + // Handle Weixin OpenClaw config change + const weixinOpenClawConfig = config.weixin; + const handleWeixinOpenClawChange = (update: Partial) => { + dispatch(setWeixinConfig(update)); + }; + const handleSaveWeixinOpenClawConfig = async (override?: Partial) => { + if (!configLoaded) return; + const configToSave = override + ? { ...weixinOpenClawConfig, ...override } + : weixinOpenClawConfig; + await imService.persistConfig({ weixin: configToSave }); + }; + // Handle POPO OpenClaw config change const popoConfig = config.popo; const handlePopoChange = (update: Partial) => { @@ -462,6 +496,46 @@ const IMSettings: React.FC = () => { } }; + const handleWeixinQrLogin = async () => { + setWeixinQrStatus('loading'); + setWeixinQrError(''); + try { + const startResult = await window.electron.im.weixinQrLoginStart(); + if (!isMountedRef.current) return; + + if (!startResult.success || !startResult.qrDataUrl) { + setWeixinQrStatus('error'); + setWeixinQrError(startResult.message || i18nService.t('imWeixinQrFailed')); + return; + } + + setWeixinQrUrl(startResult.qrDataUrl); + setWeixinQrStatus('showing'); + + // Start polling for scan result + setWeixinQrStatus('waiting'); + const waitResult = await window.electron.im.weixinQrLoginWait(startResult.sessionKey); + if (!isMountedRef.current) return; + + if (waitResult.success && waitResult.connected) { + setWeixinQrStatus('success'); + // Enable weixin and save config + const fullConfig = { ...weixinOpenClawConfig, enabled: true }; + dispatch(setWeixinConfig({ enabled: true })); + dispatch(clearError()); + await imService.updateConfig({ weixin: fullConfig }); + await imService.loadStatus(); + } else { + setWeixinQrStatus('error'); + setWeixinQrError(waitResult.message || i18nService.t('imWeixinQrFailed')); + } + } catch (err) { + if (!isMountedRef.current) return; + setWeixinQrStatus('error'); + setWeixinQrError(String(err)); + } + }; + const handleSaveConfig = async () => { if (!configLoaded) return; @@ -496,6 +570,12 @@ const IMSettings: React.FC = () => { return; } + // For Weixin, save weixin config directly (OpenClaw mode) + if (activePlatform === 'weixin') { + await imService.persistConfig({ weixin: weixinOpenClawConfig }); + return; + } + // For POPO, save popo config directly (OpenClaw mode) if (activePlatform === 'popo') { await imService.persistConfig({ popo: popoConfig }); @@ -626,6 +706,17 @@ const IMSettings: React.FC = () => { return; } + if (platform === 'weixin') { + const newEnabled = !weixinOpenClawConfig.enabled; + const success = await imService.updateConfig({ weixin: { ...weixinOpenClawConfig, enabled: newEnabled } }); + if (success) { + dispatch(setWeixinConfig({ enabled: newEnabled })); + if (newEnabled) dispatch(clearError()); + await imService.loadStatus(); + } + return; + } + if (platform === 'popo') { const newEnabled = !popoConfig.enabled; const success = await imService.updateConfig({ popo: { ...popoConfig, enabled: newEnabled } }); @@ -687,6 +778,7 @@ const IMSettings: React.FC = () => { const xiaomifengConnected = status.xiaomifeng?.connected ?? false; const qqConnected = status.qq?.connected ?? false; const wecomConnected = status.wecom?.connected ?? false; + const weixinConnected = status.weixin?.connected ?? false; const popoConnected = status.popo?.connected ?? false; // Compute visible platforms based on language @@ -725,6 +817,9 @@ const IMSettings: React.FC = () => { if (platform === 'wecom') { return !!(wecomOpenClawConfig.botId && wecomOpenClawConfig.secret); } + if (platform === 'weixin') { + return true; // No credentials needed, connects via QR code in CLI + } if (platform === 'popo') { const effectiveMode = config.popo.connectionMode || (config.popo.token ? 'webhook' : 'websocket'); if (effectiveMode === 'webhook') { @@ -749,6 +844,7 @@ const IMSettings: React.FC = () => { if (platform === 'xiaomifeng') return xiaomifengConnected; if (platform === 'qq') return qqConnected; if (platform === 'wecom') return wecomConnected; + if (platform === 'weixin') return weixinConnected; if (platform === 'popo') return popoConnected; return feishuConnected; }; @@ -811,6 +907,21 @@ const IMSettings: React.FC = () => { return; } + // For Weixin, persist weixin config and test (OpenClaw mode) + if (platform === 'weixin') { + await imService.persistConfig({ weixin: weixinOpenClawConfig }); + const result = await runConnectivityTest(platform, { + weixin: weixinOpenClawConfig, + } as Partial); + if (!weixinOpenClawConfig.enabled && result) { + const authCheck = result.checks.find((c) => c.code === 'auth_check'); + if (authCheck && authCheck.level === 'pass') { + toggleGateway(platform); + } + } + return; + } + // For Feishu, persist feishu config and test (OpenClaw mode) if (platform === 'feishu') { await imService.persistConfig({ feishu: fsOpenClawConfig }); @@ -886,6 +997,7 @@ const IMSettings: React.FC = () => { nim: setNimConfig, xiaomifeng: setXiaomifengConfig, wecom: setWecomConfig, + weixin: setWeixinConfig, popo: setPopoConfig, }; return actionMap[platform]; @@ -2869,6 +2981,299 @@ const IMSettings: React.FC = () => { )} + {/* Weixin (微信) Settings */} + {activePlatform === 'weixin' && ( +
+ {/* Scan QR code section */} +
+ {(weixinQrStatus === 'idle' || weixinQrStatus === 'error') && ( + <> + +

+ {i18nService.t('imWeixinScanHint')} +

+ {weixinQrStatus === 'error' && weixinQrError && ( +
+ + {weixinQrError} +
+ )} + + )} + {weixinQrStatus === 'loading' && ( +
+ + + {i18nService.t('imWeixinQrLoading')} + +
+ )} + {(weixinQrStatus === 'showing' || weixinQrStatus === 'waiting') && weixinQrUrl && ( +
+

+ {i18nService.t('imWeixinQrScanPrompt')} +

+
+
+ +
+
+ {weixinQrStatus === 'waiting' && ( +
+ + + {i18nService.t('imWeixinQrWaiting')} + +
+ )} +
+ )} + {weixinQrStatus === 'success' && ( +
+ + {i18nService.t('imWeixinQrSuccess')} +
+ )} +
+ + {/* Platform Guide */} + + + {/* Advanced Settings (collapsible) */} +
+ + {i18nService.t('imAdvancedSettings')} + +
+ {/* DM Policy */} +
+ + +
+ + {/* Pairing Requests (shown when dmPolicy is 'pairing') */} + {weixinOpenClawConfig.dmPolicy === 'pairing' && renderPairingSection('weixin')} + + {/* Allow From */} +
+ +
+ setWeixinAllowedUserIdInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const id = weixinAllowedUserIdInput.trim(); + if (id && !weixinOpenClawConfig.allowFrom.includes(id)) { + const newIds = [...weixinOpenClawConfig.allowFrom, id]; + handleWeixinOpenClawChange({ allowFrom: newIds }); + setWeixinAllowedUserIdInput(''); + void imService.persistConfig({ weixin: { ...weixinOpenClawConfig, allowFrom: newIds } }); + } + } + }} + className="block flex-1 rounded-lg dark:bg-claude-darkSurface/80 bg-claude-surface/80 dark:border-claude-darkBorder/60 border-claude-border/60 border focus:border-claude-accent focus:ring-1 focus:ring-claude-accent/30 dark:text-claude-darkText text-claude-text px-3 py-2 text-sm transition-colors" + placeholder={i18nService.t('imUserIdPlaceholder') || 'Enter user ID'} + /> + +
+ {weixinOpenClawConfig.allowFrom.length > 0 && ( +
+ {weixinOpenClawConfig.allowFrom.map((id) => ( + + {id} + + + ))} +
+ )} +
+ + {/* Group Policy */} +
+ + +
+ + {/* Group Allow From (shown when groupPolicy is 'allowlist') */} + {weixinOpenClawConfig.groupPolicy === 'allowlist' && ( +
+ +
+ setWeixinGroupAllowIdInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + const id = weixinGroupAllowIdInput.trim(); + if (id && !weixinOpenClawConfig.groupAllowFrom.includes(id)) { + const newIds = [...weixinOpenClawConfig.groupAllowFrom, id]; + handleWeixinOpenClawChange({ groupAllowFrom: newIds }); + setWeixinGroupAllowIdInput(''); + void imService.persistConfig({ weixin: { ...weixinOpenClawConfig, groupAllowFrom: newIds } }); + } + } + }} + className="block flex-1 rounded-lg dark:bg-claude-darkSurface/80 bg-claude-surface/80 dark:border-claude-darkBorder/60 border-claude-border/60 border focus:border-claude-accent focus:ring-1 focus:ring-claude-accent/30 dark:text-claude-darkText text-claude-text px-3 py-2 text-sm transition-colors" + placeholder={i18nService.t('imGroupIdPlaceholder') || 'Enter group ID'} + /> + +
+ {weixinOpenClawConfig.groupAllowFrom.length > 0 && ( +
+ {weixinOpenClawConfig.groupAllowFrom.map((id) => ( + + {id} + + + ))} +
+ )} +
+ )} + + {/* Debug toggle */} +
+ + +
+
+
+ + {/* Connectivity test */} +
+ {renderConnectivityTestButton('weixin')} +
+ + {/* Error display */} + {status.weixin?.lastError && ( +
+ {status.weixin.lastError} +
+ )} +
+ )} + {/* WeCom (企业微信) Settings */} {activePlatform === 'wecom' && (
diff --git a/src/renderer/services/i18n.ts b/src/renderer/services/i18n.ts index d2be982..389d421 100644 --- a/src/renderer/services/i18n.ts +++ b/src/renderer/services/i18n.ts @@ -676,6 +676,7 @@ const translations: Record> = { discord: 'Discord', nim: '云信', xiaomifeng: '小蜜蜂', + weixin: '微信', wecom: '企业微信', popo: 'POPO', connected: '已连接', @@ -832,6 +833,17 @@ const translations: Record> = { imWecomGuideStep1: '在企业微信管理后台创建一个新的机器人应用', imWecomGuideStep2: '获取对应的 BotID 与 Secret 凭证', imWecomGuideStep3: '将 BotID 和 Secret 填入下方表单', + imWeixinGuideStep1: '点击下方按钮启用微信接入', + imWeixinGuideStep2: '使用微信扫描二维码完成连接', + imWeixinGuideStep3: '连接成功后,即可通过微信发送消息与 AI 对话', + imWeixinScanBtn: '扫码连接微信', + imWeixinScanHint: '使用微信扫描二维码,连接微信 IM 机器人', + imWeixinQrLoading: '正在获取二维码...', + imWeixinQrScanPrompt: '请使用微信扫描下方二维码', + imWeixinQrWaiting: '等待扫码确认...', + imWeixinQrSuccess: '微信连接成功!', + imWeixinQrFailed: '连接失败', + imWeixinQrExpired: '二维码已过期,请重新获取', imPopoCredentialHint: '从 POPO 开发者后台获取 AppKey、AppSecret 和 AES Key', imPopoGuideStep1: '在 POPO 开发者后台创建一个新的机器人应用', imPopoGuideStep2: '获取 AppKey、AppSecret 凭证,并配置 AES Key', @@ -1024,6 +1036,7 @@ const translations: Record> = { scheduledTasksFormNotifyNim: '云信', scheduledTasksFormNotifyXiaomifeng: '小蜜蜂', scheduledTasksFormNotifyWecom: '企业微信', + scheduledTasksFormNotifyWeixin: '微信', scheduledTasksFormNotifyPopo: 'POPO', scheduledTasksFormNotifyNone: '无', scheduledTasksFormNotifyNotConfigured: '未配置', @@ -1721,8 +1734,8 @@ const translations: Record> = { discord: 'Discord', nim: 'NetEase IM', xiaomifeng: 'Netease Bee', - wecom: 'WeCom', - popo: 'POPO', + weixin: 'WeChat', + wecom: 'WeCom', popo: 'POPO', connected: 'Connected', disconnected: 'Disconnected', kickedByOtherClient: 'Account logged in elsewhere', @@ -1877,6 +1890,17 @@ const translations: Record> = { imWecomGuideStep1: 'Create a new bot application in the WeCom Admin Console', imWecomGuideStep2: 'Copy the BotID and Secret credentials', imWecomGuideStep3: 'Enter BotID and Secret in the form below', + imWeixinGuideStep1: 'Click the button below to enable WeChat integration', + imWeixinGuideStep2: 'Scan the QR code with WeChat to connect', + imWeixinGuideStep3: 'Once connected, you can chat with AI via WeChat messages', + imWeixinScanBtn: 'Scan to Connect WeChat', + imWeixinScanHint: 'Scan the QR code with WeChat to connect IM bot', + imWeixinQrLoading: 'Getting QR code...', + imWeixinQrScanPrompt: 'Scan the QR code below with WeChat', + imWeixinQrWaiting: 'Waiting for scan confirmation...', + imWeixinQrSuccess: 'WeChat connected successfully!', + imWeixinQrFailed: 'Connection failed', + imWeixinQrExpired: 'QR code expired, please try again', imPopoCredentialHint: 'Get AppKey, AppSecret and AES Key from the POPO Developer Portal', imPopoGuideStep1: 'Create a new bot application in the POPO Developer Portal', imPopoGuideStep2: 'Get AppKey, AppSecret credentials and configure AES Key', @@ -2069,6 +2093,7 @@ const translations: Record> = { scheduledTasksFormNotifyNim: 'NetEase IM', scheduledTasksFormNotifyXiaomifeng: 'Xiaomifeng', scheduledTasksFormNotifyWecom: 'WeCom', + scheduledTasksFormNotifyWeixin: 'WeChat', scheduledTasksFormNotifyPopo: 'POPO', scheduledTasksFormNotifyNone: 'None', scheduledTasksFormNotifyNotConfigured: 'Not configured', diff --git a/src/renderer/store/slices/imSlice.ts b/src/renderer/store/slices/imSlice.ts index c2f495c..a052c3b 100644 --- a/src/renderer/store/slices/imSlice.ts +++ b/src/renderer/store/slices/imSlice.ts @@ -16,6 +16,7 @@ import type { XiaomifengConfig, WecomOpenClawConfig, PopoOpenClawConfig, + WeixinOpenClawConfig, IMSettings, } from '../../types/im'; import { @@ -74,6 +75,9 @@ const imSlice = createSlice({ setPopoConfig: (state, action: PayloadAction>) => { state.config.popo = { ...state.config.popo, ...action.payload }; }, + setWeixinConfig: (state, action: PayloadAction>) => { + state.config.weixin = { ...state.config.weixin, ...action.payload }; + }, setIMSettings: (state, action: PayloadAction>) => { state.config.settings = { ...state.config.settings, ...action.payload }; }, @@ -103,6 +107,7 @@ export const { setXiaomifengConfig, setWecomConfig, setPopoConfig, + setWeixinConfig, setIMSettings, setStatus, setLoading, diff --git a/src/renderer/types/electron.d.ts b/src/renderer/types/electron.d.ts index 17a2bf7..56c923d 100644 --- a/src/renderer/types/electron.d.ts +++ b/src/renderer/types/electron.d.ts @@ -382,15 +382,17 @@ interface IElectronAPI { getConfig: () => Promise<{ success: boolean; config?: IMGatewayConfig; error?: string }>; setConfig: (config: Partial, options?: { syncGateway?: boolean }) => Promise<{ success: boolean; error?: string }>; syncConfig: () => Promise<{ success: boolean; error?: string }>; - startGateway: (platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo') => Promise<{ success: boolean; error?: string }>; - stopGateway: (platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo') => Promise<{ success: boolean; error?: string }>; + startGateway: (platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo' | 'weixin') => Promise<{ success: boolean; error?: string }>; + stopGateway: (platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo' | 'weixin') => Promise<{ success: boolean; error?: string }>; testGateway: ( - platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo', + platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo' | 'weixin', configOverride?: Partial ) => Promise<{ success: boolean; result?: IMConnectivityTestResult; error?: string }>; getStatus: () => Promise<{ success: boolean; status?: IMGatewayStatus; error?: string }>; getLocalIp: () => Promise; getOpenClawConfigSchema: () => Promise<{ success: boolean; result?: { schema: Record; uiHints: Record> }; error?: string }>; + weixinQrLoginStart: () => Promise<{ success: boolean; qrDataUrl?: string; message: string; sessionKey?: string }>; + weixinQrLoginWait: (accountId?: string) => Promise<{ success: boolean; connected: boolean; message: string; accountId?: string }>; listPairingRequests: (platform: string) => Promise<{ success: boolean; requests: Array<{ id: string; code: string; createdAt: string; lastSeenAt: string; meta?: Record }>; @@ -469,6 +471,7 @@ interface IMGatewayConfig { xiaomifeng: XiaomifengConfig; wecom: WecomConfig; popo: PopoOpenClawConfig; + weixin: WeixinOpenClawConfig; settings: IMSettings; } @@ -639,6 +642,15 @@ interface PopoOpenClawConfig { debug: boolean; } +interface WeixinOpenClawConfig { + enabled: boolean; + dmPolicy: 'open' | 'pairing' | 'allowlist' | 'disabled'; + allowFrom: string[]; + groupPolicy: 'open' | 'allowlist' | 'disabled'; + groupAllowFrom: string[]; + debug: boolean; +} + interface IMSettings { systemPrompt?: string; skillsEnabled: boolean; @@ -654,6 +666,7 @@ interface IMGatewayStatus { xiaomifeng: XiaomifengGatewayStatus; wecom: WecomGatewayStatus; popo: PopoGatewayStatus; + weixin: WeixinGatewayStatus; } type IMConnectivityVerdict = 'pass' | 'warn' | 'fail'; @@ -768,8 +781,16 @@ interface PopoGatewayStatus { lastOutboundAt: number | null; } +interface WeixinGatewayStatus { + connected: boolean; + startedAt: number | null; + lastError: string | null; + lastInboundAt: number | null; + lastOutboundAt: number | null; +} + interface IMMessage { - platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo'; + platform: 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo' | 'weixin'; messageId: string; conversationId: string; senderId: string; diff --git a/src/renderer/types/im.ts b/src/renderer/types/im.ts index 1fdcebb..f6fb91f 100644 --- a/src/renderer/types/im.ts +++ b/src/renderer/types/im.ts @@ -278,9 +278,28 @@ export interface PopoGatewayStatus { lastOutboundAt: number | null; } +// ==================== Weixin (微信) Types ==================== + +export interface WeixinOpenClawConfig { + enabled: boolean; + dmPolicy: 'open' | 'pairing' | 'allowlist' | 'disabled'; + allowFrom: string[]; + groupPolicy: 'open' | 'allowlist' | 'disabled'; + groupAllowFrom: string[]; + debug: boolean; +} + +export interface WeixinGatewayStatus { + connected: boolean; + startedAt: number | null; + lastError: string | null; + lastInboundAt: number | null; + lastOutboundAt: number | null; +} + // ==================== Common IM Types ==================== -export type IMPlatform = 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo'; +export type IMPlatform = 'dingtalk' | 'feishu' | 'qq' | 'telegram' | 'discord' | 'nim' | 'xiaomifeng' | 'wecom' | 'popo' | 'weixin'; export interface IMGatewayConfig { dingtalk: DingTalkOpenClawConfig; @@ -292,6 +311,7 @@ export interface IMGatewayConfig { xiaomifeng: XiaomifengConfig; wecom: WecomOpenClawConfig; popo: PopoOpenClawConfig; + weixin: WeixinOpenClawConfig; settings: IMSettings; } @@ -310,6 +330,7 @@ export interface IMGatewayStatus { xiaomifeng: XiaomifengGatewayStatus; wecom: WecomGatewayStatus; popo: PopoGatewayStatus; + weixin: WeixinGatewayStatus; } // ==================== Media Attachment Types ==================== @@ -544,6 +565,15 @@ export const DEFAULT_POPO_CONFIG: PopoOpenClawConfig = { debug: true, }; +export const DEFAULT_WEIXIN_CONFIG: WeixinOpenClawConfig = { + enabled: false, + dmPolicy: 'open', + allowFrom: [], + groupPolicy: 'open', + groupAllowFrom: [], + debug: true, +}; + export const DEFAULT_IM_SETTINGS: IMSettings = { systemPrompt: '', skillsEnabled: true, @@ -559,6 +589,7 @@ export const DEFAULT_IM_CONFIG: IMGatewayConfig = { xiaomifeng: DEFAULT_XIAOMIFENG_CONFIG, wecom: DEFAULT_WECOM_CONFIG, popo: DEFAULT_POPO_CONFIG, + weixin: DEFAULT_WEIXIN_CONFIG, settings: DEFAULT_IM_SETTINGS, }; @@ -633,4 +664,11 @@ export const DEFAULT_IM_STATUS: IMGatewayStatus = { lastInboundAt: null, lastOutboundAt: null, }, + weixin: { + connected: false, + startedAt: null, + lastError: null, + lastInboundAt: null, + lastOutboundAt: null, + }, }; diff --git a/src/renderer/utils/regionFilter.ts b/src/renderer/utils/regionFilter.ts index 9368a65..e18df14 100644 --- a/src/renderer/utils/regionFilter.ts +++ b/src/renderer/utils/regionFilter.ts @@ -1,5 +1,5 @@ // IM 平台分类 -export const CHINA_IM_PLATFORMS = ['dingtalk', 'feishu', 'wecom', 'qq', 'nim', 'xiaomifeng', 'popo'] as const; +export const CHINA_IM_PLATFORMS = ['dingtalk', 'feishu', 'weixin', 'wecom', 'qq', 'nim', 'xiaomifeng', 'popo'] as const; export const GLOBAL_IM_PLATFORMS = ['telegram', 'discord'] as const; /**