feat(wecom): 企微 API 插件增加 MCP Streamable HTTP 端点
与 REST/OpenAPI 并存,按实例权限自动 tools/list,免手填工具。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,10 +2,13 @@ import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
WECOM_PLUGIN_API_KEY_HEADER,
|
||||
WECOM_PLUGIN_BASE_PATH,
|
||||
WECOM_PLUGIN_MCP_PATH,
|
||||
WECOM_PLUGIN_PATH_PERMISSION,
|
||||
allowedWecomPluginMcpTools,
|
||||
allowedWecomPluginOpenApiPaths,
|
||||
maskWecomPluginApiKey,
|
||||
parseWecomPluginPermissions,
|
||||
resolveWecomPluginMcpUrl,
|
||||
resolveWecomPluginPublicUrl,
|
||||
wecomPluginHasPermission,
|
||||
} from './wecom-plugin';
|
||||
@@ -35,6 +38,15 @@ describe('resolveWecomPluginPublicUrl', () => {
|
||||
it('uses X-Api-Key as the plugin auth header', () => {
|
||||
expect(WECOM_PLUGIN_API_KEY_HEADER).toBe('X-Api-Key');
|
||||
});
|
||||
|
||||
it('maps hosts to the Streamable HTTP MCP endpoint', () => {
|
||||
expect(resolveWecomPluginMcpUrl('admin.dukanghaoke.com')).toBe(
|
||||
`https://api.dukanghaoke.com${WECOM_PLUGIN_BASE_PATH}${WECOM_PLUGIN_MCP_PATH}`,
|
||||
);
|
||||
expect(resolveWecomPluginMcpUrl('admin-test.dukanghaoke.com')).toBe(
|
||||
`https://api-test.dukanghaoke.com${WECOM_PLUGIN_BASE_PATH}${WECOM_PLUGIN_MCP_PATH}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseWecomPluginPermissions', () => {
|
||||
@@ -76,4 +88,20 @@ describe('maskWecomPluginApiKey / path permission map', () => {
|
||||
expect(wecomPluginHasPermission(['order.read'], 'order.read')).toBe(true);
|
||||
expect(wecomPluginHasPermission(['order.read'], 'metrics.read')).toBe(false);
|
||||
});
|
||||
|
||||
it('filters MCP tools by instance permissions', () => {
|
||||
expect(allowedWecomPluginMcpTools(['order.read', 'user.read'])).toEqual([
|
||||
'query_orders',
|
||||
'query_users',
|
||||
]);
|
||||
expect(allowedWecomPluginMcpTools(['store.audit.read'])).toEqual([
|
||||
'query_store_audits',
|
||||
'query_store_info_audits',
|
||||
'query_store_info_audit_detail',
|
||||
'query_store_package_audits',
|
||||
'query_store_package_audit_detail',
|
||||
]);
|
||||
expect(allowedWecomPluginMcpTools(['partner.read'])).toContain('query_partners');
|
||||
expect(allowedWecomPluginMcpTools(['metrics.read'])).not.toContain('query_orders');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,11 @@ export const WECOM_PLUGIN_API_KEY_HEADER = 'X-Api-Key';
|
||||
|
||||
export const WECOM_PLUGIN_BASE_PATH = '/api/v1/wecom/plugin';
|
||||
|
||||
/** 企微 MCP 插件相对 Base 的路径(Streamable HTTP 无状态) */
|
||||
export const WECOM_PLUGIN_MCP_PATH = '/mcp';
|
||||
|
||||
export const WECOM_PLUGIN_MCP_TRANSPORT = 'Streamable HTTP';
|
||||
|
||||
export const WECOM_PLUGIN_PAGE_SIZE_DEFAULT = 5;
|
||||
|
||||
export const WECOM_PLUGIN_PAGE_SIZE_MAX = 10;
|
||||
@@ -81,6 +86,81 @@ export const WECOM_PLUGIN_PATH_PERMISSION: Record<string, WecomPluginPermission>
|
||||
'/partners/{partnerId}/orders': 'partner.read',
|
||||
};
|
||||
|
||||
export const WECOM_PLUGIN_MCP_TOOL_NAMES = [
|
||||
'query_orders',
|
||||
'query_users',
|
||||
'query_stores',
|
||||
'query_redeems',
|
||||
'query_promo_codes',
|
||||
'query_promo_code_stats',
|
||||
'query_metrics',
|
||||
'query_store_audits',
|
||||
'query_store_info_audits',
|
||||
'query_store_info_audit_detail',
|
||||
'query_store_package_audits',
|
||||
'query_store_package_audit_detail',
|
||||
'query_partners',
|
||||
'query_partner_users',
|
||||
'query_partner_stores',
|
||||
'query_partner_orders',
|
||||
] as const;
|
||||
|
||||
export type WecomPluginMcpToolName = (typeof WECOM_PLUGIN_MCP_TOOL_NAMES)[number];
|
||||
|
||||
export const WECOM_PLUGIN_MCP_TOOL_META: Record<
|
||||
WecomPluginMcpToolName,
|
||||
{ permission: WecomPluginPermission; description: string }
|
||||
> = {
|
||||
query_orders: { permission: 'order.read', description: '按订单号模糊查询' },
|
||||
query_users: { permission: 'user.read', description: '按用户号或 11 位手机号查询;手机号脱敏' },
|
||||
query_stores: {
|
||||
permission: 'store.read',
|
||||
description: '按门店名称模糊查询,返回经营数据:评分、核销笔数、累计核销好客权益、合伙人、审核状态',
|
||||
},
|
||||
query_redeems: { permission: 'redeem.read', description: '按核销单号或门店名查询' },
|
||||
query_promo_codes: { permission: 'promo.read', description: '按推广码 code 或名称查询' },
|
||||
query_promo_code_stats: { permission: 'promo.read', description: '按推广码 code 查询扫码/成交统计' },
|
||||
query_metrics: {
|
||||
permission: 'metrics.read',
|
||||
description: '经营指标。kind=today 为今日截至当前;daily/weekly/monthly 与企微经营报告同一口径',
|
||||
},
|
||||
query_store_audits: { permission: 'store.audit.read', description: '门店入驻审核列表,默认 status=PENDING' },
|
||||
query_store_info_audits: { permission: 'store.audit.read', description: '门店信息变更审核列表' },
|
||||
query_store_info_audit_detail: {
|
||||
permission: 'store.audit.read',
|
||||
description: '门店信息变更审核对比,返回 live vs proposed',
|
||||
},
|
||||
query_store_package_audits: { permission: 'store.audit.read', description: '门店套餐审核列表' },
|
||||
query_store_package_audit_detail: {
|
||||
permission: 'store.audit.read',
|
||||
description: '门店套餐审核对比,返回 proposedPackages 与 livePackages',
|
||||
},
|
||||
query_partners: { permission: 'partner.read', description: '按姓名、公司名、手机号或 ID 搜索合伙人主账号' },
|
||||
query_partner_users: { permission: 'partner.read', description: '查询合伙人关联用户,可选 from/to' },
|
||||
query_partner_stores: { permission: 'partner.read', description: '查询合伙人名下门店及经营数据,可选 from/to' },
|
||||
query_partner_orders: {
|
||||
permission: 'partner.read',
|
||||
description: '查询佣金归属该合伙人的订单,可选 from/to',
|
||||
},
|
||||
};
|
||||
|
||||
export const WECOM_PLUGIN_MCP_TOOLS: Record<WecomPluginPermission, WecomPluginMcpToolName[]> = {
|
||||
'order.read': ['query_orders'],
|
||||
'user.read': ['query_users'],
|
||||
'store.read': ['query_stores'],
|
||||
'redeem.read': ['query_redeems'],
|
||||
'promo.read': ['query_promo_codes', 'query_promo_code_stats'],
|
||||
'metrics.read': ['query_metrics'],
|
||||
'store.audit.read': [
|
||||
'query_store_audits',
|
||||
'query_store_info_audits',
|
||||
'query_store_info_audit_detail',
|
||||
'query_store_package_audits',
|
||||
'query_store_package_audit_detail',
|
||||
],
|
||||
'partner.read': ['query_partners', 'query_partner_users', 'query_partner_stores', 'query_partner_orders'],
|
||||
};
|
||||
|
||||
export const WECOM_PLUGIN_TOOL_PATHS: Record<WecomPluginPermission, string[]> = {
|
||||
'order.read': ['/orders'],
|
||||
'user.read': ['/users'],
|
||||
@@ -145,6 +225,14 @@ export function allowedWecomPluginOpenApiPaths(
|
||||
.map(([path]) => path);
|
||||
}
|
||||
|
||||
/** 当前实例可暴露给企微 MCP 插件的 tools/list */
|
||||
export function allowedWecomPluginMcpTools(
|
||||
permissions: WecomPluginPermission[],
|
||||
): WecomPluginMcpToolName[] {
|
||||
const allowed = new Set(permissions);
|
||||
return WECOM_PLUGIN_MCP_TOOL_NAMES.filter((name) => allowed.has(WECOM_PLUGIN_MCP_TOOL_META[name].permission));
|
||||
}
|
||||
|
||||
export function maskWecomPluginApiKey(apiKey?: string | null): string {
|
||||
const v = String(apiKey ?? '').trim();
|
||||
if (!v) return '未配置';
|
||||
@@ -205,3 +293,7 @@ export function resolveWecomPluginPublicUrl(hostname: string): string {
|
||||
}
|
||||
return `http://localhost:3010${WECOM_PLUGIN_BASE_PATH}`;
|
||||
}
|
||||
|
||||
export function resolveWecomPluginMcpUrl(hostname: string): string {
|
||||
return `${resolveWecomPluginPublicUrl(hostname)}${WECOM_PLUGIN_MCP_PATH}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user