feat(channels): add DingTalk via bundled plugin mirror and pure Node deploy (#215)
This commit is contained in:
committed by
GitHub
Unverified
parent
d4f77a442c
commit
d63810f54b
@@ -26,6 +26,9 @@ extraResources:
|
||||
# because electron-builder respects .gitignore which excludes node_modules/)
|
||||
- from: build/openclaw/
|
||||
to: openclaw/
|
||||
# Bundled OpenClaw plugin mirrors (dingtalk, etc.)
|
||||
- from: build/openclaw-plugins/
|
||||
to: openclaw-plugins/
|
||||
|
||||
afterPack: ./scripts/after-pack.cjs
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { GatewayManager, GatewayStatus } from './manager';
|
||||
/**
|
||||
* Channel types supported by OpenClaw
|
||||
*/
|
||||
export type ChannelType = 'whatsapp' | 'telegram' | 'discord' | 'wechat';
|
||||
export type ChannelType = 'whatsapp' | 'dingtalk' | 'telegram' | 'discord' | 'wechat';
|
||||
|
||||
/**
|
||||
* Channel status
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Registers all IPC handlers for main-renderer communication
|
||||
*/
|
||||
import { ipcMain, BrowserWindow, shell, dialog, app, nativeImage } from 'electron';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { existsSync, cpSync, mkdirSync, rmSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join, extname, basename } from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
@@ -608,6 +608,50 @@ function registerGatewayHandlers(
|
||||
* For checking package status and channel configuration
|
||||
*/
|
||||
function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
||||
async function ensureDingTalkPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
const targetDir = join(homedir(), '.openclaw', 'extensions', 'dingtalk');
|
||||
const targetManifest = join(targetDir, 'openclaw.plugin.json');
|
||||
|
||||
if (existsSync(targetManifest)) {
|
||||
logger.info('DingTalk plugin already installed from local mirror');
|
||||
return { installed: true };
|
||||
}
|
||||
|
||||
const candidateSources = app.isPackaged
|
||||
? [join(process.resourcesPath, 'openclaw-plugins', 'dingtalk')]
|
||||
: [
|
||||
join(app.getAppPath(), 'build', 'openclaw-plugins', 'dingtalk'),
|
||||
join(process.cwd(), 'build', 'openclaw-plugins', 'dingtalk'),
|
||||
join(__dirname, '../../build/openclaw-plugins/dingtalk'),
|
||||
];
|
||||
|
||||
const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json')));
|
||||
if (!sourceDir) {
|
||||
return {
|
||||
installed: false,
|
||||
warning: 'Bundled DingTalk plugin mirror not found. Run: pnpm run bundle:openclaw-plugins',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(join(homedir(), '.openclaw', 'extensions'), { recursive: true });
|
||||
rmSync(targetDir, { recursive: true, force: true });
|
||||
cpSync(sourceDir, targetDir, { recursive: true, dereference: true });
|
||||
|
||||
if (!existsSync(targetManifest)) {
|
||||
return { installed: false, warning: 'Failed to install DingTalk plugin mirror (manifest missing).' };
|
||||
}
|
||||
|
||||
logger.info(`Installed DingTalk plugin from bundled mirror: ${sourceDir}`);
|
||||
return { installed: true };
|
||||
} catch (error) {
|
||||
logger.warn('Failed to install DingTalk plugin from bundled mirror:', error);
|
||||
return {
|
||||
installed: false,
|
||||
warning: 'Failed to install bundled DingTalk plugin mirror',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get OpenClaw package status
|
||||
ipcMain.handle('openclaw:status', () => {
|
||||
@@ -666,12 +710,23 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
||||
ipcMain.handle('channel:saveConfig', async (_, channelType: string, config: Record<string, unknown>) => {
|
||||
try {
|
||||
logger.info('channel:saveConfig', { channelType, keys: Object.keys(config || {}) });
|
||||
if (channelType === 'dingtalk') {
|
||||
const installResult = await ensureDingTalkPluginInstalled();
|
||||
if (!installResult.installed) {
|
||||
return {
|
||||
success: false,
|
||||
error: installResult.warning || 'DingTalk plugin install failed',
|
||||
};
|
||||
}
|
||||
saveChannelConfig(channelType, config);
|
||||
return {
|
||||
success: true,
|
||||
pluginInstalled: installResult.installed,
|
||||
warning: installResult.warning,
|
||||
};
|
||||
}
|
||||
await saveChannelConfig(channelType, config);
|
||||
// Debounced restart so the gateway picks up the new channel config.
|
||||
// The gateway watches openclaw.json, but a restart ensures a clean
|
||||
// start for newly-added channels. Using debouncedRestart() here
|
||||
// instead of an explicit restart on the frontend side means that
|
||||
// rapid config changes (e.g. setup wizard) coalesce into one restart.
|
||||
gatewayManager.debouncedRestart();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
|
||||
@@ -86,6 +86,36 @@ export async function saveChannelConfig(
|
||||
): Promise<void> {
|
||||
const currentConfig = await readOpenClawConfig();
|
||||
|
||||
// DingTalk is a channel plugin; make sure it's explicitly allowed.
|
||||
// Newer OpenClaw versions may not load non-bundled plugins when allowlist is empty.
|
||||
if (channelType === 'dingtalk') {
|
||||
if (!currentConfig.plugins) {
|
||||
currentConfig.plugins = {};
|
||||
}
|
||||
currentConfig.plugins.enabled = true;
|
||||
const allow = Array.isArray(currentConfig.plugins.allow)
|
||||
? currentConfig.plugins.allow as string[]
|
||||
: [];
|
||||
if (!allow.includes('dingtalk')) {
|
||||
currentConfig.plugins.allow = [...allow, 'dingtalk'];
|
||||
}
|
||||
}
|
||||
|
||||
// DingTalk is a channel plugin; make sure it's explicitly allowed.
|
||||
// Newer OpenClaw versions may not load non-bundled plugins when allowlist is empty.
|
||||
if (channelType === 'dingtalk') {
|
||||
if (!currentConfig.plugins) {
|
||||
currentConfig.plugins = {};
|
||||
}
|
||||
currentConfig.plugins.enabled = true;
|
||||
const allow = Array.isArray(currentConfig.plugins.allow)
|
||||
? currentConfig.plugins.allow as string[]
|
||||
: [];
|
||||
if (!allow.includes('dingtalk')) {
|
||||
currentConfig.plugins.allow = [...allow, 'dingtalk'];
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin-based channels (e.g. WhatsApp) go under plugins.entries, not channels
|
||||
if (PLUGIN_CHANNELS.includes(channelType)) {
|
||||
if (!currentConfig.plugins) {
|
||||
|
||||
12
package.json
12
package.json
@@ -19,8 +19,9 @@
|
||||
"scripts": {
|
||||
"init": "pnpm install && pnpm run uv:download",
|
||||
"dev": "vite",
|
||||
"build": "vite build && zx scripts/bundle-openclaw.mjs && electron-builder",
|
||||
"build": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder",
|
||||
"build:vite": "vite build",
|
||||
"bundle:openclaw-plugins": "zx scripts/bundle-openclaw-plugins.mjs",
|
||||
"lint": "eslint . --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
@@ -32,10 +33,10 @@
|
||||
"uv:download:all": "zx scripts/download-bundled-uv.mjs --all",
|
||||
"icons": "zx scripts/generate-icons.mjs",
|
||||
"package": "electron-builder",
|
||||
"package:mac": "vite build && zx scripts/bundle-openclaw.mjs && electron-builder --mac",
|
||||
"package:win": "vite build && zx scripts/bundle-openclaw.mjs && electron-builder --win",
|
||||
"package:linux": "vite build && zx scripts/bundle-openclaw.mjs && electron-builder --linux",
|
||||
"release": "pnpm run uv:download && vite build && electron-builder --publish always",
|
||||
"package:mac": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --mac",
|
||||
"package:win": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --win",
|
||||
"package:linux": "vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --linux",
|
||||
"release": "pnpm run uv:download && vite build && zx scripts/bundle-openclaw.mjs && zx scripts/bundle-openclaw-plugins.mjs && electron-builder --publish always",
|
||||
"version:patch": "pnpm version patch",
|
||||
"version:minor": "pnpm version minor",
|
||||
"version:major": "pnpm version major",
|
||||
@@ -62,6 +63,7 @@
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@soimy/dingtalk": "^3.1.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/node": "^25.3.0",
|
||||
|
||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
@@ -63,6 +63,9 @@ importers:
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@soimy/dingtalk':
|
||||
specifier: ^3.1.4
|
||||
version: 3.1.4(openclaw@2026.2.26(@napi-rs/canvas@0.1.93)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.15.1(typescript@5.9.3)))
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.9.1
|
||||
version: 6.9.1
|
||||
@@ -2596,6 +2599,11 @@ packages:
|
||||
resolution: {integrity: sha512-vNZk5y+IsxjwzTAXikvzz5pqMLb35YytC64nVF2MAFVhjpXu9ITOKUriZ0JG/llwzCAi56jb5x0cXDRIyE2A2A==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@soimy/dingtalk@3.1.4':
|
||||
resolution: {integrity: sha512-57MRun9Z8Kt7GhsbL8f04m2QhWOvaE9x5o+eAdj/V3MxlFPLPCT2OMgvMxKHggfOk1rRmHI3h0/778uiXmUHKA==}
|
||||
peerDependencies:
|
||||
openclaw: '>=2026.2.13'
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
@@ -3620,6 +3628,9 @@ packages:
|
||||
resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
dingtalk-stream@2.1.4:
|
||||
resolution: {integrity: sha512-rgQbXLGWfASuB9onFcqXTnRSj4ZotimhBOnzrB4kS19AaU9lshXiuofs1GAYcKh5uzPWCAuEs3tMtiadTQWP4A==}
|
||||
|
||||
dir-compare@4.2.0:
|
||||
resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==}
|
||||
|
||||
@@ -9697,6 +9708,19 @@ snapshots:
|
||||
'@snazzah/davey-win32-ia32-msvc': 0.1.9
|
||||
'@snazzah/davey-win32-x64-msvc': 0.1.9
|
||||
|
||||
'@soimy/dingtalk@3.1.4(openclaw@2026.2.26(@napi-rs/canvas@0.1.93)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.15.1(typescript@5.9.3)))':
|
||||
dependencies:
|
||||
axios: 1.13.5(debug@4.4.3)
|
||||
dingtalk-stream: 2.1.4
|
||||
form-data: 4.0.5
|
||||
openclaw: 2026.2.26(@napi-rs/canvas@0.1.93)(@types/express@5.0.6)(encoding@0.1.13)(hono@4.11.8)(node-llama-cpp@3.15.1(typescript@5.9.3))
|
||||
zod: 4.3.6
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- debug
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@szmarczak/http-timer@4.0.6':
|
||||
@@ -10947,6 +10971,16 @@ snapshots:
|
||||
|
||||
diff@8.0.3: {}
|
||||
|
||||
dingtalk-stream@2.1.4:
|
||||
dependencies:
|
||||
axios: 1.13.5(debug@4.4.3)
|
||||
debug: 4.4.3
|
||||
ws: 8.19.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
dir-compare@4.2.0:
|
||||
dependencies:
|
||||
minimatch: 3.1.2
|
||||
|
||||
@@ -149,6 +149,8 @@ exports.default = async function afterPack(context) {
|
||||
|
||||
const openclawRoot = join(resourcesDir, 'openclaw');
|
||||
const dest = join(openclawRoot, 'node_modules');
|
||||
const pluginsSrcRoot = join(__dirname, '..', 'build', 'openclaw-plugins');
|
||||
const pluginsDestRoot = join(resourcesDir, 'openclaw-plugins');
|
||||
|
||||
if (!existsSync(src)) {
|
||||
console.warn('[after-pack] ⚠️ build/openclaw/node_modules not found. Run bundle-openclaw first.');
|
||||
@@ -164,6 +166,29 @@ exports.default = async function afterPack(context) {
|
||||
cpSync(src, dest, { recursive: true });
|
||||
console.log('[after-pack] ✅ openclaw node_modules copied.');
|
||||
|
||||
// 1.1 Copy plugin node_modules (also skipped due to .gitignore)
|
||||
if (existsSync(pluginsSrcRoot) && existsSync(pluginsDestRoot)) {
|
||||
const pluginDirs = readdirSync(pluginsSrcRoot, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name);
|
||||
|
||||
for (const pluginId of pluginDirs) {
|
||||
const pluginSrcNM = join(pluginsSrcRoot, pluginId, 'node_modules');
|
||||
const pluginDestRoot = join(pluginsDestRoot, pluginId);
|
||||
const pluginDestNM = join(pluginDestRoot, 'node_modules');
|
||||
if (!existsSync(pluginSrcNM) || !existsSync(pluginDestRoot)) continue;
|
||||
|
||||
console.log(`[after-pack] Copying plugin deps for ${pluginId} -> ${pluginDestNM}`);
|
||||
cpSync(pluginSrcNM, pluginDestNM, { recursive: true });
|
||||
|
||||
// Apply the same cleanup strategy for plugin bundles.
|
||||
cleanupUnnecessaryFiles(pluginDestRoot);
|
||||
cleanupKoffi(pluginDestNM, platform, arch);
|
||||
cleanupNativePlatformPackages(pluginDestNM, platform, arch);
|
||||
}
|
||||
console.log('[after-pack] ✅ openclaw plugin node_modules copied.');
|
||||
}
|
||||
|
||||
// 2. General cleanup on the full openclaw directory (not just node_modules)
|
||||
console.log('[after-pack] 🧹 Cleaning up unnecessary files ...');
|
||||
const removedRoot = cleanupUnnecessaryFiles(openclawRoot);
|
||||
|
||||
159
scripts/bundle-openclaw-plugins.mjs
Normal file
159
scripts/bundle-openclaw-plugins.mjs
Normal file
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env zx
|
||||
|
||||
/**
|
||||
* bundle-openclaw-plugins.mjs
|
||||
*
|
||||
* Build a self-contained mirror of OpenClaw third-party plugins for packaging.
|
||||
* Current plugins:
|
||||
* - @soimy/dingtalk -> build/openclaw-plugins/dingtalk
|
||||
*
|
||||
* The output plugin directory contains:
|
||||
* - plugin source files (index.ts, openclaw.plugin.json, package.json, ...)
|
||||
* - plugin runtime node_modules/ (flattened direct + transitive deps)
|
||||
*/
|
||||
|
||||
import 'zx/globals';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const OUTPUT_ROOT = path.join(ROOT, 'build', 'openclaw-plugins');
|
||||
const NODE_MODULES = path.join(ROOT, 'node_modules');
|
||||
|
||||
const PLUGINS = [
|
||||
{ npmName: '@soimy/dingtalk', pluginId: 'dingtalk' },
|
||||
];
|
||||
|
||||
function getVirtualStoreNodeModules(realPkgPath) {
|
||||
let dir = realPkgPath;
|
||||
while (dir !== path.dirname(dir)) {
|
||||
if (path.basename(dir) === 'node_modules') return dir;
|
||||
dir = path.dirname(dir);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function listPackages(nodeModulesDir) {
|
||||
const result = [];
|
||||
if (!fs.existsSync(nodeModulesDir)) return result;
|
||||
|
||||
for (const entry of fs.readdirSync(nodeModulesDir)) {
|
||||
if (entry === '.bin') continue;
|
||||
const entryPath = path.join(nodeModulesDir, entry);
|
||||
const stat = fs.lstatSync(entryPath);
|
||||
|
||||
if (entry.startsWith('@')) {
|
||||
if (!(stat.isDirectory() || stat.isSymbolicLink())) continue;
|
||||
let scopeEntries = [];
|
||||
try {
|
||||
scopeEntries = fs.readdirSync(entryPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const sub of scopeEntries) {
|
||||
result.push({
|
||||
name: `${entry}/${sub}`,
|
||||
fullPath: path.join(entryPath, sub),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
result.push({ name: entry, fullPath: entryPath });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function bundleOnePlugin({ npmName, pluginId }) {
|
||||
const pkgPath = path.join(NODE_MODULES, ...npmName.split('/'));
|
||||
if (!fs.existsSync(pkgPath)) {
|
||||
throw new Error(`Missing dependency "${npmName}". Run pnpm install first.`);
|
||||
}
|
||||
|
||||
const realPluginPath = fs.realpathSync(pkgPath);
|
||||
const outputDir = path.join(OUTPUT_ROOT, pluginId);
|
||||
|
||||
echo`📦 Bundling plugin ${npmName} -> ${outputDir}`;
|
||||
|
||||
if (fs.existsSync(outputDir)) {
|
||||
fs.rmSync(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
// 1) Copy plugin package itself
|
||||
fs.cpSync(realPluginPath, outputDir, { recursive: true, dereference: true });
|
||||
|
||||
// 2) Collect transitive deps from pnpm virtual store
|
||||
const collected = new Map();
|
||||
const queue = [];
|
||||
const rootVirtualNM = getVirtualStoreNodeModules(realPluginPath);
|
||||
if (!rootVirtualNM) {
|
||||
throw new Error(`Cannot resolve virtual store node_modules for ${npmName}`);
|
||||
}
|
||||
queue.push({ nodeModulesDir: rootVirtualNM, skipPkg: npmName });
|
||||
|
||||
const SKIP_PACKAGES = new Set(['typescript', '@playwright/test']);
|
||||
const SKIP_SCOPES = ['@types/'];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { nodeModulesDir, skipPkg } = queue.shift();
|
||||
for (const { name, fullPath } of listPackages(nodeModulesDir)) {
|
||||
if (name === skipPkg) continue;
|
||||
if (SKIP_PACKAGES.has(name) || SKIP_SCOPES.some((s) => name.startsWith(s))) continue;
|
||||
|
||||
let realPath;
|
||||
try {
|
||||
realPath = fs.realpathSync(fullPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (collected.has(realPath)) continue;
|
||||
collected.set(realPath, name);
|
||||
|
||||
const depVirtualNM = getVirtualStoreNodeModules(realPath);
|
||||
if (depVirtualNM && depVirtualNM !== nodeModulesDir) {
|
||||
queue.push({ nodeModulesDir: depVirtualNM, skipPkg: name });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Copy flattened deps into plugin/node_modules
|
||||
const outputNodeModules = path.join(outputDir, 'node_modules');
|
||||
fs.mkdirSync(outputNodeModules, { recursive: true });
|
||||
|
||||
let copiedCount = 0;
|
||||
let skippedDupes = 0;
|
||||
const copiedNames = new Set();
|
||||
|
||||
for (const [realPath, pkgName] of collected) {
|
||||
if (copiedNames.has(pkgName)) {
|
||||
skippedDupes++;
|
||||
continue;
|
||||
}
|
||||
copiedNames.add(pkgName);
|
||||
|
||||
const dest = path.join(outputNodeModules, pkgName);
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.cpSync(realPath, dest, { recursive: true, dereference: true });
|
||||
copiedCount++;
|
||||
} catch (err) {
|
||||
echo` ⚠️ Skipped ${pkgName}: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
const manifestPath = path.join(outputDir, 'openclaw.plugin.json');
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
throw new Error(`Missing openclaw.plugin.json in bundled plugin output: ${pluginId}`);
|
||||
}
|
||||
|
||||
echo` ✅ ${pluginId}: copied ${copiedCount} deps (skipped dupes: ${skippedDupes})`;
|
||||
}
|
||||
|
||||
echo`📦 Bundling OpenClaw plugin mirrors...`;
|
||||
fs.mkdirSync(OUTPUT_ROOT, { recursive: true });
|
||||
|
||||
for (const plugin of PLUGINS) {
|
||||
bundleOnePlugin(plugin);
|
||||
}
|
||||
|
||||
echo`✅ Plugin mirrors ready: ${OUTPUT_ROOT}`;
|
||||
@@ -111,6 +111,38 @@
|
||||
"The system will automatically identify your phone number"
|
||||
]
|
||||
},
|
||||
"dingtalk": {
|
||||
"description": "Connect DingTalk via OpenClaw channel plugin (Stream mode)",
|
||||
"docsUrl": "https://github.com/soimy/openclaw-channel-dingtalk",
|
||||
"fields": {
|
||||
"clientId": {
|
||||
"label": "Client ID (AppKey)",
|
||||
"placeholder": "dingxxxxxx"
|
||||
},
|
||||
"clientSecret": {
|
||||
"label": "Client Secret (AppSecret)",
|
||||
"placeholder": "Your app secret"
|
||||
},
|
||||
"robotCode": {
|
||||
"label": "Robot Code (optional)",
|
||||
"placeholder": "Usually same as Client ID"
|
||||
},
|
||||
"corpId": {
|
||||
"label": "Corp ID (optional)",
|
||||
"placeholder": "dingxxxxxx"
|
||||
},
|
||||
"agentId": {
|
||||
"label": "Agent ID (optional)",
|
||||
"placeholder": "123456789"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Install and enable the dingtalk plugin in OpenClaw",
|
||||
"Create a DingTalk internal app and enable Stream mode",
|
||||
"Fill in Client ID and Client Secret (required)",
|
||||
"Fill in Robot Code / Corp ID / Agent ID if your setup requires them"
|
||||
]
|
||||
},
|
||||
"signal": {
|
||||
"description": "Connect Signal using signal-cli",
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/signal",
|
||||
|
||||
@@ -108,6 +108,38 @@
|
||||
"システムが自動的に電話番号を識別します"
|
||||
]
|
||||
},
|
||||
"dingtalk": {
|
||||
"description": "OpenClaw のチャンネルプラグイン経由で DingTalk に接続します(Stream モード)",
|
||||
"docsUrl": "https://github.com/soimy/openclaw-channel-dingtalk",
|
||||
"fields": {
|
||||
"clientId": {
|
||||
"label": "Client ID (AppKey)",
|
||||
"placeholder": "dingxxxxxx"
|
||||
},
|
||||
"clientSecret": {
|
||||
"label": "Client Secret (AppSecret)",
|
||||
"placeholder": "アプリのシークレット"
|
||||
},
|
||||
"robotCode": {
|
||||
"label": "Robot Code(任意)",
|
||||
"placeholder": "通常は Client ID と同じ"
|
||||
},
|
||||
"corpId": {
|
||||
"label": "Corp ID(任意)",
|
||||
"placeholder": "dingxxxxxx"
|
||||
},
|
||||
"agentId": {
|
||||
"label": "Agent ID(任意)",
|
||||
"placeholder": "123456789"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"まず OpenClaw に dingtalk プラグインをインストールして有効化します",
|
||||
"DingTalk 開発者コンソールで社内アプリを作成し Stream モードを有効にします",
|
||||
"Client ID と Client Secret を入力します(必須)",
|
||||
"必要に応じて Robot Code / Corp ID / Agent ID を入力します"
|
||||
]
|
||||
},
|
||||
"signal": {
|
||||
"description": "signal-cli を使用して Signal に接続します",
|
||||
"fields": {
|
||||
|
||||
@@ -111,6 +111,38 @@
|
||||
"系统将自动识别您的手机号"
|
||||
]
|
||||
},
|
||||
"dingtalk": {
|
||||
"description": "通过 OpenClaw 渠道插件连接钉钉(Stream 模式)",
|
||||
"docsUrl": "https://github.com/soimy/openclaw-channel-dingtalk",
|
||||
"fields": {
|
||||
"clientId": {
|
||||
"label": "Client ID (AppKey)",
|
||||
"placeholder": "dingxxxxxx"
|
||||
},
|
||||
"clientSecret": {
|
||||
"label": "Client Secret (AppSecret)",
|
||||
"placeholder": "您的应用密钥"
|
||||
},
|
||||
"robotCode": {
|
||||
"label": "Robot Code(可选)",
|
||||
"placeholder": "通常与 Client ID 相同"
|
||||
},
|
||||
"corpId": {
|
||||
"label": "Corp ID(可选)",
|
||||
"placeholder": "dingxxxxxx"
|
||||
},
|
||||
"agentId": {
|
||||
"label": "Agent ID(可选)",
|
||||
"placeholder": "123456789"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"先在 OpenClaw 安装并启用 dingtalk 插件",
|
||||
"在钉钉开发者后台创建企业内部应用并开启 Stream 模式",
|
||||
"填写 Client ID 和 Client Secret(必填)",
|
||||
"根据你的应用配置按需填写 Robot Code / Corp ID / Agent ID"
|
||||
]
|
||||
},
|
||||
"signal": {
|
||||
"description": "使用 signal-cli 连接 Signal",
|
||||
"docsUrl": "https://docs.openclaw.ai/zh-CN/channels/signal",
|
||||
|
||||
@@ -570,7 +570,17 @@ function AddChannelDialog({ selectedType, onSelectType, onClose, onChannelAdded
|
||||
|
||||
// Step 2: Save channel configuration via IPC
|
||||
const config: Record<string, unknown> = { ...configValues };
|
||||
await window.electron.ipcRenderer.invoke('channel:saveConfig', selectedType, config);
|
||||
const saveResult = await window.electron.ipcRenderer.invoke('channel:saveConfig', selectedType, config) as {
|
||||
success?: boolean;
|
||||
warning?: string;
|
||||
pluginInstalled?: boolean;
|
||||
};
|
||||
if (!saveResult?.success) {
|
||||
throw new Error('Failed to save channel config');
|
||||
}
|
||||
if (typeof saveResult.warning === 'string' && saveResult.warning) {
|
||||
toast.warning(saveResult.warning);
|
||||
}
|
||||
|
||||
// Step 3: Add a local channel entry for the UI
|
||||
await addChannel({
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
export type ChannelType =
|
||||
| 'whatsapp'
|
||||
| 'dingtalk'
|
||||
| 'telegram'
|
||||
| 'discord'
|
||||
| 'signal'
|
||||
@@ -78,6 +79,7 @@ export interface ChannelMeta {
|
||||
*/
|
||||
export const CHANNEL_ICONS: Record<ChannelType, string> = {
|
||||
whatsapp: '📱',
|
||||
dingtalk: '💬',
|
||||
telegram: '✈️',
|
||||
discord: '🎮',
|
||||
signal: '🔒',
|
||||
@@ -95,6 +97,7 @@ export const CHANNEL_ICONS: Record<ChannelType, string> = {
|
||||
*/
|
||||
export const CHANNEL_NAMES: Record<ChannelType, string> = {
|
||||
whatsapp: 'WhatsApp',
|
||||
dingtalk: 'DingTalk',
|
||||
telegram: 'Telegram',
|
||||
discord: 'Discord',
|
||||
signal: 'Signal',
|
||||
@@ -111,6 +114,58 @@ export const CHANNEL_NAMES: Record<ChannelType, string> = {
|
||||
* Channel metadata with configuration information
|
||||
*/
|
||||
export const CHANNEL_META: Record<ChannelType, ChannelMeta> = {
|
||||
dingtalk: {
|
||||
id: 'dingtalk',
|
||||
name: 'DingTalk',
|
||||
icon: '💬',
|
||||
description: 'channels:meta.dingtalk.description',
|
||||
connectionType: 'token',
|
||||
docsUrl: 'channels:meta.dingtalk.docsUrl',
|
||||
configFields: [
|
||||
{
|
||||
key: 'clientId',
|
||||
label: 'channels:meta.dingtalk.fields.clientId.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.dingtalk.fields.clientId.placeholder',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'channels:meta.dingtalk.fields.clientSecret.label',
|
||||
type: 'password',
|
||||
placeholder: 'channels:meta.dingtalk.fields.clientSecret.placeholder',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'robotCode',
|
||||
label: 'channels:meta.dingtalk.fields.robotCode.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.dingtalk.fields.robotCode.placeholder',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: 'corpId',
|
||||
label: 'channels:meta.dingtalk.fields.corpId.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.dingtalk.fields.corpId.placeholder',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: 'agentId',
|
||||
label: 'channels:meta.dingtalk.fields.agentId.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.dingtalk.fields.agentId.placeholder',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
instructions: [
|
||||
'channels:meta.dingtalk.instructions.0',
|
||||
'channels:meta.dingtalk.instructions.1',
|
||||
'channels:meta.dingtalk.instructions.2',
|
||||
'channels:meta.dingtalk.instructions.3',
|
||||
],
|
||||
isPlugin: true,
|
||||
},
|
||||
telegram: {
|
||||
id: 'telegram',
|
||||
name: 'Telegram',
|
||||
@@ -441,7 +496,7 @@ export const CHANNEL_META: Record<ChannelType, ChannelMeta> = {
|
||||
* Get primary supported channels (non-plugin, commonly used)
|
||||
*/
|
||||
export function getPrimaryChannels(): ChannelType[] {
|
||||
return ['telegram', 'discord', 'whatsapp', 'feishu'];
|
||||
return ['telegram', 'discord', 'whatsapp', 'dingtalk', 'feishu'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user