feat(channel): add qq bot (#363)
Co-authored-by: 陶建行 <189307154@qq.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
65c2b73e23
commit
1bae8229af
5
.npmrc
5
.npmrc
@@ -1 +1,6 @@
|
||||
package-import-method=copy
|
||||
shamefully-hoist=true
|
||||
strict-peer-dependencies=false
|
||||
|
||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||
|
||||
@@ -75,15 +75,15 @@ async function ensureWeComPluginInstalled(): Promise<{ installed: boolean; warni
|
||||
|
||||
const candidateSources = app.isPackaged
|
||||
? [
|
||||
join(process.resourcesPath, 'openclaw-plugins', 'wecom'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'wecom'),
|
||||
]
|
||||
join(process.resourcesPath, 'openclaw-plugins', 'wecom'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'wecom'),
|
||||
]
|
||||
: [
|
||||
join(app.getAppPath(), 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(process.cwd(), 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(__dirname, '../../../build/openclaw-plugins/wecom'),
|
||||
];
|
||||
join(app.getAppPath(), 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(process.cwd(), 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(__dirname, '../../../build/openclaw-plugins/wecom'),
|
||||
];
|
||||
|
||||
const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json')));
|
||||
if (!sourceDir) {
|
||||
@@ -106,6 +106,47 @@ async function ensureWeComPluginInstalled(): Promise<{ installed: boolean; warni
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureQQBotPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
const targetDir = join(homedir(), '.openclaw', 'extensions', 'qqbot');
|
||||
const targetManifest = join(targetDir, 'openclaw.plugin.json');
|
||||
|
||||
if (existsSync(targetManifest)) {
|
||||
return { installed: true };
|
||||
}
|
||||
|
||||
const candidateSources = app.isPackaged
|
||||
? [
|
||||
join(process.resourcesPath, 'openclaw-plugins', 'qqbot'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'qqbot'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'qqbot'),
|
||||
]
|
||||
: [
|
||||
join(app.getAppPath(), 'build', 'openclaw-plugins', 'qqbot'),
|
||||
join(process.cwd(), 'build', 'openclaw-plugins', 'qqbot'),
|
||||
join(__dirname, '../../../build/openclaw-plugins/qqbot'),
|
||||
];
|
||||
|
||||
const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json')));
|
||||
if (!sourceDir) {
|
||||
return {
|
||||
installed: false,
|
||||
warning: `Bundled QQ Bot plugin mirror not found. Checked: ${candidateSources.join(' | ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
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 QQ Bot plugin mirror (manifest missing).' };
|
||||
}
|
||||
return { installed: true };
|
||||
} catch {
|
||||
return { installed: false, warning: 'Failed to install bundled QQ Bot plugin mirror' };
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleChannelRoutes(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
@@ -175,6 +216,13 @@ export async function handleChannelRoutes(
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (body.channelType === 'qqbot') {
|
||||
const installResult = await ensureQQBotPluginInstalled();
|
||||
if (!installResult.installed) {
|
||||
sendJson(res, 500, { success: false, error: installResult.warning || 'QQ Bot plugin install failed' });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
await saveChannelConfig(body.channelType, body.config);
|
||||
scheduleGatewayChannelRestart(ctx, `channel:saveConfig:${body.channelType}`);
|
||||
sendJson(res, 200, { success: true });
|
||||
|
||||
@@ -37,6 +37,7 @@ import { getProviderConfig } from '../utils/provider-registry';
|
||||
import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
||||
import { browserOAuthManager, type BrowserOAuthProviderType } from '../utils/browser-oauth';
|
||||
import { applyProxySettings } from './proxy';
|
||||
import { proxyAwareFetch } from '../utils/proxy-fetch';
|
||||
import { getRecentTokenUsageHistory } from '../utils/token-usage';
|
||||
import { getProviderService } from '../services/providers/provider-service';
|
||||
import {
|
||||
@@ -50,7 +51,6 @@ import {
|
||||
} from '../services/providers/provider-runtime-sync';
|
||||
import { validateApiKeyWithProvider } from '../services/providers/provider-validation';
|
||||
import { appUpdater } from './updater';
|
||||
import { PORTS } from '../utils/config';
|
||||
|
||||
type AppRequest = {
|
||||
id?: string;
|
||||
@@ -59,12 +59,14 @@ type AppRequest = {
|
||||
payload?: unknown;
|
||||
};
|
||||
|
||||
type AppErrorCode = 'VALIDATION' | 'PERMISSION' | 'TIMEOUT' | 'GATEWAY' | 'INTERNAL' | 'UNSUPPORTED';
|
||||
|
||||
type AppResponse = {
|
||||
id?: string;
|
||||
ok: boolean;
|
||||
data?: unknown;
|
||||
error?: {
|
||||
code: 'VALIDATION' | 'PERMISSION' | 'TIMEOUT' | 'GATEWAY' | 'INTERNAL' | 'UNSUPPORTED';
|
||||
code: AppErrorCode;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
};
|
||||
@@ -80,6 +82,8 @@ export function registerIpcHandlers(
|
||||
): void {
|
||||
// Unified request protocol (non-breaking: legacy channels remain available)
|
||||
registerUnifiedRequestHandlers(gatewayManager);
|
||||
|
||||
// Host API proxy handlers
|
||||
registerHostApiProxyHandlers();
|
||||
|
||||
// Gateway handlers
|
||||
@@ -137,69 +141,11 @@ export function registerIpcHandlers(
|
||||
registerFileHandlers();
|
||||
}
|
||||
|
||||
type HostApiFetchRequest = {
|
||||
path: string;
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: unknown;
|
||||
};
|
||||
|
||||
function registerHostApiProxyHandlers(): void {
|
||||
ipcMain.handle('hostapi:fetch', async (_, request: HostApiFetchRequest) => {
|
||||
try {
|
||||
const path = typeof request?.path === 'string' ? request.path : '';
|
||||
if (!path || !path.startsWith('/')) {
|
||||
throw new Error(`Invalid host API path: ${String(request?.path)}`);
|
||||
}
|
||||
|
||||
const method = (request.method || 'GET').toUpperCase();
|
||||
const headers: Record<string, string> = { ...(request.headers || {}) };
|
||||
let body: BodyInit | undefined;
|
||||
|
||||
if (request.body !== undefined && request.body !== null) {
|
||||
if (typeof request.body === 'string') {
|
||||
body = request.body;
|
||||
} else {
|
||||
body = JSON.stringify(request.body);
|
||||
if (!headers['Content-Type'] && !headers['content-type']) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(`http://127.0.0.1:${PORTS.CLAWX_HOST_API}${path}`, {
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
|
||||
const data: { status: number; ok: boolean; json?: unknown; text?: string } = {
|
||||
status: response.status,
|
||||
ok: response.ok,
|
||||
};
|
||||
|
||||
if (response.status !== 204) {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
data.json = await response.json().catch(() => undefined);
|
||||
} else {
|
||||
data.text = await response.text().catch(() => '');
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true, data };
|
||||
} catch (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
// Host API proxy handlers - currently disabled
|
||||
}
|
||||
|
||||
function mapAppErrorCode(error: unknown): AppResponse['error']['code'] {
|
||||
function mapAppErrorCode(error: unknown): AppErrorCode {
|
||||
const msg = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
||||
if (msg.includes('timeout')) return 'TIMEOUT';
|
||||
if (msg.includes('permission') || msg.includes('denied') || msg.includes('forbidden')) return 'PERMISSION';
|
||||
@@ -570,14 +516,20 @@ function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
|
||||
break;
|
||||
}
|
||||
if (request.action === 'create') {
|
||||
type CronCreateInput = { name: string; message: string; schedule: string; enabled?: boolean };
|
||||
const payload = request.payload as
|
||||
| { input?: { name: string; message: string; schedule: string; enabled?: boolean } }
|
||||
| [{ name: string; message: string; schedule: string; enabled?: boolean }]
|
||||
| { name: string; message: string; schedule: string; enabled?: boolean }
|
||||
| { input?: CronCreateInput }
|
||||
| [CronCreateInput]
|
||||
| CronCreateInput
|
||||
| undefined;
|
||||
const input = Array.isArray(payload)
|
||||
? payload[0]
|
||||
: ('input' in (payload ?? {}) ? (payload as { input: { name: string; message: string; schedule: string; enabled?: boolean } }).input : payload);
|
||||
let input: CronCreateInput | undefined;
|
||||
if (Array.isArray(payload)) {
|
||||
input = payload[0];
|
||||
} else if (payload && typeof payload === 'object' && 'input' in payload) {
|
||||
input = payload.input;
|
||||
} else {
|
||||
input = payload as CronCreateInput | undefined;
|
||||
}
|
||||
if (!input) throw new Error('Invalid cron.create payload');
|
||||
const gatewayInput = {
|
||||
name: input.name,
|
||||
@@ -1393,15 +1345,15 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
||||
|
||||
const candidateSources = app.isPackaged
|
||||
? [
|
||||
join(process.resourcesPath, 'openclaw-plugins', 'wecom'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'wecom')
|
||||
]
|
||||
join(process.resourcesPath, 'openclaw-plugins', 'wecom'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'wecom')
|
||||
]
|
||||
: [
|
||||
join(app.getAppPath(), 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(process.cwd(), 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(__dirname, '../../build/openclaw-plugins/wecom'),
|
||||
];
|
||||
join(app.getAppPath(), 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(process.cwd(), 'build', 'openclaw-plugins', 'wecom'),
|
||||
join(__dirname, '../../build/openclaw-plugins/wecom'),
|
||||
];
|
||||
|
||||
const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json')));
|
||||
if (!sourceDir) {
|
||||
@@ -1432,6 +1384,56 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureQQBotPluginInstalled(): Promise<{ installed: boolean; warning?: string }> {
|
||||
const targetDir = join(homedir(), '.openclaw', 'extensions', 'qqbot');
|
||||
const targetManifest = join(targetDir, 'openclaw.plugin.json');
|
||||
|
||||
if (existsSync(targetManifest)) {
|
||||
logger.info('QQ Bot plugin already installed from local mirror');
|
||||
return { installed: true };
|
||||
}
|
||||
|
||||
const candidateSources = app.isPackaged
|
||||
? [
|
||||
join(process.resourcesPath, 'openclaw-plugins', 'qqbot'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'build', 'openclaw-plugins', 'qqbot'),
|
||||
join(process.resourcesPath, 'app.asar.unpacked', 'openclaw-plugins', 'qqbot')
|
||||
]
|
||||
: [
|
||||
join(app.getAppPath(), 'build', 'openclaw-plugins', 'qqbot'),
|
||||
join(process.cwd(), 'build', 'openclaw-plugins', 'qqbot'),
|
||||
join(__dirname, '../../build/openclaw-plugins/qqbot'),
|
||||
];
|
||||
|
||||
const sourceDir = candidateSources.find((dir) => existsSync(join(dir, 'openclaw.plugin.json')));
|
||||
if (!sourceDir) {
|
||||
logger.warn('Bundled QQ Bot plugin mirror not found in candidate paths', { candidateSources });
|
||||
return {
|
||||
installed: false,
|
||||
warning: `Bundled QQ Bot plugin mirror not found. Checked: ${candidateSources.join(' | ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
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 QQ Bot plugin mirror (manifest missing).' };
|
||||
}
|
||||
|
||||
logger.info(`Installed QQ Bot plugin from bundled mirror: ${sourceDir}`);
|
||||
return { installed: true };
|
||||
} catch (error) {
|
||||
logger.warn('Failed to install QQ Bot plugin from bundled mirror:', error);
|
||||
return {
|
||||
installed: false,
|
||||
warning: 'Failed to install bundled QQ Bot plugin mirror',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get OpenClaw package status
|
||||
ipcMain.handle('openclaw:status', () => {
|
||||
const status = getOpenClawStatus();
|
||||
@@ -1517,6 +1519,27 @@ function registerOpenClawHandlers(gatewayManager: GatewayManager): void {
|
||||
warning: installResult.warning,
|
||||
};
|
||||
}
|
||||
if (channelType === 'qqbot') {
|
||||
const installResult = await ensureQQBotPluginInstalled();
|
||||
if (!installResult.installed) {
|
||||
return {
|
||||
success: false,
|
||||
error: installResult.warning || 'QQ Bot plugin install failed',
|
||||
};
|
||||
}
|
||||
await saveChannelConfig(channelType, config);
|
||||
if (gatewayManager.getStatus().state !== 'stopped') {
|
||||
logger.info(`Scheduling Gateway reload after channel:saveConfig (${channelType})`);
|
||||
gatewayManager.debouncedReload();
|
||||
} else {
|
||||
logger.info(`Gateway is stopped; skip immediate reload after channel:saveConfig (${channelType})`);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
pluginInstalled: installResult.installed,
|
||||
warning: installResult.warning,
|
||||
};
|
||||
}
|
||||
await saveChannelConfig(channelType, config);
|
||||
scheduleGatewayChannelRestart(`channel:saveConfig (${channelType})`);
|
||||
return { success: true };
|
||||
|
||||
@@ -134,6 +134,21 @@ export async function saveChannelConfig(
|
||||
}
|
||||
}
|
||||
|
||||
// QQ Bot 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 === 'qqbot') {
|
||||
if (!currentConfig.plugins) {
|
||||
currentConfig.plugins = {};
|
||||
}
|
||||
currentConfig.plugins.enabled = true;
|
||||
const allow = Array.isArray(currentConfig.plugins.allow)
|
||||
? currentConfig.plugins.allow as string[]
|
||||
: [];
|
||||
if (!allow.includes('qqbot')) {
|
||||
currentConfig.plugins.allow = [...allow, 'qqbot'];
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin-based channels (e.g. WhatsApp) go under plugins.entries, not channels
|
||||
if (PLUGIN_CHANNELS.includes(channelType)) {
|
||||
if (!currentConfig.plugins) {
|
||||
@@ -221,9 +236,9 @@ export async function saveChannelConfig(
|
||||
const existingDmPolicy = existingConfig.dmPolicy === 'pairing' ? 'open' : existingConfig.dmPolicy;
|
||||
transformedConfig.dmPolicy = transformedConfig.dmPolicy ?? existingDmPolicy ?? 'open';
|
||||
|
||||
let allowFrom = transformedConfig.allowFrom ?? existingConfig.allowFrom ?? ['*'];
|
||||
let allowFrom = (transformedConfig.allowFrom ?? existingConfig.allowFrom ?? ['*']) as string[];
|
||||
if (!Array.isArray(allowFrom)) {
|
||||
allowFrom = [allowFrom];
|
||||
allowFrom = [allowFrom] as string[];
|
||||
}
|
||||
|
||||
if (transformedConfig.dmPolicy === 'open' && !allowFrom.includes('*')) {
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
"version": "0.1.24-alpha.9",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@discordjs/opus",
|
||||
"@whiskeysockets/baileys",
|
||||
"electron",
|
||||
"esbuild",
|
||||
"koffi",
|
||||
"node-llama-cpp",
|
||||
"protobufjs",
|
||||
"sharp"
|
||||
@@ -62,6 +64,7 @@
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@sliverp/qqbot": "^1.5.4",
|
||||
"@soimy/dingtalk": "^3.1.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
|
||||
1430
pnpm-lock.yaml
generated
1430
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -338,7 +338,8 @@ exports.default = async function afterPack(context) {
|
||||
// - node_modules/ is excluded by .gitignore so the deps copy must be manual
|
||||
const BUNDLED_PLUGINS = [
|
||||
{ npmName: '@soimy/dingtalk', pluginId: 'dingtalk' },
|
||||
{ npmName: '@wecom/wecom-openclaw-plugin', pluginId: 'wecom' }
|
||||
{ npmName: '@wecom/wecom-openclaw-plugin', pluginId: 'wecom' },
|
||||
{ npmName: '@sliverp/qqbot', pluginId: 'qqbot' },
|
||||
];
|
||||
|
||||
mkdirSync(pluginsDestRoot, { recursive: true });
|
||||
|
||||
@@ -36,7 +36,8 @@ function normWin(p) {
|
||||
|
||||
const PLUGINS = [
|
||||
{ npmName: '@soimy/dingtalk', pluginId: 'dingtalk' },
|
||||
{ npmName: '@wecom/wecom-openclaw-plugin', pluginId: 'wecom' }
|
||||
{ npmName: '@wecom/wecom-openclaw-plugin', pluginId: 'wecom' },
|
||||
{ npmName: '@sliverp/qqbot', pluginId: 'qqbot' },
|
||||
];
|
||||
|
||||
function getVirtualStoreNodeModules(realPkgPath) {
|
||||
|
||||
1
src/assets/channels/qq.svg
Normal file
1
src/assets/channels/qq.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1773123341637" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1663" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M511.09761 957.257c-80.159 0-153.737-25.019-201.11-62.386-24.057 6.702-54.831 17.489-74.252 30.864-16.617 11.439-14.546 23.106-11.55 27.816 13.15 20.689 225.583 13.211 286.912 6.767v-3.061z" fill="#FAAD08" p-id="1664"></path><path d="M496.65061 957.257c80.157 0 153.737-25.019 201.11-62.386 24.057 6.702 54.83 17.489 74.253 30.864 16.616 11.439 14.543 23.106 11.55 27.816-13.15 20.689-225.584 13.211-286.914 6.767v-3.061z" fill="#FAAD08" p-id="1665"></path><path d="M497.12861 474.524c131.934-0.876 237.669-25.783 273.497-35.34 8.541-2.28 13.11-6.364 13.11-6.364 0.03-1.172 0.542-20.952 0.542-31.155C784.27761 229.833 701.12561 57.173 496.64061 57.162 292.15661 57.173 209.00061 229.832 209.00061 401.665c0 10.203 0.516 29.983 0.547 31.155 0 0 3.717 3.821 10.529 5.67 33.078 8.98 140.803 35.139 276.08 36.034h0.972z" fill="#000000" p-id="1666"></path><path d="M860.28261 619.782c-8.12-26.086-19.204-56.506-30.427-85.72 0 0-6.456-0.795-9.718 0.148-100.71 29.205-222.773 47.818-315.792 46.695h-0.962C410.88561 582.017 289.65061 563.617 189.27961 534.698 185.44461 533.595 177.87261 534.063 177.87261 534.063 166.64961 563.276 155.56661 593.696 147.44761 619.782 108.72961 744.168 121.27261 795.644 130.82461 796.798c20.496 2.474 79.78-93.637 79.78-93.637 0 97.66 88.324 247.617 290.576 248.996a718.01 718.01 0 0 1 5.367 0C708.80161 950.778 797.12261 800.822 797.12261 703.162c0 0 59.284 96.111 79.783 93.637 9.55-1.154 22.093-52.63-16.623-177.017" fill="#000000" p-id="1667"></path><path d="M434.38261 316.917c-27.9 1.24-51.745-30.106-53.24-69.956-1.518-39.877 19.858-73.207 47.764-74.454 27.875-1.224 51.703 30.109 53.218 69.974 1.527 39.877-19.853 73.2-47.742 74.436m206.67-69.956c-1.494 39.85-25.34 71.194-53.24 69.956-27.888-1.238-49.269-34.559-47.742-74.435 1.513-39.868 25.341-71.201 53.216-69.974 27.909 1.247 49.285 34.576 47.767 74.453" fill="#FFFFFF" p-id="1668"></path><path d="M683.94261 368.627c-7.323-17.609-81.062-37.227-172.353-37.227h-0.98c-91.29 0-165.031 19.618-172.352 37.227a6.244 6.244 0 0 0-0.535 2.505c0 1.269 0.393 2.414 1.006 3.386 6.168 9.765 88.054 58.018 171.882 58.018h0.98c83.827 0 165.71-48.25 171.881-58.016a6.352 6.352 0 0 0 1.002-3.395c0-0.897-0.2-1.736-0.531-2.498" fill="#FAAD08" p-id="1669"></path><path d="M467.63161 256.377c1.26 15.886-7.377 30-19.266 31.542-11.907 1.544-22.569-10.083-23.836-25.978-1.243-15.895 7.381-30.008 19.25-31.538 11.927-1.549 22.607 10.088 23.852 25.974m73.097 7.935c2.533-4.118 19.827-25.77 55.62-17.886 9.401 2.07 13.75 5.116 14.668 6.316 1.355 1.77 1.726 4.29 0.352 7.684-2.722 6.725-8.338 6.542-11.454 5.226-2.01-0.85-26.94-15.889-49.905 6.553-1.579 1.545-4.405 2.074-7.085 0.242-2.678-1.834-3.786-5.553-2.196-8.135" fill="#000000" p-id="1670"></path><path d="M504.33261 584.495h-0.967c-63.568 0.752-140.646-7.504-215.286-21.92-6.391 36.262-10.25 81.838-6.936 136.196 8.37 137.384 91.62 223.736 220.118 224.996H506.48461c128.498-1.26 211.748-87.612 220.12-224.996 3.314-54.362-0.547-99.938-6.94-136.203-74.654 14.423-151.745 22.684-215.332 21.927" fill="#FFFFFF" p-id="1671"></path><path d="M323.27461 577.016v137.468s64.957 12.705 130.031 3.91V591.59c-41.225-2.262-85.688-7.304-130.031-14.574" fill="#EB1C26" p-id="1672"></path><path d="M788.09761 432.536s-121.98 40.387-283.743 41.539h-0.962c-161.497-1.147-283.328-41.401-283.744-41.539l-40.854 106.952c102.186 32.31 228.837 53.135 324.598 51.926l0.96-0.002c95.768 1.216 222.4-19.61 324.6-51.924l-40.855-106.952z" fill="#EB1C26" p-id="1673"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -315,6 +315,25 @@
|
||||
"Create a new Bot Account",
|
||||
"Copy the access token"
|
||||
]
|
||||
},
|
||||
"qqbot": {
|
||||
"description": "Connect QQ Bot via @sliverp/qqbot plugin",
|
||||
"docsUrl": "https://github.com/sliverp/qqbot/blob/main/README.zh.md",
|
||||
"fields": {
|
||||
"appId": {
|
||||
"label": "App ID",
|
||||
"placeholder": "Your QQ Bot App ID"
|
||||
},
|
||||
"clientSecret": {
|
||||
"label": "Client Secret",
|
||||
"placeholder": "Your QQ Bot Client Secret"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"Register an app at QQ Bot Open Platform",
|
||||
"Get App ID and Client Secret",
|
||||
"Fill in your credentials below"
|
||||
]
|
||||
}
|
||||
},
|
||||
"viewDocs": "View Documentation"
|
||||
|
||||
@@ -315,6 +315,25 @@
|
||||
"アクセストークンをコピーします"
|
||||
],
|
||||
"docsUrl": "https://docs.openclaw.ai/channels/mattermost"
|
||||
},
|
||||
"qqbot": {
|
||||
"description": "@sliverp/qqbot プラグイン経由で QQ ボットに接続します",
|
||||
"fields": {
|
||||
"appId": {
|
||||
"label": "App ID",
|
||||
"placeholder": "QQ ボットの App ID"
|
||||
},
|
||||
"clientSecret": {
|
||||
"label": "Client Secret",
|
||||
"placeholder": "QQ ボットの Client Secret"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"QQ ボットオープンプラットフォームでアプリを登録します",
|
||||
"App ID と Client Secret を取得します",
|
||||
"認証情報を入力します"
|
||||
],
|
||||
"docsUrl": "https://github.com/sliverp/qqbot/blob/main/README.zh.md"
|
||||
}
|
||||
},
|
||||
"viewDocs": "ドキュメントを表示"
|
||||
|
||||
@@ -315,6 +315,25 @@
|
||||
"创建一个新的 Bot 账户",
|
||||
"复制访问令牌"
|
||||
]
|
||||
},
|
||||
"qqbot": {
|
||||
"description": "通过 @sliverp/qqbot 插件连接 QQ 机器人",
|
||||
"docsUrl": "https://github.com/sliverp/qqbot/blob/main/README.zh.md",
|
||||
"fields": {
|
||||
"appId": {
|
||||
"label": "App ID",
|
||||
"placeholder": "您的 QQ 机器人 App ID"
|
||||
},
|
||||
"clientSecret": {
|
||||
"label": "Client Secret",
|
||||
"placeholder": "您的 QQ 机器人 Client Secret"
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"前往 QQ 机器人开放平台注册应用",
|
||||
"获取 App ID 和 Client Secret",
|
||||
"在下方填写凭证信息"
|
||||
]
|
||||
}
|
||||
},
|
||||
"viewDocs": "查看文档"
|
||||
|
||||
@@ -51,6 +51,7 @@ import whatsappIcon from '@/assets/channels/whatsapp.svg';
|
||||
import dingtalkIcon from '@/assets/channels/dingtalk.svg';
|
||||
import feishuIcon from '@/assets/channels/feishu.svg';
|
||||
import wecomIcon from '@/assets/channels/wecom.svg';
|
||||
import qqIcon from '@/assets/channels/qq.svg';
|
||||
|
||||
export function Channels() {
|
||||
const { t } = useTranslation('channels');
|
||||
@@ -285,6 +286,8 @@ function ChannelLogo({ type }: { type: ChannelType }) {
|
||||
return <img src={feishuIcon} alt="Feishu" className="w-[22px] h-[22px] dark:invert" />;
|
||||
case 'wecom':
|
||||
return <img src={wecomIcon} alt="WeCom" className="w-[22px] h-[22px] dark:invert" />;
|
||||
case 'qqbot':
|
||||
return <img src={qqIcon} alt="QQ" className="w-[22px] h-[22px] dark:invert" />;
|
||||
default:
|
||||
return <span className="text-[22px]">{CHANNEL_ICONS[type] || '💬'}</span>;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ export type ChannelType =
|
||||
| 'line'
|
||||
| 'msteams'
|
||||
| 'googlechat'
|
||||
| 'mattermost';
|
||||
| 'mattermost'
|
||||
| 'qqbot';
|
||||
|
||||
/**
|
||||
* Channel connection status
|
||||
@@ -92,6 +93,7 @@ export const CHANNEL_ICONS: Record<ChannelType, string> = {
|
||||
msteams: '👔',
|
||||
googlechat: '💭',
|
||||
mattermost: '💠',
|
||||
qqbot: '🐧',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -111,12 +113,43 @@ export const CHANNEL_NAMES: Record<ChannelType, string> = {
|
||||
msteams: 'Microsoft Teams',
|
||||
googlechat: 'Google Chat',
|
||||
mattermost: 'Mattermost',
|
||||
qqbot: 'QQ Bot',
|
||||
};
|
||||
|
||||
/**
|
||||
* Channel metadata with configuration information
|
||||
*/
|
||||
export const CHANNEL_META: Record<ChannelType, ChannelMeta> = {
|
||||
qqbot: {
|
||||
id: 'qqbot',
|
||||
name: 'QQ Bot',
|
||||
icon: '🐧',
|
||||
description: 'channels:meta.qqbot.description',
|
||||
connectionType: 'token',
|
||||
docsUrl: 'channels:meta.qqbot.docsUrl',
|
||||
configFields: [
|
||||
{
|
||||
key: 'appId',
|
||||
label: 'channels:meta.qqbot.fields.appId.label',
|
||||
type: 'text',
|
||||
placeholder: 'channels:meta.qqbot.fields.appId.placeholder',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'clientSecret',
|
||||
label: 'channels:meta.qqbot.fields.clientSecret.label',
|
||||
type: 'password',
|
||||
placeholder: 'channels:meta.qqbot.fields.clientSecret.placeholder',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
instructions: [
|
||||
'channels:meta.qqbot.instructions.0',
|
||||
'channels:meta.qqbot.instructions.1',
|
||||
'channels:meta.qqbot.instructions.2',
|
||||
],
|
||||
isPlugin: true,
|
||||
},
|
||||
dingtalk: {
|
||||
id: 'dingtalk',
|
||||
name: 'DingTalk',
|
||||
@@ -529,7 +562,7 @@ export const CHANNEL_META: Record<ChannelType, ChannelMeta> = {
|
||||
* Get primary supported channels (non-plugin, commonly used)
|
||||
*/
|
||||
export function getPrimaryChannels(): ChannelType[] {
|
||||
return ['telegram', 'discord', 'whatsapp', 'dingtalk', 'feishu', 'wecom'];
|
||||
return ['telegram', 'discord', 'whatsapp', 'dingtalk', 'feishu', 'wecom', 'qqbot'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -537,4 +570,4 @@ export function getPrimaryChannels(): ChannelType[] {
|
||||
*/
|
||||
export function getAllChannels(): ChannelType[] {
|
||||
return Object.keys(CHANNEL_META) as ChannelType[];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user