mirror of
https://gh-proxy.org/https://github.com/klinsmaya/LobsterAI.git
synced 2026-11-08 01:06:44 +08:00
feat(im): add one-click Feishu bot creation via QR code device flow
This commit is contained in:
@@ -101,7 +101,10 @@
|
||||
"@electron/remote": "^2.0.12",
|
||||
"@headlessui/react": "^1.7.18",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"@larksuite/openclaw-lark": "^2026.3.17",
|
||||
"@larksuite/openclaw-lark-tools": "~1.0.26",
|
||||
"@larksuiteoapi/node-sdk": "^1.58.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||
"@reduxjs/toolkit": "^2.2.1",
|
||||
"@types/uuid": "^10.0.0",
|
||||
|
||||
@@ -62,6 +62,7 @@ const EXCLUDED_DIRS = new Set([
|
||||
'examples',
|
||||
'coverage',
|
||||
'.venv',
|
||||
'.bin', // node_modules/.bin contains symlinks that break tar on cross-platform builds
|
||||
]);
|
||||
|
||||
const EXCLUDED_ENVFILE = /^\.env(\..+)?$/i;
|
||||
|
||||
@@ -26,6 +26,10 @@ const translations: Record<LanguageType, Record<string, string>> = {
|
||||
|
||||
// Timeout hint
|
||||
taskTimedOut: '[任务超时] 任务因超过最大允许时长而被自动停止。你可以继续对话以从中断处继续。',
|
||||
|
||||
// Feishu bot install
|
||||
feishuVerifyCredentialsFailed: '凭证验证失败,请检查 App ID 和 App Secret 是否正确',
|
||||
feishuVerifyFailed: '验证失败',
|
||||
},
|
||||
en: {
|
||||
// Tray menu
|
||||
@@ -39,6 +43,10 @@ const translations: Record<LanguageType, Record<string, string>> = {
|
||||
|
||||
// Timeout hint
|
||||
taskTimedOut: '[Task timed out] The task was automatically stopped because it exceeded the maximum allowed duration. You can continue the conversation to pick up where it left off.',
|
||||
|
||||
// Feishu bot install
|
||||
feishuVerifyCredentialsFailed: 'Credential validation failed. Please check your App ID and App Secret.',
|
||||
feishuVerifyFailed: 'Verification failed',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import * as path from 'path';
|
||||
import { t } from '../i18n';
|
||||
import { NimGateway } from './nimGateway';
|
||||
import { XiaomifengGateway } from './xiaomifengGateway';
|
||||
import { IMChatHandler } from './imChatHandler';
|
||||
@@ -2113,6 +2114,90 @@ export class IMGatewayManager extends EventEmitter {
|
||||
return status.discord.lastError;
|
||||
}
|
||||
|
||||
// ==================== Feishu Bot Install Helpers ====================
|
||||
|
||||
/** Lazy-load and cache the feishu-auth module (avoid repeated dynamic import overhead). */
|
||||
private _feishuAuthModule: any = null;
|
||||
private async getFeishuAuthModule() {
|
||||
if (!this._feishuAuthModule) {
|
||||
this._feishuAuthModule = await import('@larksuite/openclaw-lark-tools/dist/utils/feishu-auth.js');
|
||||
}
|
||||
return this._feishuAuthModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Feishu Device Flow onboarding: init + begin.
|
||||
* Returns data needed to render a QR code in the UI.
|
||||
* Also caches isLark so that pollFeishuInstall uses the correct domain.
|
||||
*/
|
||||
private _feishuInstallIsLark = false;
|
||||
async startFeishuInstallQrcode(isLark: boolean): Promise<{
|
||||
url: string;
|
||||
deviceCode: string;
|
||||
interval: number;
|
||||
expireIn: number;
|
||||
}> {
|
||||
const { FeishuAuth } = await this.getFeishuAuthModule();
|
||||
this._feishuInstallIsLark = isLark;
|
||||
const auth = new FeishuAuth();
|
||||
auth.setDomain(isLark);
|
||||
await auth.init();
|
||||
const resp = await auth.begin();
|
||||
return {
|
||||
url: resp.verification_uri_complete,
|
||||
deviceCode: resp.device_code,
|
||||
interval: resp.interval ?? 5,
|
||||
expireIn: resp.expire_in ?? 300,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll Feishu Device Flow for the result of a QR code scan.
|
||||
* Uses the domain set during startFeishuInstallQrcode to ensure consistency.
|
||||
*/
|
||||
async pollFeishuInstall(deviceCode: string): Promise<{
|
||||
done: boolean;
|
||||
appId?: string;
|
||||
appSecret?: string;
|
||||
domain?: string;
|
||||
error?: string;
|
||||
}> {
|
||||
const { FeishuAuth } = await this.getFeishuAuthModule();
|
||||
const auth = new FeishuAuth();
|
||||
auth.setDomain(this._feishuInstallIsLark);
|
||||
const resp = await auth.poll(deviceCode);
|
||||
if (resp.error) {
|
||||
if (resp.error === 'authorization_pending' || resp.error === 'slow_down') {
|
||||
return { done: false };
|
||||
}
|
||||
return { done: false, error: resp.error_description || resp.error };
|
||||
}
|
||||
if (resp.client_id && resp.client_secret) {
|
||||
const domain = resp.user_info?.tenant_brand === 'lark' ? 'lark' : 'feishu';
|
||||
return { done: true, appId: resp.client_id, appSecret: resp.client_secret, domain };
|
||||
}
|
||||
return { done: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate existing Feishu app credentials (App ID + App Secret).
|
||||
*/
|
||||
async verifyFeishuCredentials(appId: string, appSecret: string): Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
const { validateAppCredentials } = await this.getFeishuAuthModule();
|
||||
try {
|
||||
const valid = await validateAppCredentials(appId, appSecret);
|
||||
if (valid) {
|
||||
return { success: true };
|
||||
}
|
||||
return { success: false, error: t('feishuVerifyCredentialsFailed') };
|
||||
} catch (err: any) {
|
||||
return { success: false, error: err?.message || t('feishuVerifyFailed') };
|
||||
}
|
||||
}
|
||||
|
||||
private calculateVerdict(checks: IMConnectivityCheck[]): IMConnectivityVerdict {
|
||||
if (checks.some((check) => check.level === 'fail')) {
|
||||
return 'fail';
|
||||
|
||||
@@ -2993,6 +2993,31 @@ if (!gotTheLock) {
|
||||
}
|
||||
});
|
||||
|
||||
// Feishu bot install helpers
|
||||
ipcMain.handle('feishu:install:qrcode', async (_event, { isLark }: { isLark: boolean }) => {
|
||||
try {
|
||||
return await getIMGatewayManager().startFeishuInstallQrcode(isLark);
|
||||
} catch (error) {
|
||||
throw new Error(error instanceof Error ? error.message : '获取二维码失败');
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('feishu:install:poll', async (_event, { deviceCode }: { deviceCode: string }) => {
|
||||
try {
|
||||
return await getIMGatewayManager().pollFeishuInstall(deviceCode);
|
||||
} catch (error) {
|
||||
return { done: false, error: error instanceof Error ? error.message : '轮询失败' };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('feishu:install:verify', async (_event, { appId, appSecret }: { appId: string; appSecret: string }) => {
|
||||
try {
|
||||
return await getIMGatewayManager().verifyFeishuCredentials(appId, appSecret);
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : '验证失败' };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('generate-session-title', async (_event, userInput: string | null) => {
|
||||
return generateSessionTitle(userInput);
|
||||
});
|
||||
|
||||
@@ -360,4 +360,28 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
networkStatus: {
|
||||
send: (status: 'online' | 'offline') => ipcRenderer.send('network:status-change', status),
|
||||
},
|
||||
feishu: {
|
||||
install: {
|
||||
qrcode: (isLark: boolean) =>
|
||||
ipcRenderer.invoke('feishu:install:qrcode', { isLark }) as Promise<{
|
||||
url: string;
|
||||
deviceCode: string;
|
||||
interval: number;
|
||||
expireIn: number;
|
||||
}>,
|
||||
poll: (deviceCode: string) =>
|
||||
ipcRenderer.invoke('feishu:install:poll', { deviceCode }) as Promise<{
|
||||
done: boolean;
|
||||
appId?: string;
|
||||
appSecret?: string;
|
||||
domain?: string;
|
||||
error?: string;
|
||||
}>,
|
||||
verify: (appId: string, appSecret: string) =>
|
||||
ipcRenderer.invoke('feishu:install:verify', { appId, appSecret }) as Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2940,7 +2940,7 @@ const Settings: React.FC<SettingsProps> = ({ onClose, initialTab, notice, onUpda
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 modal-backdrop flex items-center justify-center"
|
||||
onClick={onClose}
|
||||
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||
>
|
||||
<div
|
||||
className="relative flex w-[900px] h-[80vh] rounded-2xl dark:border-claude-darkBorder border-claude-border border shadow-modal overflow-hidden modal-content"
|
||||
|
||||
@@ -14,6 +14,8 @@ import { i18nService } from '../../services/i18n';
|
||||
import type { IMPlatform, IMConnectivityCheck, IMConnectivityTestResult, IMGatewayConfig, TelegramOpenClawConfig, DiscordOpenClawConfig, FeishuOpenClawConfig, DingTalkOpenClawConfig, QQOpenClawConfig, WecomOpenClawConfig, PopoOpenClawConfig } from '../../types/im';
|
||||
import { getVisibleIMPlatforms } from '../../utils/regionFilter';
|
||||
import WecomAIBotSDK from '@wecom/wecom-aibot-sdk';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { ArrowPathIcon } from '@heroicons/react/24/outline';
|
||||
import { SchemaForm } from './SchemaForm';
|
||||
import type { UiHint } from './SchemaForm';
|
||||
|
||||
@@ -158,6 +160,82 @@ const IMSettings: React.FC = () => {
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Cleanup feishu QR timers on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (feishuQrPollTimerRef.current) clearInterval(feishuQrPollTimerRef.current);
|
||||
if (feishuQrCountdownTimerRef.current) clearInterval(feishuQrCountdownTimerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Reset feishu QR state when switching away from feishu
|
||||
useEffect(() => {
|
||||
if (activePlatform !== 'feishu') {
|
||||
if (feishuQrPollTimerRef.current) { clearInterval(feishuQrPollTimerRef.current); feishuQrPollTimerRef.current = null; }
|
||||
if (feishuQrCountdownTimerRef.current) { clearInterval(feishuQrCountdownTimerRef.current); feishuQrCountdownTimerRef.current = null; }
|
||||
setFeishuQrStatus('idle');
|
||||
setFeishuQrUrl('');
|
||||
setFeishuQrError('');
|
||||
}
|
||||
}, [activePlatform]);
|
||||
|
||||
const handleFeishuStartQr = async () => {
|
||||
if (feishuQrPollTimerRef.current) clearInterval(feishuQrPollTimerRef.current);
|
||||
if (feishuQrCountdownTimerRef.current) clearInterval(feishuQrCountdownTimerRef.current);
|
||||
setFeishuQrStatus('loading');
|
||||
setFeishuQrError('');
|
||||
try {
|
||||
const result = await window.electron.feishu.install.qrcode(false);
|
||||
if (!isMountedRef.current) return;
|
||||
setFeishuQrUrl(result.url);
|
||||
feishuQrDeviceCodeRef.current = result.deviceCode;
|
||||
const expireIn = result.expireIn ?? 300;
|
||||
setFeishuQrTimeLeft(expireIn);
|
||||
setFeishuQrStatus('showing');
|
||||
|
||||
// Countdown
|
||||
feishuQrCountdownTimerRef.current = setInterval(() => {
|
||||
setFeishuQrTimeLeft((prev) => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(feishuQrCountdownTimerRef.current!);
|
||||
feishuQrCountdownTimerRef.current = null;
|
||||
if (feishuQrPollTimerRef.current) { clearInterval(feishuQrPollTimerRef.current); feishuQrPollTimerRef.current = null; }
|
||||
setFeishuQrStatus('error');
|
||||
setFeishuQrError(i18nService.t('feishuBotCreateWizardQrcodeExpired'));
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// Poll
|
||||
const intervalMs = Math.max(result.interval ?? 5, 3) * 1000;
|
||||
feishuQrPollTimerRef.current = setInterval(async () => {
|
||||
try {
|
||||
const pollResult = await window.electron.feishu.install.poll(feishuQrDeviceCodeRef.current);
|
||||
if (!isMountedRef.current) return;
|
||||
if (pollResult.done && pollResult.appId && pollResult.appSecret) {
|
||||
clearInterval(feishuQrPollTimerRef.current!); feishuQrPollTimerRef.current = null;
|
||||
clearInterval(feishuQrCountdownTimerRef.current!); feishuQrCountdownTimerRef.current = null;
|
||||
dispatch(setFeishuConfig({ appId: pollResult.appId, appSecret: pollResult.appSecret, enabled: true }));
|
||||
await imService.updateConfig({ feishu: { ...config.feishu, appId: pollResult.appId, appSecret: pollResult.appSecret, enabled: true } });
|
||||
if (!isMountedRef.current) return; // re-check after async updateConfig
|
||||
setFeishuQrStatus('success');
|
||||
} else if (pollResult.error && pollResult.error !== 'authorization_pending' && pollResult.error !== 'slow_down') {
|
||||
clearInterval(feishuQrPollTimerRef.current!); feishuQrPollTimerRef.current = null;
|
||||
clearInterval(feishuQrCountdownTimerRef.current!); feishuQrCountdownTimerRef.current = null;
|
||||
setFeishuQrStatus('error');
|
||||
setFeishuQrError(pollResult.error);
|
||||
}
|
||||
} catch { /* keep retrying */ }
|
||||
}, intervalMs);
|
||||
} catch (err: any) {
|
||||
if (!isMountedRef.current) return;
|
||||
setFeishuQrStatus('error');
|
||||
setFeishuQrError(err?.message || '获取二维码失败');
|
||||
}
|
||||
};
|
||||
|
||||
// Reset wecom quick setup state when switching away from wecom
|
||||
useEffect(() => {
|
||||
if (activePlatform !== 'wecom') {
|
||||
@@ -245,6 +323,15 @@ const IMSettings: React.FC = () => {
|
||||
// State for Feishu allow-from inputs
|
||||
const [feishuAllowedUserIdInput, setFeishuAllowedUserIdInput] = useState('');
|
||||
const [feishuGroupAllowIdInput, setFeishuGroupAllowIdInput] = useState('');
|
||||
// Inline QR code state for feishu bot creation (mirroring WeCom quick-setup pattern)
|
||||
const [feishuQrStatus, setFeishuQrStatus] = useState<'idle' | 'loading' | 'showing' | 'success' | 'error'>('idle');
|
||||
const [feishuQrUrl, setFeishuQrUrl] = useState<string>('');
|
||||
const [feishuQrTimeLeft, setFeishuQrTimeLeft] = useState<number>(0);
|
||||
const [feishuQrError, setFeishuQrError] = useState<string>('');
|
||||
// These don't need to be state — they don't affect rendering directly
|
||||
const feishuQrDeviceCodeRef = useRef<string>('');
|
||||
const feishuQrPollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const feishuQrCountdownTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// Pairing state for OpenClaw platforms
|
||||
const [pairingCodeInput, setPairingCodeInput] = useState<Record<string, string>>({});
|
||||
@@ -1288,6 +1375,65 @@ const IMSettings: React.FC = () => {
|
||||
{/* Feishu Settings */}
|
||||
{activePlatform === 'feishu' && (
|
||||
<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">
|
||||
{(feishuQrStatus === 'idle' || feishuQrStatus === 'error') && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleFeishuStartQr()}
|
||||
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('feishuBotCreateWizardScanBtn')}
|
||||
</button>
|
||||
<p className="text-xs text-claude-textSecondary dark:text-claude-darkTextSecondary">
|
||||
{i18nService.t('feishuBotCreateWizardScanHint')}
|
||||
</p>
|
||||
{feishuQrStatus === 'error' && feishuQrError && (
|
||||
<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" />
|
||||
{feishuQrError}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{feishuQrStatus === 'loading' && (
|
||||
<div className="flex flex-col items-center gap-2 py-2">
|
||||
<ArrowPathIcon className="h-7 w-7 text-claude-accent animate-spin" />
|
||||
<span className="text-xs text-claude-textSecondary dark:text-claude-darkTextSecondary">正在生成二维码…</span>
|
||||
</div>
|
||||
)}
|
||||
{feishuQrStatus === 'showing' && feishuQrUrl && (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="p-2 bg-white rounded-lg inline-block">
|
||||
<QRCodeSVG value={feishuQrUrl} size={160} />
|
||||
</div>
|
||||
<p className="text-xs text-claude-textSecondary dark:text-claude-darkTextSecondary max-w-[240px]">
|
||||
{i18nService.t('feishuBotCreateWizardQrcodeDesc')}
|
||||
</p>
|
||||
<p className="text-xs text-claude-textSecondary dark:text-claude-darkTextSecondary">
|
||||
{feishuQrTimeLeft}s
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{feishuQrStatus === '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('feishuBotCreateWizardSuccessTitle')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative flex items-center">
|
||||
<div className="flex-1 border-t dark:border-claude-darkBorder/40 border-claude-border/40" />
|
||||
<span className="px-3 text-xs text-claude-textSecondary dark:text-claude-darkTextSecondary whitespace-nowrap">
|
||||
{i18nService.t('feishuBotCreateWizardOrManual')}
|
||||
</span>
|
||||
<div className="flex-1 border-t dark:border-claude-darkBorder/40 border-claude-border/40" />
|
||||
</div>
|
||||
|
||||
{/* Manual guide */}
|
||||
<PlatformGuide
|
||||
steps={[
|
||||
i18nService.t('imFeishuGuideStep1'),
|
||||
@@ -1626,6 +1772,7 @@ const IMSettings: React.FC = () => {
|
||||
{status.feishu.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -737,6 +737,23 @@ const translations: Record<LanguageType, Record<string, string>> = {
|
||||
imDingtalkGuideStep4: '保存后机器人将自动通过 Stream 模式建立长连接',
|
||||
imFeishuGuideStep1: '在下方填写飞书机器人的 App ID 和 App Secret 即可完成配置',
|
||||
imFeishuGuideStep2: '可在飞书开放平台查看应用凭证,详情参考配置手册。',
|
||||
feishuBotCreateWizardTitle: '创建机器人',
|
||||
feishuBotCreateWizardScanBtn: '扫码创建机器人',
|
||||
feishuBotCreateWizardScanHint: '使用飞书客户端扫描二维码,一键创建并配置机器人',
|
||||
feishuBotCreateWizardOrManual: '或 手动填写、修改已有机器人信息',
|
||||
feishuBotCreateWizardOptionNew: '新建机器人',
|
||||
feishuBotCreateWizardOptionExisting: '关联已有机器人',
|
||||
feishuBotCreateWizardQrcodeDesc: '使用飞书客户端扫描二维码,选择「一键创建飞书机器人」完成配置。',
|
||||
feishuBotCreateWizardQrcodeExpired: '二维码已过期,请点击刷新',
|
||||
feishuBotCreateWizardQrcodeRefresh: '刷新二维码',
|
||||
feishuBotCreateWizardVerify: '验证',
|
||||
feishuBotCreateWizardVerifying: '验证中…',
|
||||
feishuBotCreateWizardSuccessTitle: '机器人配置成功',
|
||||
feishuBotCreateWizardSuccessDesc: '飞书机器人已成功配置,可以开始使用了。',
|
||||
feishuBotCreateWizardErrorRetry: '重试',
|
||||
feishuBotCreateWizardDone: '完成',
|
||||
feishuBotCreateWizardAppIdPlaceholder: '输入 App ID',
|
||||
feishuBotCreateWizardAppSecretPlaceholder: '输入 App Secret',
|
||||
imWecomGuideStep1: '在企业微信管理后台创建一个新的机器人应用',
|
||||
imWecomGuideStep2: '获取对应的 BotID 与 Secret 凭证',
|
||||
imWecomGuideStep3: '将 BotID 和 Secret 填入下方表单',
|
||||
@@ -1680,6 +1697,23 @@ const translations: Record<LanguageType, Record<string, string>> = {
|
||||
imDingtalkGuideStep4: 'Once saved, the bot will auto-connect via Stream mode',
|
||||
imFeishuGuideStep1: 'Enter the Feishu bot App ID and App Secret below to complete setup',
|
||||
imFeishuGuideStep2: 'App credentials are available on the Feishu Open Platform. See the setup manual for details.',
|
||||
feishuBotCreateWizardTitle: 'Create Bot',
|
||||
feishuBotCreateWizardScanBtn: 'Scan QR Code to Create Bot',
|
||||
feishuBotCreateWizardScanHint: 'Scan the QR code with your Feishu app to create and configure a bot in one step',
|
||||
feishuBotCreateWizardOrManual: 'or manually enter / edit bot credentials',
|
||||
feishuBotCreateWizardOptionNew: 'Create a new bot',
|
||||
feishuBotCreateWizardOptionExisting: 'Use an existing bot',
|
||||
feishuBotCreateWizardQrcodeDesc: 'Scan the QR code with your Feishu app and select "One-click create Feishu bot" to complete setup.',
|
||||
feishuBotCreateWizardQrcodeExpired: 'QR code expired, please refresh',
|
||||
feishuBotCreateWizardQrcodeRefresh: 'Refresh QR Code',
|
||||
feishuBotCreateWizardVerify: 'Verify',
|
||||
feishuBotCreateWizardVerifying: 'Verifying...',
|
||||
feishuBotCreateWizardSuccessTitle: 'Bot configured successfully',
|
||||
feishuBotCreateWizardSuccessDesc: 'Your Feishu bot has been configured and is ready to use.',
|
||||
feishuBotCreateWizardErrorRetry: 'Retry',
|
||||
feishuBotCreateWizardDone: 'Done',
|
||||
feishuBotCreateWizardAppIdPlaceholder: 'Enter App ID',
|
||||
feishuBotCreateWizardAppSecretPlaceholder: 'Enter App Secret',
|
||||
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',
|
||||
|
||||
Vendored
+21
@@ -433,6 +433,27 @@ interface IElectronAPI {
|
||||
networkStatus: {
|
||||
send: (status: 'online' | 'offline') => void;
|
||||
};
|
||||
feishu: {
|
||||
install: {
|
||||
qrcode: (isLark: boolean) => Promise<{
|
||||
url: string;
|
||||
deviceCode: string;
|
||||
interval: number;
|
||||
expireIn: number;
|
||||
}>;
|
||||
poll: (deviceCode: string) => Promise<{
|
||||
done: boolean;
|
||||
appId?: string;
|
||||
appSecret?: string;
|
||||
domain?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
verify: (appId: string, appSecret: string) => Promise<{
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// IM Gateway types
|
||||
|
||||
+7
-2
@@ -26,7 +26,12 @@ export default defineConfig({
|
||||
outDir: 'dist-electron',
|
||||
minify: false,
|
||||
rollupOptions: {
|
||||
external: ['sql.js', 'discord.js', 'zlib-sync', '@discordjs/opus', 'bufferutil', 'utf-8-validate', 'node-nim', 'nim-web-sdk-ng'],
|
||||
external: (id) => {
|
||||
const staticExternals = ['sql.js', 'discord.js', 'zlib-sync', '@discordjs/opus', 'bufferutil', 'utf-8-validate', 'node-nim', 'nim-web-sdk-ng'];
|
||||
if (staticExternals.includes(id)) return true;
|
||||
if (id.startsWith('@larksuite/openclaw-lark-tools') || id.startsWith('@larksuite/openclaw-lark')) return true;
|
||||
return false;
|
||||
},
|
||||
output: {
|
||||
// Keep CJS format (default), but load via ESM loader.mjs
|
||||
inlineDynamicImports: true,
|
||||
@@ -78,7 +83,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['electron'],
|
||||
exclude: ['electron', '@larksuite/openclaw-lark-tools', '@larksuite/openclaw-lark'],
|
||||
esbuildOptions: {
|
||||
define: {
|
||||
__VERSION__: JSON.stringify(katexVersion),
|
||||
|
||||
Reference in New Issue
Block a user