fix(feishu): feishu connector name validate (#797)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
Haze
2026-04-08 19:16:15 +08:00
committed by GitHub
Unverified
parent c1e165d48d
commit d03902dd4d
13 changed files with 521 additions and 17 deletions

View File

@@ -38,6 +38,7 @@ import {
OPENCLAW_WECHAT_CHANNEL_TYPE,
UI_WECHAT_CHANNEL_TYPE,
buildQrChannelEventName,
isCanonicalOpenClawAccountId,
toOpenClawChannelType,
toUiChannelType,
} from '../../utils/channel-alias';
@@ -89,6 +90,47 @@ function buildQrLoginKey(channelType: string, accountId?: string): string {
return `${toUiChannelType(channelType)}:${accountId?.trim() || '__new__'}`;
}
async function isLegacyConfiguredAccountId(channelType: string, accountId: string): Promise<boolean> {
const config = await readOpenClawConfig();
const configuredAccounts = listConfiguredChannelAccountsFromConfig(config) ?? {};
const storedChannelType = resolveStoredChannelType(channelType);
const knownAccountIds = configuredAccounts[storedChannelType]?.accountIds ?? [];
return knownAccountIds.includes(accountId);
}
async function validateCanonicalAccountId(
channelType: string,
accountId: string | undefined,
options?: { allowLegacyConfiguredId?: boolean },
): Promise<string | null> {
if (!accountId) return null;
const trimmed = accountId.trim();
if (!trimmed) return 'accountId cannot be empty';
if (isCanonicalOpenClawAccountId(trimmed)) {
return null;
}
if (options?.allowLegacyConfiguredId && await isLegacyConfiguredAccountId(channelType, trimmed)) {
return null;
}
// Backward compatibility note:
// existing legacy IDs can still be edited/bound if they already exist in config.
// new account IDs must be canonical to match OpenClaw runtime routing behavior.
return 'Invalid accountId format. Use lowercase letters, numbers, hyphens, or underscores only (max 64 chars, must start with a letter or number).';
}
async function validateAccountIdOrReply(
res: ServerResponse,
channelType: string,
accountId: string | undefined,
): Promise<boolean> {
const error = await validateCanonicalAccountId(channelType, accountId, { allowLegacyConfiguredId: true });
if (!error) {
return true;
}
sendJson(res, 400, { success: false, error });
return false;
}
function setActiveQrLogin(channelType: string, sessionKey: string, accountId?: string): string {
const loginKey = buildQrLoginKey(channelType, accountId);
activeQrLogins.set(loginKey, sessionKey);
@@ -1087,6 +1129,10 @@ export async function handleChannelRoutes(
if (url.pathname === '/api/channels/default-account' && req.method === 'PUT') {
try {
const body = await parseJsonBody<{ channelType: string; accountId: string }>(req);
const validAccountId = await validateAccountIdOrReply(res, body.channelType, body.accountId);
if (!validAccountId) {
return true;
}
await setChannelDefaultAccount(body.channelType, body.accountId);
scheduleGatewayChannelSaveRefresh(ctx, body.channelType, `channel:setDefaultAccount:${body.channelType}`);
sendJson(res, 200, { success: true });
@@ -1099,6 +1145,10 @@ export async function handleChannelRoutes(
if (url.pathname === '/api/channels/binding' && req.method === 'PUT') {
try {
const body = await parseJsonBody<{ channelType: string; accountId: string; agentId: string }>(req);
const validAccountId = await validateAccountIdOrReply(res, body.channelType, body.accountId);
if (!validAccountId) {
return true;
}
await assignChannelAccountToAgent(body.agentId, resolveStoredChannelType(body.channelType), body.accountId);
scheduleGatewayChannelSaveRefresh(ctx, body.channelType, `channel:setBinding:${body.channelType}`);
sendJson(res, 200, { success: true });
@@ -1111,6 +1161,10 @@ export async function handleChannelRoutes(
if (url.pathname === '/api/channels/binding' && req.method === 'DELETE') {
try {
const body = await parseJsonBody<{ channelType: string; accountId: string }>(req);
const validAccountId = await validateAccountIdOrReply(res, body.channelType, body.accountId);
if (!validAccountId) {
return true;
}
await clearChannelBinding(resolveStoredChannelType(body.channelType), body.accountId);
scheduleGatewayChannelSaveRefresh(ctx, body.channelType, `channel:clearBinding:${body.channelType}`);
sendJson(res, 200, { success: true });
@@ -1212,6 +1266,10 @@ export async function handleChannelRoutes(
if (url.pathname === '/api/channels/config' && req.method === 'POST') {
try {
const body = await parseJsonBody<{ channelType: string; config: Record<string, unknown>; accountId?: string }>(req);
const validAccountId = await validateAccountIdOrReply(res, body.channelType, body.accountId);
if (!validAccountId) {
return true;
}
const storedChannelType = resolveStoredChannelType(body.channelType);
if (storedChannelType === 'dingtalk') {
const installResult = await ensureDingTalkPluginInstalled();

View File

@@ -44,3 +44,9 @@ export function normalizeOpenClawAccountId(value: string | null | undefined, fal
}
return normalized;
}
export function isCanonicalOpenClawAccountId(value: string | null | undefined): boolean {
const trimmed = (value ?? '').trim();
if (!trimmed) return false;
return normalizeOpenClawAccountId(trimmed, '') === trimmed;
}