feat(channel): add qq bot (#363)

Co-authored-by: 陶建行 <189307154@qq.com>
This commit is contained in:
taojianhang
2026-03-10 14:20:02 +08:00
committed by GitHub
Unverified
parent 65c2b73e23
commit 1bae8229af
14 changed files with 1018 additions and 784 deletions

5
.npmrc
View File

@@ -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/

View File

@@ -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 });

View File

@@ -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 };

View File

@@ -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('*')) {

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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 });

View File

@@ -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) {

View 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

View File

@@ -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"

View File

@@ -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": "ドキュメントを表示"

View File

@@ -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": "查看文档"

View File

@@ -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>;
}

View File

@@ -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[];
}
}