Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c6ae7cd21c | |||
| 1765cac65a | |||
| efbbe69e2c |
+12
-2
@@ -131,6 +131,7 @@ export async function uploadWechatGeneratedImage(
|
||||
{
|
||||
wechatFetch = undiciFetch,
|
||||
publicBaseUrl = '',
|
||||
allowedPublicBaseUrls = [],
|
||||
uploadUrl = DEFAULT_WECHAT_MEDIA_UPLOAD_URL,
|
||||
maxBytes = DEFAULT_MAX_OUTBOUND_IMAGE_BYTES,
|
||||
} = {},
|
||||
@@ -138,8 +139,17 @@ export async function uploadWechatGeneratedImage(
|
||||
if (!accessToken) throw new Error('缺少微信 access_token');
|
||||
if (!publicUrl) throw new Error('缺少生成图片公网地址');
|
||||
const resolvedUrl = new URL(String(publicUrl), publicBaseUrl || undefined).toString();
|
||||
if (publicBaseUrl && new URL(resolvedUrl).origin !== new URL(publicBaseUrl).origin) {
|
||||
throw new Error('生成图片地址不属于当前 MindSpace 公网域名');
|
||||
const allowedOrigins = new Set();
|
||||
for (const baseUrl of [publicBaseUrl, ...allowedPublicBaseUrls]) {
|
||||
if (!baseUrl) continue;
|
||||
try {
|
||||
allowedOrigins.add(new URL(String(baseUrl)).origin);
|
||||
} catch {
|
||||
// Ignore invalid optional bases; at least one valid configured origin is required below.
|
||||
}
|
||||
}
|
||||
if (allowedOrigins.size > 0 && !allowedOrigins.has(new URL(resolvedUrl).origin)) {
|
||||
throw new Error('生成图片地址不属于当前 MindSpace 可信公网域名');
|
||||
}
|
||||
const sourceResponse = await wechatFetch(resolvedUrl, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -36,3 +36,38 @@ test('uploadWechatGeneratedImage converts a generated asset and uploads WeChat i
|
||||
assert.match(calls[1].url, /access_token=access-1/);
|
||||
assert.match(calls[1].url, /type=image/);
|
||||
});
|
||||
|
||||
test('uploadWechatGeneratedImage accepts configured imgproxy origin and rejects unknown origins', async () => {
|
||||
const source = await sharp({
|
||||
create: { width: 16, height: 16, channels: 3, background: '#884422' },
|
||||
}).png().toBuffer();
|
||||
const wechatFetch = async (url) => {
|
||||
if (String(url).startsWith('https://img.example.com/')) {
|
||||
return new Response(source, { status: 200, headers: { 'Content-Type': 'image/png' } });
|
||||
}
|
||||
return new Response(JSON.stringify({ type: 'image', media_id: 'wx-media-imgproxy' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
};
|
||||
|
||||
const accepted = await uploadWechatGeneratedImage(
|
||||
'access-2',
|
||||
'https://img.example.com/signed/generated.webp',
|
||||
{
|
||||
publicBaseUrl: 'https://app.example.com',
|
||||
allowedPublicBaseUrls: ['https://img.example.com'],
|
||||
wechatFetch,
|
||||
},
|
||||
);
|
||||
assert.equal(accepted.mediaId, 'wx-media-imgproxy');
|
||||
|
||||
await assert.rejects(
|
||||
uploadWechatGeneratedImage('access-2', 'https://untrusted.example.net/generated.webp', {
|
||||
publicBaseUrl: 'https://app.example.com',
|
||||
allowedPublicBaseUrls: ['https://img.example.com'],
|
||||
wechatFetch,
|
||||
}),
|
||||
/可信公网域名/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -36,6 +36,11 @@ export function loadWechatMpConfig(env = process.env) {
|
||||
env.H5_WECHAT_MP_APP_SECRET?.trim() ?? env.H5_WECHAT_APP_SECRET?.trim() ?? '';
|
||||
const token = env.H5_WECHAT_MP_TOKEN?.trim() ?? '';
|
||||
const publicBaseUrl = env.H5_PUBLIC_BASE_URL?.trim()?.replace(/\/$/, '') ?? '';
|
||||
const generatedImagePublicBaseUrls = [...new Set([
|
||||
publicBaseUrl,
|
||||
env.IMGPROXY_BASE_URL?.trim()?.replace(/\/$/, '') ?? '',
|
||||
...parseCsvList(env.H5_WECHAT_MP_GENERATED_IMAGE_BASE_URLS).map((value) => value.replace(/\/$/, '')),
|
||||
].filter(Boolean))];
|
||||
const enabledFlag = env.H5_WECHAT_MP_ENABLED === '1';
|
||||
const bindPath = env.H5_WECHAT_MP_BIND_PATH?.trim() || '/auth/wechat/authorize?intent=login';
|
||||
return {
|
||||
@@ -73,6 +78,7 @@ export function loadWechatMpConfig(env = process.env) {
|
||||
DEFAULT_WECHAT_JSAPI_TICKET_URL,
|
||||
mediaPublicBaseUrl:
|
||||
env.H5_WECHAT_MP_MEDIA_PUBLIC_BASE_URL?.trim()?.replace(/\/$/, '') || publicBaseUrl,
|
||||
generatedImagePublicBaseUrls,
|
||||
maxImageBytes: Math.max(1, Number(env.H5_WECHAT_MP_MAX_IMAGE_BYTES ?? 10 * 1024 * 1024)),
|
||||
maxFileBytes: Math.max(1, Number(env.H5_WECHAT_MP_MAX_FILE_BYTES ?? 30 * 1024 * 1024)),
|
||||
acceptVoice: env.H5_WECHAT_MP_ACCEPT_VOICE !== '0',
|
||||
|
||||
@@ -1801,6 +1801,7 @@ export function createWechatMpService({
|
||||
const uploaded = await uploadWechatGeneratedImage(accessToken, publicUrl, {
|
||||
wechatFetch,
|
||||
publicBaseUrl: config.publicBaseUrl,
|
||||
allowedPublicBaseUrls: config.generatedImagePublicBaseUrls,
|
||||
});
|
||||
const payload = await readJsonResponse(
|
||||
await wechatFetch(
|
||||
|
||||
@@ -859,10 +859,15 @@ test('loadWechatMpConfig requires full config and enable flag', () => {
|
||||
H5_WECHAT_MP_APP_SECRET: 'secret',
|
||||
H5_WECHAT_MP_TOKEN: 'token',
|
||||
H5_PUBLIC_BASE_URL: 'https://example.com',
|
||||
IMGPROXY_BASE_URL: 'https://img.example.com',
|
||||
});
|
||||
assert.equal(config.enabled, true);
|
||||
assert.equal(config.bindPath, '/auth/wechat/authorize?intent=login');
|
||||
assert.equal(config.requireFreshPageThumbnail, true);
|
||||
assert.deepEqual(config.generatedImagePublicBaseUrls, [
|
||||
'https://example.com',
|
||||
'https://img.example.com',
|
||||
]);
|
||||
assert.equal(loadWechatMpConfig({ H5_WECHAT_MP_FRESH_PAGE_THUMBNAILS: '0' }).requireFreshPageThumbnail, false);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
resolveWechatImageGenerationPolicy,
|
||||
WECHAT_PAGE_THUMBNAIL_MODE,
|
||||
} from './image-generation-policy.mjs';
|
||||
import { classifyWechatIntent } from './intent/classifier.mjs';
|
||||
|
||||
test('service-account page always requires a fresh thumbnail without forcing body images', () => {
|
||||
const policy = resolveWechatImageGenerationPolicy({
|
||||
@@ -47,3 +48,17 @@ test('standalone image intent gets an inline_image tool contract', () => {
|
||||
assert.match(instruction, /purpose=`inline_image`/);
|
||||
assert.match(instruction, /idempotency_key=wechat-msg-1-image/);
|
||||
});
|
||||
|
||||
test('explicitly declining a page keeps standalone image generation out of page flow', () => {
|
||||
const text = '请生成一张雨夜橘猫插画,只生成图片,不要生成页面';
|
||||
const intent = classifyWechatIntent({ msgType: 'text', agentText: text });
|
||||
assert.equal(intent.kind, 'chat.general');
|
||||
|
||||
const policy = resolveWechatImageGenerationPolicy({
|
||||
text,
|
||||
isPageGenerate: intent.kind === 'page.generate',
|
||||
requireFreshPageThumbnail: true,
|
||||
});
|
||||
assert.equal(policy.pageThumbnailMode, 'auto');
|
||||
assert.equal(policy.standaloneImageMode, 'required');
|
||||
});
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
export const PAGE_GENERATE_PATTERN =
|
||||
/(?:生成|创建|做|写|帮我.*(?:生成|创建|做|写)).*(?:html|页面|网页|page|文件)/iu;
|
||||
|
||||
export const PAGE_GENERATE_NEGATION_PATTERN =
|
||||
/(?:不要|无需|不用|别|不需要)\s*(?:再\s*)?(?:生成|创建|制作|做|写)\s*(?:任何|一个|新的)?\s*(?:html|页面|网页|page|文件)/iu;
|
||||
|
||||
export const DOCX_DOWNLOAD_PATTERN =
|
||||
/(?:(?:word|docx|\.docx|\.doc|文档).*(?:下载|链接|导出|给我)|(?:下载|导出|提供|给我).*(?:word|docx|\.docx|\.doc|文档))/iu;
|
||||
|
||||
@@ -21,6 +24,7 @@ export const CONNECTIVITY_TEST_PATTERN = /^(测试\s*\d*|test\s*\d*)[!!。.\s]
|
||||
export function isPageGenerateText(text) {
|
||||
const normalized = String(text ?? '').trim();
|
||||
if (!normalized) return false;
|
||||
if (PAGE_GENERATE_NEGATION_PATTERN.test(normalized)) return false;
|
||||
return PAGE_GENERATE_PATTERN.test(normalized);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user