feat(wecom): 企微 API 插件增加 MCP Streamable HTTP 端点

与 REST/OpenAPI 并存,按实例权限自动 tools/list,免手填工具。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-08 16:57:42 +08:00
parent cae426fbc2
commit 976bde37cc
23 changed files with 1212 additions and 63 deletions
@@ -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');
});
});
+92
View File
@@ -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}`;
}