feat: gate WeChat media analysis by user
This commit is contained in:
@@ -23,6 +23,13 @@ function deriveWechatEndpointFromUrl(baseUrl, suffix) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseCsvList(value = '') {
|
||||
return String(value ?? '')
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function loadWechatMpConfig(env = process.env) {
|
||||
const appId = env.H5_WECHAT_MP_APP_ID?.trim() ?? env.H5_WECHAT_APP_ID?.trim() ?? '';
|
||||
const appSecret =
|
||||
@@ -73,6 +80,7 @@ export function loadWechatMpConfig(env = process.env) {
|
||||
acceptFile: env.H5_WECHAT_MP_ACCEPT_FILE !== '0',
|
||||
acceptLocation: env.H5_WECHAT_MP_ACCEPT_LOCATION !== '0',
|
||||
acceptLink: env.H5_WECHAT_MP_ACCEPT_LINK !== '0',
|
||||
mediaAnalysisGrayUsers: parseCsvList(env.H5_WECHAT_MP_MEDIA_GRAY_USERS),
|
||||
encodingAesKey: env.H5_WECHAT_MP_ENCODING_AES_KEY?.trim() ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
+47
-5
@@ -1170,6 +1170,23 @@ function normalizeNumber(value) {
|
||||
return Number.isFinite(num) ? num : null;
|
||||
}
|
||||
|
||||
function isWechatMediaGrayUser(user, configuredUsers = []) {
|
||||
const allowlist = Array.isArray(configuredUsers)
|
||||
? configuredUsers.map((value) => String(value ?? '').trim().toLowerCase()).filter(Boolean)
|
||||
: [];
|
||||
if (allowlist.length === 0) return false;
|
||||
const identities = [
|
||||
user?.userId,
|
||||
user?.username,
|
||||
user?.slug,
|
||||
user?.displayName,
|
||||
user?.nickname,
|
||||
]
|
||||
.map((value) => String(value ?? '').trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
return identities.some((identity) => allowlist.includes(identity));
|
||||
}
|
||||
|
||||
function normalizeWechatInboundIntent(inbound) {
|
||||
const msgType = String(inbound?.msgType ?? '').toLowerCase();
|
||||
const base = {
|
||||
@@ -1462,6 +1479,9 @@ export function createWechatMpService({
|
||||
acceptFile: config.acceptFile !== false,
|
||||
acceptLocation: config.acceptLocation !== false,
|
||||
acceptLink: config.acceptLink !== false,
|
||||
mediaAnalysisGrayUsers: Array.isArray(config.mediaAnalysisGrayUsers)
|
||||
? config.mediaAnalysisGrayUsers
|
||||
: [],
|
||||
asrTarget: config.asrTarget || DEFAULT_ASR_TARGET,
|
||||
};
|
||||
|
||||
@@ -1896,7 +1916,7 @@ export function createWechatMpService({
|
||||
}
|
||||
};
|
||||
|
||||
const buildIntentMetadata = (intent) => {
|
||||
const buildIntentMetadata = (intent, { mediaAnalysisEnabled = false } = {}) => {
|
||||
const mediaPublicUrl = intent.media?.publicUrl || null;
|
||||
const fileAttachment =
|
||||
intent.msgType === 'file' && mediaPublicUrl && intent.attachment?.filename
|
||||
@@ -1913,8 +1933,10 @@ export function createWechatMpService({
|
||||
originalMsgId: intent.msgId || null,
|
||||
displayText: intent.displayText || '',
|
||||
mediaPublicUrl,
|
||||
...(intent.msgType === 'image' && mediaPublicUrl ? { imageUrls: [mediaPublicUrl] } : {}),
|
||||
...(fileAttachment ? { fileAttachments: [fileAttachment] } : {}),
|
||||
...(mediaAnalysisEnabled && intent.msgType === 'image' && mediaPublicUrl
|
||||
? { imageUrls: [mediaPublicUrl] }
|
||||
: {}),
|
||||
...(mediaAnalysisEnabled && fileAttachment ? { fileAttachments: [fileAttachment] } : {}),
|
||||
recognition: intent.msgType === 'voice' ? intent.agentText || null : null,
|
||||
location: intent.location || null,
|
||||
link: intent.link || null,
|
||||
@@ -1959,6 +1981,7 @@ export function createWechatMpService({
|
||||
|
||||
const runIntentMessage = async ({ inbound, intent, user }) => {
|
||||
const wechatIntent = classifyWechatIntent(intent);
|
||||
const mediaAnalysisEnabled = isWechatMediaGrayUser(user, config.mediaAnalysisGrayUsers);
|
||||
const resetCandidate =
|
||||
intent.msgType === 'text' || intent.msgType === 'voice' ? intent.agentText : '';
|
||||
// Page Data delivery owns persistent files, datasets and two publication
|
||||
@@ -2008,7 +2031,7 @@ export function createWechatMpService({
|
||||
sessionId,
|
||||
requestId,
|
||||
agentPrompt,
|
||||
buildIntentMetadata(intent),
|
||||
buildIntentMetadata(intent, { mediaAnalysisEnabled }),
|
||||
);
|
||||
const workingDir = publishLayout?.publishDir ?? (await userAuth.resolveWorkingDir(user.userId));
|
||||
const linkExistsForRequest = resolveLinkExistsForWorkingDir(workingDir, linkExists);
|
||||
@@ -2167,7 +2190,7 @@ export function createWechatMpService({
|
||||
sessionId,
|
||||
retryId,
|
||||
retryPrompt,
|
||||
buildIntentMetadata(intent),
|
||||
buildIntentMetadata(intent, { mediaAnalysisEnabled }),
|
||||
);
|
||||
const workingDir = publishLayout?.publishDir ?? (await userAuth.resolveWorkingDir(user.userId));
|
||||
const linkExistsForRequest = resolveLinkExistsForWorkingDir(workingDir, linkExists);
|
||||
@@ -2389,6 +2412,25 @@ export function createWechatMpService({
|
||||
};
|
||||
}
|
||||
|
||||
const mediaAnalysisEnabled = isWechatMediaGrayUser(
|
||||
boundUser,
|
||||
config.mediaAnalysisGrayUsers,
|
||||
);
|
||||
|
||||
if (intent.msgType === 'file' && !mediaAnalysisEnabled) {
|
||||
await persistIntentDetail({ intent, userId: boundUser.userId, rawXmlHash });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
contentType: 'application/xml; charset=utf-8',
|
||||
body: buildWechatTextReply({
|
||||
toUserName: inbound.fromUserName,
|
||||
fromUserName: inbound.toUserName,
|
||||
content: '当前账号尚未开启服务号文件分析灰度,请先通过 H5 上传文件。',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (intent.msgType === 'voice' && !intent.agentText.trim() && intent.media?.mediaId) {
|
||||
try {
|
||||
const fallbackText = await transcribeWechatVoiceMedia(intent.media.mediaId, intent.media.format);
|
||||
|
||||
+40
-1
@@ -3702,6 +3702,7 @@ test('wechat mp service persists image and routes image url into agent prompt',
|
||||
unboundTextPrefix: '请先绑定',
|
||||
progressDelayMs: 0,
|
||||
maxImageBytes: 1024 * 1024,
|
||||
mediaAnalysisGrayUsers: [testUserId],
|
||||
},
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid() {
|
||||
@@ -3831,6 +3832,7 @@ test('wechat mp service persists Word and Excel files in user public area and re
|
||||
config: {
|
||||
maxFileBytes: 1024 * 1024,
|
||||
acceptFile: true,
|
||||
mediaAnalysisGrayUsers: [testUserId],
|
||||
},
|
||||
userAuth: {
|
||||
async findWechatUserByOpenid() {
|
||||
@@ -3939,7 +3941,7 @@ test('wechat mp service rejects unsupported public file types without entering t
|
||||
let mediaDownloads = 0;
|
||||
let sessionCalls = 0;
|
||||
const service = createBoundWechatService({
|
||||
config: { acceptFile: true },
|
||||
config: { acceptFile: true, mediaAnalysisGrayUsers: ['user-1'] },
|
||||
sessionApiFetch: async () => {
|
||||
sessionCalls += 1;
|
||||
throw new Error('session should not be called');
|
||||
@@ -3975,6 +3977,43 @@ test('wechat mp service rejects unsupported public file types without entering t
|
||||
assert.equal(sessionCalls, 0);
|
||||
});
|
||||
|
||||
test('wechat mp service keeps file analysis disabled outside the media gray allowlist', async () => {
|
||||
let mediaDownloads = 0;
|
||||
let sessionCalls = 0;
|
||||
const service = createBoundWechatService({
|
||||
config: {
|
||||
acceptFile: true,
|
||||
mediaAnalysisGrayUsers: ['唐'],
|
||||
},
|
||||
sessionApiFetch: async () => {
|
||||
sessionCalls += 1;
|
||||
throw new Error('session should not be called');
|
||||
},
|
||||
wechatFetch: async (url) => {
|
||||
if (String(url).includes('/cgi-bin/media/get')) mediaDownloads += 1;
|
||||
throw new Error(`unexpected wechat url: ${url}`);
|
||||
},
|
||||
});
|
||||
|
||||
const result = await service.handleInboundMessage(
|
||||
inboundXml({
|
||||
msgType: 'file',
|
||||
content: '',
|
||||
extraFields: { MediaId: 'file-media-gray', FileName: '测试.docx' },
|
||||
}),
|
||||
{
|
||||
timestamp: '1710000000',
|
||||
nonce: 'nonce',
|
||||
signature: signatureFor('token', '1710000000', 'nonce'),
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(result.status, 200);
|
||||
assert.match(result.body, /尚未开启服务号文件分析灰度/);
|
||||
assert.equal(mediaDownloads, 0);
|
||||
assert.equal(sessionCalls, 0);
|
||||
});
|
||||
|
||||
test('wechat mp service accepts full voice xml payload and routes recognition text into agent', async () => {
|
||||
const token = 'token';
|
||||
const timestamp = '1710000000';
|
||||
|
||||
Reference in New Issue
Block a user