feat: enhance channel configuration UI and validation

- Updated ChannelInstructionsPanel to include a button for viewing documentation, improving user guidance.
- Enhanced ChannelTokenField to support showing/hiding secret values with appropriate labels and icons.
- Refined ChannelTypeSelector to display connection type icons and improved layout for better user experience.
- Added new messages for documentation links, validation feedback, and secret management in i18n.
- Extended ChannelMeta to include optional documentation URLs for better context on configuration fields.
- Implemented credential validation logic in ChannelsPage to ensure user inputs are validated before saving.
- Introduced ChannelLogo component to display channel icons in the UI.
- Added tests for channel credential validation to ensure proper error handling and feedback.
This commit is contained in:
duanshuwen
2026-04-19 16:43:07 +08:00
parent d2e48b21d8
commit 18f12d6ce3
22 changed files with 1131 additions and 301 deletions

View File

@@ -4,6 +4,15 @@ const mocks = vi.hoisted(() => ({
configGet: vi.fn(),
listStoredChannelAccountRecords: vi.fn(),
listCronJobs: vi.fn(),
listAgentsSnapshot: vi.fn(),
assignChannelToAgent: vi.fn(),
clearAllChannelBindings: vi.fn(),
clearChannelBinding: vi.fn(),
listAgentsSnapshotResult: {
agents: [],
channelOwners: {},
channelAccountOwners: {},
},
}));
vi.mock('@electron/service/config-service', () => ({
@@ -23,17 +32,46 @@ vi.mock('../electron/utils/channel-config', () => ({
listStoredChannelAccountRecords: mocks.listStoredChannelAccountRecords,
}));
vi.mock('../electron/utils/agent-config', () => ({
assignChannelToAgent: mocks.assignChannelToAgent,
clearAllChannelBindings: mocks.clearAllChannelBindings,
clearChannelBinding: mocks.clearChannelBinding,
listAgentsSnapshot: mocks.listAgentsSnapshot,
}));
import {
getSelectedChannelsConfig,
listSelectedChannelAccountGroups,
listSelectedChannelTargets,
} from '../electron/utils/channels';
import { normalizeRequest } from '../electron/api/route-utils';
import { handleChannelRoutes } from '../electron/api/routes/channels';
function createRouteContext() {
const notifyRuntimeChanged = vi.fn();
return {
gatewayManager: {
notifyRuntimeChanged,
},
providerApiService: {
getAccounts: () => [],
getDefault: () => ({ accountId: 'default' }),
},
mainWindow: null,
notifyRuntimeChanged,
} as any;
}
describe('channels utilities', () => {
beforeEach(() => {
mocks.configGet.mockReset();
mocks.listStoredChannelAccountRecords.mockReset();
mocks.listCronJobs.mockReset();
mocks.listAgentsSnapshot.mockReset();
mocks.assignChannelToAgent.mockReset();
mocks.clearAllChannelBindings.mockReset();
mocks.clearChannelBinding.mockReset();
mocks.listAgentsSnapshot.mockReturnValue(mocks.listAgentsSnapshotResult);
});
it('normalizes and groups selected channels by inferred channel type', () => {
@@ -285,3 +323,118 @@ describe('channels utilities', () => {
});
});
});
describe('channel credential validation route', () => {
it('returns a valid result with warnings for a token/password/url payload', async () => {
const ctx = createRouteContext();
const response = await handleChannelRoutes(normalizeRequest({
path: '/api/channels/credentials/validate',
method: 'POST',
body: JSON.stringify({
channelType: 'imessage',
accountId: 'acct-1',
credentials: {
serverUrl: 'http://bridge.example.com',
password: 'bridge-password',
},
}),
}), ctx);
expect(response?.ok).toBe(true);
expect(response?.json).toMatchObject({
success: true,
valid: true,
errors: [],
warnings: ['Server URL 使用了不安全的 http URL建议改用 https'],
details: expect.objectContaining({
channelType: 'imessage',
accountId: 'acct-1',
connectionType: 'token',
}),
});
expect(response?.json).toMatchObject({
details: expect.objectContaining({
fields: expect.arrayContaining([
expect.objectContaining({
key: 'serverUrl',
kind: 'url',
provided: true,
valid: true,
warnings: ['Server URL 使用了不安全的 http URL建议改用 https'],
}),
expect.objectContaining({
key: 'password',
kind: 'password',
provided: true,
valid: true,
}),
]),
}),
});
expect(ctx.notifyRuntimeChanged).not.toHaveBeenCalled();
});
it('returns field errors for missing token and text credentials', async () => {
const ctx = createRouteContext();
const response = await handleChannelRoutes(normalizeRequest({
path: '/api/channels/credentials/validate',
method: 'POST',
body: JSON.stringify({
channelType: 'discord',
credentials: {
token: ' ',
guildId: '',
channelId: '123456789012345678',
},
}),
}), ctx);
expect(response?.ok).toBe(true);
expect(response?.json).toMatchObject({
success: true,
valid: false,
warnings: [],
errors: [
'Bot Token is required',
'Guild ID is required',
],
details: expect.objectContaining({
channelType: 'discord',
connectionType: 'token',
}),
});
expect(response?.json).toMatchObject({
details: expect.objectContaining({
fields: expect.arrayContaining([
expect.objectContaining({
key: 'token',
kind: 'password',
provided: false,
valid: false,
errors: ['Bot Token is required'],
}),
expect.objectContaining({
key: 'guildId',
kind: 'text',
provided: false,
valid: false,
errors: ['Guild ID is required'],
}),
expect.objectContaining({
key: 'channelId',
kind: 'text',
provided: true,
valid: true,
}),
]),
}),
});
expect(ctx.notifyRuntimeChanged).not.toHaveBeenCalled();
});
});