v4.0.17企业微信API插件优化
This commit is contained in:
@@ -2,7 +2,12 @@ import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
WECOM_PLUGIN_API_KEY_HEADER,
|
||||
WECOM_PLUGIN_BASE_PATH,
|
||||
WECOM_PLUGIN_PATH_PERMISSION,
|
||||
allowedWecomPluginOpenApiPaths,
|
||||
maskWecomPluginApiKey,
|
||||
parseWecomPluginPermissions,
|
||||
resolveWecomPluginPublicUrl,
|
||||
wecomPluginHasPermission,
|
||||
} from './wecom-plugin';
|
||||
|
||||
describe('resolveWecomPluginPublicUrl', () => {
|
||||
@@ -31,3 +36,44 @@ describe('resolveWecomPluginPublicUrl', () => {
|
||||
expect(WECOM_PLUGIN_API_KEY_HEADER).toBe('X-Api-Key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseWecomPluginPermissions', () => {
|
||||
it('keeps catalog keys and drops unknown', () => {
|
||||
expect(parseWecomPluginPermissions(['order.read', 'bogus', 'metrics.read'])).toEqual([
|
||||
'order.read',
|
||||
'metrics.read',
|
||||
]);
|
||||
expect(parseWecomPluginPermissions('["user.read","store.read"]')).toEqual([
|
||||
'user.read',
|
||||
'store.read',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('maskWecomPluginApiKey / path permission map', () => {
|
||||
it('masks middle of the key', () => {
|
||||
expect(maskWecomPluginApiKey('WAv2xxxxYb9')).toBe('WAv2••••xYb9');
|
||||
});
|
||||
|
||||
it('maps openapi paths to tool permissions', () => {
|
||||
expect(WECOM_PLUGIN_PATH_PERMISSION['/orders']).toBe('order.read');
|
||||
expect(WECOM_PLUGIN_PATH_PERMISSION['/promo-codes/{code}/stats']).toBe('promo.read');
|
||||
});
|
||||
|
||||
it('filters openapi paths by instance permissions', () => {
|
||||
expect(allowedWecomPluginOpenApiPaths(['order.read', 'user.read'])).toEqual([
|
||||
'/orders',
|
||||
'/users',
|
||||
]);
|
||||
expect(allowedWecomPluginOpenApiPaths(['store.audit.read'])).toEqual([
|
||||
'/store-audits',
|
||||
'/store-info-audits',
|
||||
'/store-info-audits/{id}',
|
||||
'/store-package-audits',
|
||||
'/store-package-audits/{id}',
|
||||
]);
|
||||
expect(allowedWecomPluginOpenApiPaths(['partner.read'])).toContain('/partners');
|
||||
expect(wecomPluginHasPermission(['order.read'], 'order.read')).toBe(true);
|
||||
expect(wecomPluginHasPermission(['order.read'], 'metrics.read')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,188 @@ export const WECOM_PLUGIN_METRICS_KINDS = ['today', 'daily', 'weekly', 'monthly'
|
||||
|
||||
export type WecomPluginMetricsKind = (typeof WECOM_PLUGIN_METRICS_KINDS)[number];
|
||||
|
||||
export const WECOM_PLUGIN_PERMISSIONS = [
|
||||
'order.read',
|
||||
'user.read',
|
||||
'store.read',
|
||||
'redeem.read',
|
||||
'promo.read',
|
||||
'metrics.read',
|
||||
'store.audit.read',
|
||||
'partner.read',
|
||||
] as const;
|
||||
|
||||
export type WecomPluginPermission = (typeof WECOM_PLUGIN_PERMISSIONS)[number];
|
||||
|
||||
export const WECOM_PLUGIN_PERMISSION_LABELS: Record<WecomPluginPermission, string> = {
|
||||
'order.read': '查询订单',
|
||||
'user.read': '查询用户',
|
||||
'store.read': '查询门店',
|
||||
'redeem.read': '查询核销',
|
||||
'promo.read': '查询推广码',
|
||||
'metrics.read': '经营指标',
|
||||
'store.audit.read': '门店/套餐审核',
|
||||
'partner.read': '合伙人关联查询',
|
||||
};
|
||||
|
||||
export const WECOM_PLUGIN_PERMISSION_GROUPS: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
permissions: WecomPluginPermission[];
|
||||
}> = [
|
||||
{
|
||||
key: 'query',
|
||||
label: '只读查询',
|
||||
permissions: [
|
||||
'order.read',
|
||||
'user.read',
|
||||
'store.read',
|
||||
'redeem.read',
|
||||
'promo.read',
|
||||
'metrics.read',
|
||||
'partner.read',
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'audit',
|
||||
label: '审核对照',
|
||||
permissions: ['store.audit.read'],
|
||||
},
|
||||
];
|
||||
|
||||
/** OpenAPI path → 工具权限 */
|
||||
export const WECOM_PLUGIN_PATH_PERMISSION: Record<string, WecomPluginPermission> = {
|
||||
'/orders': 'order.read',
|
||||
'/users': 'user.read',
|
||||
'/stores': 'store.read',
|
||||
'/redeems': 'redeem.read',
|
||||
'/promo-codes': 'promo.read',
|
||||
'/promo-codes/{code}/stats': 'promo.read',
|
||||
'/metrics': 'metrics.read',
|
||||
'/store-audits': 'store.audit.read',
|
||||
'/store-info-audits': 'store.audit.read',
|
||||
'/store-info-audits/{id}': 'store.audit.read',
|
||||
'/store-package-audits': 'store.audit.read',
|
||||
'/store-package-audits/{id}': 'store.audit.read',
|
||||
'/partners': 'partner.read',
|
||||
'/partners/{partnerId}/users': 'partner.read',
|
||||
'/partners/{partnerId}/stores': 'partner.read',
|
||||
'/partners/{partnerId}/orders': 'partner.read',
|
||||
};
|
||||
|
||||
export const WECOM_PLUGIN_TOOL_PATHS: Record<WecomPluginPermission, string[]> = {
|
||||
'order.read': ['/orders'],
|
||||
'user.read': ['/users'],
|
||||
'store.read': ['/stores'],
|
||||
'redeem.read': ['/redeems'],
|
||||
'promo.read': ['/promo-codes', '/promo-codes/{code}/stats'],
|
||||
'metrics.read': ['/metrics'],
|
||||
'store.audit.read': [
|
||||
'/store-audits',
|
||||
'/store-info-audits',
|
||||
'/store-info-audits/{id}',
|
||||
'/store-package-audits',
|
||||
'/store-package-audits/{id}',
|
||||
],
|
||||
'partner.read': [
|
||||
'/partners',
|
||||
'/partners/{partnerId}/users',
|
||||
'/partners/{partnerId}/stores',
|
||||
'/partners/{partnerId}/orders',
|
||||
],
|
||||
};
|
||||
|
||||
export function parseWecomPluginPermissions(
|
||||
raw?: string | string[] | null,
|
||||
): WecomPluginPermission[] {
|
||||
const valid = new Set<string>(WECOM_PLUGIN_PERMISSIONS);
|
||||
let list: string[];
|
||||
if (Array.isArray(raw)) {
|
||||
list = raw.map(String);
|
||||
} else {
|
||||
const text = String(raw ?? '').trim();
|
||||
if (!text) {
|
||||
list = [];
|
||||
} else if (text.startsWith('[')) {
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(text);
|
||||
list = Array.isArray(parsed) ? parsed.map(String) : [];
|
||||
} catch {
|
||||
list = text.split(/[,,\s]+/).map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
} else {
|
||||
list = text.split(/[,,\s]+/).map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
}
|
||||
return [...new Set(list.filter((s): s is WecomPluginPermission => valid.has(s)))];
|
||||
}
|
||||
|
||||
export function wecomPluginHasPermission(
|
||||
permissions: WecomPluginPermission[],
|
||||
permission: WecomPluginPermission,
|
||||
): boolean {
|
||||
return permissions.includes(permission);
|
||||
}
|
||||
|
||||
/** 当前实例可暴露给企微第 2 步的 OpenAPI paths */
|
||||
export function allowedWecomPluginOpenApiPaths(
|
||||
permissions: WecomPluginPermission[],
|
||||
): string[] {
|
||||
const allowed = new Set(permissions);
|
||||
return Object.entries(WECOM_PLUGIN_PATH_PERMISSION)
|
||||
.filter(([, perm]) => allowed.has(perm))
|
||||
.map(([path]) => path);
|
||||
}
|
||||
|
||||
export function maskWecomPluginApiKey(apiKey?: string | null): string {
|
||||
const v = String(apiKey ?? '').trim();
|
||||
if (!v) return '未配置';
|
||||
if (v.length <= 8) return '••••';
|
||||
return `${v.slice(0, 4)}••••${v.slice(-4)}`;
|
||||
}
|
||||
|
||||
export type WecomApiPluginDto = {
|
||||
id: string;
|
||||
name: string;
|
||||
apiKeyConfigured: boolean;
|
||||
apiKeyMasked: string;
|
||||
permissions: WecomPluginPermission[];
|
||||
remark: string | null;
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type CreateWecomApiPluginRequest = {
|
||||
name: string;
|
||||
apiKey?: string;
|
||||
permissions: WecomPluginPermission[];
|
||||
remark?: string | null;
|
||||
enabled?: boolean;
|
||||
sortOrder?: number;
|
||||
};
|
||||
|
||||
export type UpdateWecomApiPluginRequest = {
|
||||
name?: string;
|
||||
apiKey?: string;
|
||||
permissions?: WecomPluginPermission[];
|
||||
remark?: string | null;
|
||||
enabled?: boolean;
|
||||
sortOrder?: number;
|
||||
};
|
||||
|
||||
export type WecomApiPluginSecretDto = WecomApiPluginDto & {
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
export type WecomApiPluginListDto = {
|
||||
items: WecomApiPluginDto[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
};
|
||||
|
||||
/** 按 HQ 当前域名推断插件公网 Base URL(不含密钥) */
|
||||
export function resolveWecomPluginPublicUrl(hostname: string): string {
|
||||
const host = String(hostname || '').toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user