From fc692b99d6ffe0fc6384a17be0f6720ee827ff2e Mon Sep 17 00:00:00 2001 From: liuzhq Date: Sun, 22 Mar 2026 16:24:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(im):=20=E6=96=B0=E5=A2=9E=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=20IM=20=E6=8E=A5=E5=85=A5=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=89=AB=E7=A0=81=E7=BB=91=E5=AE=9A=E5=BE=AE=E4=BF=A1=E6=9C=BA?= =?UTF-8?q?=E5=99=A8=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 weixin 平台类型定义、默认配置与 Redux 状态管理 - 集成 openclaw-weixin 插件,通过 OpenClaw Gateway RPC 实现二维码扫码登录 - 新增微信 IM 设置面板(二维码扫码、DM/群策略、连通性测试) - 同步微信配置至 OpenClaw channels.openclaw-weixin - 安装后自动补丁 openclaw-weixin 插件的 gatewayMethods 声明 - 新增中英文 i18n 翻译,微信在 IM 列表中位于企微上方 --- package.json | 5 + public/weixin.png | Bin 0 -> 48877 bytes scripts/ensure-openclaw-plugins.cjs | 23 ++ src/main/im/imGatewayManager.ts | 163 ++++++++- src/main/im/imStore.ts | 21 +- src/main/im/types.ts | 42 ++- src/main/libs/openclawConfigSync.ts | 30 +- src/main/main.ts | 26 ++ src/main/preload.ts | 10 +- src/renderer/components/im/IMSettings.tsx | 409 +++++++++++++++++++++- src/renderer/services/i18n.ts | 29 +- src/renderer/store/slices/imSlice.ts | 5 + src/renderer/types/electron.d.ts | 29 +- src/renderer/types/im.ts | 40 ++- src/renderer/utils/regionFilter.ts | 2 +- 15 files changed, 815 insertions(+), 19 deletions(-) create mode 100644 public/weixin.png 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 0000000000000000000000000000000000000000..7b6a56a8703a65c87603375e9f89f5822188e628 GIT binary patch literal 48877 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 @-*cB)~HXta|qsC)!+H?6qH)E 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 KY25{n`2Pd!?e) + ): 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; /**