mirror of
https://gh-proxy.org/https://github.com/klinsmaya/LobsterAI.git
synced 2026-11-08 01:06:44 +08:00
feat(im): 新增微信 IM 接入,支持扫码绑定微信机器人
- 新增 weixin 平台类型定义、默认配置与 Redux 状态管理 - 集成 openclaw-weixin 插件,通过 OpenClaw Gateway RPC 实现二维码扫码登录 - 新增微信 IM 设置面板(二维码扫码、DM/群策略、连通性测试) - 同步微信配置至 OpenClaw channels.openclaw-weixin - 安装后自动补丁 openclaw-weixin 插件的 gatewayMethods 声明 - 新增中英文 i18n 翻译,微信在 IM 列表中位于企微上方
This commit is contained in:
@@ -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",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IMGatewayConfig>
|
||||
): Promise<IMConnectivityTestResult> {
|
||||
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<IMGatewayConfig>
|
||||
): Promise<IMConnectivityTestResult> {
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+20
-1
@@ -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<QQConfig>('qq') ?? DEFAULT_QQ_CONFIG;
|
||||
const wecom = this.getConfigValue<WecomOpenClawConfig>('wecomOpenClaw') ?? DEFAULT_WECOM_CONFIG;
|
||||
const popo = this.getConfigValue<PopoOpenClawConfig>('popo') ?? DEFAULT_POPO_CONFIG;
|
||||
const weixin = this.getConfigValue<WeixinOpenClawConfig>('weixin') ?? DEFAULT_WEIXIN_CONFIG;
|
||||
const settings = this.getConfigValue<IMSettings>('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<WeixinOpenClawConfig>('weixin');
|
||||
return { ...DEFAULT_WEIXIN_CONFIG, ...stored };
|
||||
}
|
||||
|
||||
setWeixinConfig(config: Partial<WeixinOpenClawConfig>): void {
|
||||
const current = this.getWeixinConfig();
|
||||
this.setConfigValue('weixin', { ...current, ...config });
|
||||
}
|
||||
|
||||
// ==================== IM Settings ====================
|
||||
|
||||
getIMSettings(): IMSettings {
|
||||
|
||||
+41
-1
@@ -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 ====================
|
||||
|
||||
@@ -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<string, unknown> = {
|
||||
@@ -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<string, unknown> || {}), 'nim': nimChannel };
|
||||
}
|
||||
|
||||
// Sync Weixin OpenClaw channel config (via openclaw-weixin-cli plugin)
|
||||
if (weixinConfig?.enabled) {
|
||||
const weixinChannel: Record<string, unknown> = {
|
||||
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<string, unknown> || {}), 'openclaw-weixin': weixinChannel };
|
||||
}
|
||||
|
||||
const nextContent = `${JSON.stringify(managedConfig, null, 2)}\n`;
|
||||
let currentContent = '';
|
||||
try {
|
||||
|
||||
@@ -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();
|
||||
|
||||
+7
-3
@@ -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),
|
||||
|
||||
@@ -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<IMPlatform, string> = {
|
||||
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<Record<IMPlatform, string>> = {
|
||||
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<string>('');
|
||||
// Weixin QR login state
|
||||
const [weixinQrStatus, setWeixinQrStatus] = useState<'idle' | 'loading' | 'showing' | 'waiting' | 'success' | 'error'>('idle');
|
||||
const [weixinQrUrl, setWeixinQrUrl] = useState<string>('');
|
||||
const [weixinQrError, setWeixinQrError] = useState<string>('');
|
||||
const [, setWeixinQrSessionKey] = useState<string>('');
|
||||
const [localIp, setLocalIp] = useState<string>('');
|
||||
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<WeixinOpenClawConfig>) => {
|
||||
dispatch(setWeixinConfig(update));
|
||||
};
|
||||
const handleSaveWeixinOpenClawConfig = async (override?: Partial<WeixinOpenClawConfig>) => {
|
||||
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<PopoOpenClawConfig>) => {
|
||||
@@ -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<IMGatewayConfig>);
|
||||
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 = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Weixin (微信) Settings */}
|
||||
{activePlatform === 'weixin' && (
|
||||
<div className="space-y-3">
|
||||
{/* Scan QR code section */}
|
||||
<div className="rounded-lg border border-dashed dark:border-claude-darkBorder/60 border-claude-border/60 p-4 text-center space-y-3">
|
||||
{(weixinQrStatus === 'idle' || weixinQrStatus === 'error') && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleWeixinQrLogin()}
|
||||
className="px-4 py-2.5 rounded-lg text-sm font-medium bg-claude-accent text-white hover:bg-claude-accent/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{i18nService.t('imWeixinScanBtn')}
|
||||
</button>
|
||||
<p className="text-xs text-claude-textSecondary dark:text-claude-darkTextSecondary">
|
||||
{i18nService.t('imWeixinScanHint')}
|
||||
</p>
|
||||
{weixinQrStatus === 'error' && weixinQrError && (
|
||||
<div className="flex items-center justify-center gap-1.5 text-xs text-red-500 bg-red-500/10 px-3 py-2 rounded-lg">
|
||||
<XCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||
{weixinQrError}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{weixinQrStatus === 'loading' && (
|
||||
<div className="flex items-center justify-center gap-2 py-4">
|
||||
<ArrowPathIcon className="h-5 w-5 animate-spin text-claude-accent" />
|
||||
<span className="text-sm text-claude-textSecondary dark:text-claude-darkTextSecondary">
|
||||
{i18nService.t('imWeixinQrLoading')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{(weixinQrStatus === 'showing' || weixinQrStatus === 'waiting') && weixinQrUrl && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-medium dark:text-claude-darkText text-claude-text">
|
||||
{i18nService.t('imWeixinQrScanPrompt')}
|
||||
</p>
|
||||
<div className="flex justify-center">
|
||||
<div className="p-3 bg-white rounded-lg border dark:border-claude-darkBorder/40 border-claude-border/40">
|
||||
<QRCodeSVG value={weixinQrUrl} size={192} />
|
||||
</div>
|
||||
</div>
|
||||
{weixinQrStatus === 'waiting' && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<ArrowPathIcon className="h-4 w-4 animate-spin text-claude-accent" />
|
||||
<span className="text-xs text-claude-textSecondary dark:text-claude-darkTextSecondary">
|
||||
{i18nService.t('imWeixinQrWaiting')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{weixinQrStatus === 'success' && (
|
||||
<div className="flex items-center justify-center gap-1.5 text-xs text-green-600 dark:text-green-400 bg-green-500/10 px-3 py-2 rounded-lg">
|
||||
<CheckCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||
{i18nService.t('imWeixinQrSuccess')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Platform Guide */}
|
||||
<PlatformGuide
|
||||
steps={[
|
||||
i18nService.t('imWeixinGuideStep1'),
|
||||
i18nService.t('imWeixinGuideStep2'),
|
||||
i18nService.t('imWeixinGuideStep3'),
|
||||
]}
|
||||
guideUrl={IM_GUIDE_URLS.weixin}
|
||||
/>
|
||||
|
||||
{/* Advanced Settings (collapsible) */}
|
||||
<details className="group">
|
||||
<summary className="cursor-pointer text-xs font-medium dark:text-claude-darkTextSecondary text-claude-textSecondary hover:text-claude-accent transition-colors">
|
||||
{i18nService.t('imAdvancedSettings')}
|
||||
</summary>
|
||||
<div className="mt-2 space-y-3 pl-2 border-l-2 border-claude-border/30 dark:border-claude-darkBorder/30">
|
||||
{/* DM Policy */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium dark:text-claude-darkTextSecondary text-claude-textSecondary">
|
||||
DM Policy
|
||||
</label>
|
||||
<select
|
||||
value={weixinOpenClawConfig.dmPolicy}
|
||||
onChange={(e) => {
|
||||
const update = { dmPolicy: e.target.value as WeixinOpenClawConfig['dmPolicy'] };
|
||||
handleWeixinOpenClawChange(update);
|
||||
void handleSaveWeixinOpenClawConfig(update);
|
||||
}}
|
||||
className="block w-full 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"
|
||||
>
|
||||
<option value="open">{i18nService.t('imDmPolicyOpen')}</option>
|
||||
<option value="pairing">{i18nService.t('imDmPolicyPairing')}</option>
|
||||
<option value="allowlist">{i18nService.t('imDmPolicyAllowlist')}</option>
|
||||
<option value="disabled">{i18nService.t('imDmPolicyDisabled')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Pairing Requests (shown when dmPolicy is 'pairing') */}
|
||||
{weixinOpenClawConfig.dmPolicy === 'pairing' && renderPairingSection('weixin')}
|
||||
|
||||
{/* Allow From */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium dark:text-claude-darkTextSecondary text-claude-textSecondary">
|
||||
Allow From (User IDs)
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={weixinAllowedUserIdInput}
|
||||
onChange={(e) => 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'}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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="px-3 py-2 rounded-lg text-xs font-medium bg-claude-accent/10 text-claude-accent hover:bg-claude-accent/20 transition-colors"
|
||||
>
|
||||
{i18nService.t('add') || '添加'}
|
||||
</button>
|
||||
</div>
|
||||
{weixinOpenClawConfig.allowFrom.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||||
{weixinOpenClawConfig.allowFrom.map((id) => (
|
||||
<span
|
||||
key={id}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs dark:bg-claude-darkSurface/80 bg-claude-surface/80 dark:border-claude-darkBorder/60 border-claude-border/60 border dark:text-claude-darkText text-claude-text"
|
||||
>
|
||||
{id}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newIds = weixinOpenClawConfig.allowFrom.filter((uid) => uid !== id);
|
||||
handleWeixinOpenClawChange({ allowFrom: newIds });
|
||||
void imService.persistConfig({ weixin: { ...weixinOpenClawConfig, allowFrom: newIds } });
|
||||
}}
|
||||
className="text-claude-textSecondary dark:text-claude-darkTextSecondary hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
<XMarkIcon className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Group Policy */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium dark:text-claude-darkTextSecondary text-claude-textSecondary">
|
||||
Group Policy
|
||||
</label>
|
||||
<select
|
||||
value={weixinOpenClawConfig.groupPolicy}
|
||||
onChange={(e) => {
|
||||
const update = { groupPolicy: e.target.value as WeixinOpenClawConfig['groupPolicy'] };
|
||||
handleWeixinOpenClawChange(update);
|
||||
void handleSaveWeixinOpenClawConfig(update);
|
||||
}}
|
||||
className="block w-full 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"
|
||||
>
|
||||
<option value="open">Open</option>
|
||||
<option value="allowlist">Allowlist</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Group Allow From (shown when groupPolicy is 'allowlist') */}
|
||||
{weixinOpenClawConfig.groupPolicy === 'allowlist' && (
|
||||
<div className="space-y-1.5">
|
||||
<label className="block text-xs font-medium dark:text-claude-darkTextSecondary text-claude-textSecondary">
|
||||
Group Allow From (Group IDs)
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={weixinGroupAllowIdInput}
|
||||
onChange={(e) => 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'}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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="px-3 py-2 rounded-lg text-xs font-medium bg-claude-accent/10 text-claude-accent hover:bg-claude-accent/20 transition-colors"
|
||||
>
|
||||
{i18nService.t('add') || '添加'}
|
||||
</button>
|
||||
</div>
|
||||
{weixinOpenClawConfig.groupAllowFrom.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||||
{weixinOpenClawConfig.groupAllowFrom.map((id) => (
|
||||
<span
|
||||
key={id}
|
||||
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-xs dark:bg-claude-darkSurface/80 bg-claude-surface/80 dark:border-claude-darkBorder/60 border-claude-border/60 border dark:text-claude-darkText text-claude-text"
|
||||
>
|
||||
{id}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newIds = weixinOpenClawConfig.groupAllowFrom.filter((gid) => gid !== id);
|
||||
handleWeixinOpenClawChange({ groupAllowFrom: newIds });
|
||||
void imService.persistConfig({ weixin: { ...weixinOpenClawConfig, groupAllowFrom: newIds } });
|
||||
}}
|
||||
className="text-claude-textSecondary dark:text-claude-darkTextSecondary hover:text-red-500 dark:hover:text-red-400 transition-colors"
|
||||
>
|
||||
<XMarkIcon className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Debug toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs font-medium dark:text-claude-darkTextSecondary text-claude-textSecondary">
|
||||
Debug
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const update = { debug: !weixinOpenClawConfig.debug };
|
||||
handleWeixinOpenClawChange(update);
|
||||
void handleSaveWeixinOpenClawConfig(update);
|
||||
}}
|
||||
className={`relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out ${
|
||||
weixinOpenClawConfig.debug ? 'bg-claude-accent' : 'dark:bg-claude-darkSurface bg-claude-surface'
|
||||
}`}
|
||||
>
|
||||
<span className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||
weixinOpenClawConfig.debug ? 'translate-x-4' : 'translate-x-0'
|
||||
}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{/* Connectivity test */}
|
||||
<div className="pt-1">
|
||||
{renderConnectivityTestButton('weixin')}
|
||||
</div>
|
||||
|
||||
{/* Error display */}
|
||||
{status.weixin?.lastError && (
|
||||
<div className="text-xs text-red-500 bg-red-500/10 px-3 py-2 rounded-lg">
|
||||
{status.weixin.lastError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* WeCom (企业微信) Settings */}
|
||||
{activePlatform === 'wecom' && (
|
||||
<div className="space-y-3">
|
||||
|
||||
@@ -676,6 +676,7 @@ const translations: Record<LanguageType, Record<string, string>> = {
|
||||
discord: 'Discord',
|
||||
nim: '云信',
|
||||
xiaomifeng: '小蜜蜂',
|
||||
weixin: '微信',
|
||||
wecom: '企业微信',
|
||||
popo: 'POPO',
|
||||
connected: '已连接',
|
||||
@@ -832,6 +833,17 @@ const translations: Record<LanguageType, Record<string, string>> = {
|
||||
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<LanguageType, Record<string, string>> = {
|
||||
scheduledTasksFormNotifyNim: '云信',
|
||||
scheduledTasksFormNotifyXiaomifeng: '小蜜蜂',
|
||||
scheduledTasksFormNotifyWecom: '企业微信',
|
||||
scheduledTasksFormNotifyWeixin: '微信',
|
||||
scheduledTasksFormNotifyPopo: 'POPO',
|
||||
scheduledTasksFormNotifyNone: '无',
|
||||
scheduledTasksFormNotifyNotConfigured: '未配置',
|
||||
@@ -1721,8 +1734,8 @@ const translations: Record<LanguageType, Record<string, string>> = {
|
||||
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<LanguageType, Record<string, string>> = {
|
||||
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<LanguageType, Record<string, string>> = {
|
||||
scheduledTasksFormNotifyNim: 'NetEase IM',
|
||||
scheduledTasksFormNotifyXiaomifeng: 'Xiaomifeng',
|
||||
scheduledTasksFormNotifyWecom: 'WeCom',
|
||||
scheduledTasksFormNotifyWeixin: 'WeChat',
|
||||
scheduledTasksFormNotifyPopo: 'POPO',
|
||||
scheduledTasksFormNotifyNone: 'None',
|
||||
scheduledTasksFormNotifyNotConfigured: 'Not configured',
|
||||
|
||||
@@ -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<Partial<PopoOpenClawConfig>>) => {
|
||||
state.config.popo = { ...state.config.popo, ...action.payload };
|
||||
},
|
||||
setWeixinConfig: (state, action: PayloadAction<Partial<WeixinOpenClawConfig>>) => {
|
||||
state.config.weixin = { ...state.config.weixin, ...action.payload };
|
||||
},
|
||||
setIMSettings: (state, action: PayloadAction<Partial<IMSettings>>) => {
|
||||
state.config.settings = { ...state.config.settings, ...action.payload };
|
||||
},
|
||||
@@ -103,6 +107,7 @@ export const {
|
||||
setXiaomifengConfig,
|
||||
setWecomConfig,
|
||||
setPopoConfig,
|
||||
setWeixinConfig,
|
||||
setIMSettings,
|
||||
setStatus,
|
||||
setLoading,
|
||||
|
||||
Vendored
+25
-4
@@ -382,15 +382,17 @@ interface IElectronAPI {
|
||||
getConfig: () => Promise<{ success: boolean; config?: IMGatewayConfig; error?: string }>;
|
||||
setConfig: (config: Partial<IMGatewayConfig>, 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<IMGatewayConfig>
|
||||
) => Promise<{ success: boolean; result?: IMConnectivityTestResult; error?: string }>;
|
||||
getStatus: () => Promise<{ success: boolean; status?: IMGatewayStatus; error?: string }>;
|
||||
getLocalIp: () => Promise<string>;
|
||||
getOpenClawConfigSchema: () => Promise<{ success: boolean; result?: { schema: Record<string, unknown>; uiHints: Record<string, Record<string, unknown>> }; 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<string, string> }>;
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user