Files
zn-ai/electron/api/routes/channels.ts
duanshuwen ef46c73c3e Add unit tests for channel utilities and configure testing environment
- Created a new test file `channels.test.ts` to cover utilities related to channel configurations and targets.
- Implemented tests for normalizing and grouping selected channels by type, as well as building channel targets from account data and cron history.
- Mocked necessary dependencies to isolate tests and ensure accurate results.
- Updated `vite.config.ts` to set up the testing environment with jsdom and enable global variables for tests.
2026-04-18 16:12:49 +08:00

304 lines
9.6 KiB
TypeScript

import type { HostApiContext } from '../context';
import type { NormalizedHostApiRequest } from '../route-utils';
import { fail, ok, parseJsonBody } from '../route-utils';
import {
assignChannelToAgent,
clearAllChannelBindings,
clearChannelBinding,
listAgentsSnapshot,
} from '../../utils/agent-config';
import {
getChannelFormValues,
hasStoredChannelAccount,
isCanonicalChannelAccountId,
listStoredChannelTypes,
saveChannelConfig,
setChannelDefaultAccount,
setChannelEnabled,
deleteChannelConfig,
} from '../../utils/channel-config';
import {
listSelectedChannelAccountGroups,
listSelectedChannelTargets,
} from '../../utils/channels';
function getProviderSnapshot(ctx: HostApiContext) {
const accounts = ctx.providerApiService
.getAccounts()
.filter((account) => account.enabled !== false);
const defaultAccountId = ctx.providerApiService.getDefault().accountId;
return { accounts, defaultAccountId };
}
export async function handleChannelRoutes(
request: NormalizedHostApiRequest,
ctx: HostApiContext,
) {
const { pathname, method } = request;
const { accounts, defaultAccountId } = getProviderSnapshot(ctx);
const snapshot = listAgentsSnapshot(accounts, defaultAccountId);
if (pathname === '/api/channels/accounts' && method === 'GET') {
return ok({
success: true,
channels: listSelectedChannelAccountGroups(snapshot),
});
}
if (pathname === '/api/channels/configured' && method === 'GET') {
return ok({
success: true,
channels: listStoredChannelTypes(),
});
}
if (pathname === '/api/channels/targets' && method === 'GET') {
const channelType = request.url.searchParams.get('channelType')?.trim() || '';
const accountId = request.url.searchParams.get('accountId')?.trim() || null;
const query = request.url.searchParams.get('query')?.trim() || null;
if (!channelType) {
return fail(400, 'channelType is required');
}
return ok({
success: true,
targets: listSelectedChannelTargets(channelType, accountId, query),
});
}
if (pathname === '/api/channels/default-account' && method === 'PUT') {
try {
const body = parseJsonBody<{
channelType?: string;
accountId?: string | null;
}>(request.body);
const channelType = String(body?.channelType ?? '').trim();
const accountId = String(body?.accountId ?? '').trim();
if (!channelType) {
return fail(400, 'channelType is required');
}
if (!accountId) {
return fail(400, 'accountId is required');
}
setChannelDefaultAccount(channelType, accountId);
ctx.gatewayManager.notifyRuntimeChanged({
topics: ['channels', 'channel-targets', 'agents'],
reason: 'channels:default-account-updated',
channelType,
accountId,
});
return ok({
success: true,
channels: listSelectedChannelAccountGroups(listAgentsSnapshot(accounts, defaultAccountId)),
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/channels/config/enabled' && method === 'PUT') {
try {
const body = parseJsonBody<{
channelType?: string;
enabled?: boolean;
}>(request.body);
const channelType = String(body?.channelType ?? '').trim();
if (!channelType) {
return fail(400, 'channelType is required');
}
setChannelEnabled(channelType, body?.enabled !== false);
ctx.gatewayManager.notifyRuntimeChanged({
topics: ['channels', 'channel-targets', 'agents'],
reason: 'channels:enabled-updated',
channelType,
});
return ok({
success: true,
channels: listSelectedChannelAccountGroups(listAgentsSnapshot(accounts, defaultAccountId)),
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/channels/binding' && method === 'PUT') {
try {
const body = parseJsonBody<{
channelType?: string;
accountId?: string | null;
agentId?: string;
}>(request.body);
const channelType = String(body?.channelType ?? '').trim();
const accountId = String(body?.accountId ?? '').trim();
const agentId = String(body?.agentId ?? '').trim();
if (!channelType) {
return fail(400, 'channelType is required');
}
if (!accountId) {
return fail(400, 'accountId is required');
}
if (!agentId) {
return fail(400, 'agentId is required');
}
if (!snapshot.agents.some((agent) => agent.id === agentId)) {
return fail(404, `Agent "${agentId}" not found`);
}
const groupedChannels = listSelectedChannelAccountGroups(snapshot);
const group = groupedChannels.find((entry) => entry.channelType === channelType);
if (!group || !group.accounts.some((entry) => entry.accountId === accountId)) {
return fail(404, `Channel account "${channelType}:${accountId}" not found`);
}
const result = assignChannelToAgent(agentId, channelType, accountId, accounts, defaultAccountId);
ctx.gatewayManager.notifyRuntimeChanged({
topics: ['agents', 'channels', 'channel-targets'],
reason: 'channels:binding-updated',
channelType,
accountId,
});
return ok({
success: true,
...result,
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/channels/config' && method === 'POST') {
try {
const body = parseJsonBody<{
channelType?: string;
accountId?: string | null;
channelLabel?: string | null;
accountName?: string | null;
channelUrl?: string | null;
enabled?: boolean;
config?: Record<string, unknown>;
metadata?: Record<string, unknown>;
}>(request.body);
const channelType = String(body?.channelType ?? '').trim();
const accountId = String(body?.accountId ?? '').trim();
if (!channelType) {
return fail(400, 'channelType is required');
}
if (accountId && !isCanonicalChannelAccountId(accountId) && !hasStoredChannelAccount(channelType, accountId)) {
return fail(400, 'Invalid accountId format. Use lowercase letters, numbers, hyphens, or underscores only (max 64 chars, must start with a letter or number).');
}
saveChannelConfig({
channelType,
accountId: accountId || undefined,
channelLabel: body?.channelLabel,
accountName: body?.accountName,
channelUrl: body?.channelUrl,
enabled: body?.enabled,
config: body?.config,
metadata: body?.metadata,
});
ctx.gatewayManager.notifyRuntimeChanged({
topics: ['channels', 'channel-targets', 'agents'],
reason: 'channels:config-saved',
channelType,
accountId: accountId || undefined,
});
return ok({
success: true,
channels: listSelectedChannelAccountGroups(listAgentsSnapshot(accounts, defaultAccountId)),
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/channels/binding' && method === 'DELETE') {
try {
const body = parseJsonBody<{
channelType?: string;
accountId?: string | null;
}>(request.body);
const channelType = String(body?.channelType ?? '').trim();
const accountId = String(body?.accountId ?? '').trim();
if (!channelType) {
return fail(400, 'channelType is required');
}
if (!accountId) {
return fail(400, 'accountId is required');
}
const result = clearChannelBinding(channelType, accountId, accounts, defaultAccountId);
ctx.gatewayManager.notifyRuntimeChanged({
topics: ['agents', 'channels', 'channel-targets'],
reason: 'channels:binding-cleared',
channelType,
accountId,
});
return ok({
success: true,
...result,
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname.startsWith('/api/channels/config/') && method === 'GET') {
try {
const channelType = decodeURIComponent(pathname.slice('/api/channels/config/'.length));
const accountId = request.url.searchParams.get('accountId')?.trim() || null;
return ok({
success: true,
values: getChannelFormValues(channelType, accountId) ?? {},
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname.startsWith('/api/channels/config/') && method === 'DELETE') {
try {
const channelType = decodeURIComponent(pathname.slice('/api/channels/config/'.length));
const accountId = request.url.searchParams.get('accountId')?.trim() || null;
deleteChannelConfig(channelType, accountId);
if (accountId) {
clearChannelBinding(channelType, accountId, accounts, defaultAccountId);
} else {
clearAllChannelBindings(channelType, accounts, defaultAccountId);
}
ctx.gatewayManager.notifyRuntimeChanged({
topics: ['channels', 'channel-targets', 'agents'],
reason: 'channels:config-deleted',
channelType,
accountId: accountId || undefined,
});
return ok({
success: true,
channels: listSelectedChannelAccountGroups(listAgentsSnapshot(accounts, defaultAccountId)),
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));
}
}
return null;
}