Add smart ACK provider for WeChat MP replies
Replace fixed ackText with a rule-based AckProvider that picks response templates by message type and intent (translate, summary, rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync, zero I/O, auto-falls back to config.ackText on any error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,8 @@ const MAX_SUMMARY_LENGTH = 1000;
|
||||
const MAX_CONTENT_BYTES = 1024 * 1024;
|
||||
const TEMPLATE_IDS = new Set(['editorial', 'report', 'profile', 'knowledge-card', 'static-html']);
|
||||
const SAVE_CATEGORY_CODES = new Set(['draft', 'oa', 'private', 'public']);
|
||||
const PRIVATE_ASSET_URL_PATTERN =
|
||||
/(?:https?:\/\/[^/]+)?\/api\/mindspace\/v1\/assets\/([a-z0-9-]+)\/download(?:\?[^"'<>\\\s)]*)?/gi;
|
||||
|
||||
function asNumber(value) {
|
||||
return Number(value ?? 0);
|
||||
@@ -75,6 +77,16 @@ function normalizePageInput(input) {
|
||||
};
|
||||
}
|
||||
|
||||
function extensionForMime(mimeType, filename = '') {
|
||||
const ext = path.extname(String(filename ?? '')).replace(/^\./, '').toLowerCase();
|
||||
if (ext && /^[a-z0-9]{1,8}$/.test(ext)) return ext === 'jpeg' ? 'jpg' : ext;
|
||||
if (mimeType === 'image/jpeg') return 'jpg';
|
||||
if (mimeType === 'image/png') return 'png';
|
||||
if (mimeType === 'image/webp') return 'webp';
|
||||
if (mimeType === 'image/gif') return 'gif';
|
||||
return 'bin';
|
||||
}
|
||||
|
||||
function pageResponse(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
@@ -657,6 +669,113 @@ export function createPageService(pool, options = {}) {
|
||||
};
|
||||
};
|
||||
|
||||
const localizePrivateResources = async (userId, pageId, input = {}) => {
|
||||
const page = await getPage(userId, pageId);
|
||||
if (input.pageVersionId && input.pageVersionId !== page.currentVersionId) {
|
||||
throw pageError('只能基于当前版本修复页面', 'version_conflict', {
|
||||
currentVersion: page.currentVersionId,
|
||||
});
|
||||
}
|
||||
if (input.expectedVersion != null && asNumber(input.expectedVersion) !== asNumber(page.versionNo)) {
|
||||
throw pageError('页面已被更新,请刷新后重试', 'version_conflict', {
|
||||
currentVersion: asNumber(page.versionNo),
|
||||
});
|
||||
}
|
||||
if (page.contentFormat !== 'html') {
|
||||
throw pageError('该页面不支持资源本地化修复', 'unsupported_publish_fix');
|
||||
}
|
||||
|
||||
const sourceTitle = input.title ?? page.title;
|
||||
const sourceSummary = input.summary ?? page.summary ?? '';
|
||||
const sourceContent = input.content ?? page.content ?? '';
|
||||
const matches = [...String(sourceContent).matchAll(PRIVATE_ASSET_URL_PATTERN)];
|
||||
if (matches.length === 0) {
|
||||
throw pageError('未发现可安全修复的私有图片引用', 'publish_fix_not_needed');
|
||||
}
|
||||
|
||||
const assetIds = [...new Set(matches.map((match) => match[1]).filter(Boolean))];
|
||||
const [assets] = await pool.query(
|
||||
`SELECT a.id, a.mime_type, a.original_filename, v.storage_key
|
||||
FROM h5_assets a
|
||||
JOIN h5_asset_versions v ON v.id = a.current_version_id
|
||||
WHERE a.user_id = ? AND a.id IN (?) AND a.mime_type LIKE 'image/%'
|
||||
AND a.status <> 'deleted'`,
|
||||
[userId, assetIds],
|
||||
);
|
||||
const byId = new Map(assets.map((asset) => [asset.id, asset]));
|
||||
const replacements = new Map();
|
||||
for (const assetId of assetIds) {
|
||||
const asset = byId.get(assetId);
|
||||
if (!asset) continue;
|
||||
const buffer = await fs.readFile(absoluteStoragePath(asset.storage_key));
|
||||
const mimeType = String(asset.mime_type || 'application/octet-stream');
|
||||
const ext = extensionForMime(mimeType, asset.original_filename);
|
||||
const dataUri = `data:${mimeType};base64,${buffer.toString('base64')}`;
|
||||
replacements.set(assetId, { dataUri, ext });
|
||||
}
|
||||
|
||||
if (replacements.size === 0) {
|
||||
throw pageError('私有资源不是可内联的图片,无法自动修复', 'unsupported_publish_fix');
|
||||
}
|
||||
|
||||
let changedCount = 0;
|
||||
const updatedContent = String(sourceContent).replace(
|
||||
PRIVATE_ASSET_URL_PATTERN,
|
||||
(value, assetId) => {
|
||||
const replacement = replacements.get(assetId);
|
||||
if (!replacement) return value;
|
||||
changedCount += 1;
|
||||
return replacement.dataUri;
|
||||
},
|
||||
);
|
||||
|
||||
if (updatedContent === sourceContent || changedCount === 0) {
|
||||
throw pageError('未发现可安全修复的私有图片引用', 'publish_fix_not_needed');
|
||||
}
|
||||
|
||||
const updated = await createVersion(
|
||||
userId,
|
||||
{
|
||||
pageId,
|
||||
expectedVersion: page.versionNo,
|
||||
title: sourceTitle,
|
||||
summary: sourceSummary,
|
||||
content: updatedContent,
|
||||
templateId: page.templateId,
|
||||
pageType: page.pageType,
|
||||
contentFormat: 'html',
|
||||
changeNote: `一键发布修复 v${page.versionNo + 1}`,
|
||||
},
|
||||
{
|
||||
type: 'generated',
|
||||
snapshot: {
|
||||
localized_private_resources: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
page: updated,
|
||||
originalScan: scanContent(`${sourceTitle}\n${sourceSummary}\n${sourceContent}`, {
|
||||
format: 'html',
|
||||
}),
|
||||
redactedScan: scanContent(
|
||||
`${updated.title}\n${updated.summary ?? ''}\n${updated.content ?? ''}`,
|
||||
{ format: 'html' },
|
||||
),
|
||||
redactionsApplied: changedCount,
|
||||
changes: [
|
||||
{
|
||||
field: 'content',
|
||||
fieldLabel: '正文',
|
||||
type: 'private_resource_reference',
|
||||
label: '私有资源引用',
|
||||
count: changedCount,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const collectLinkedAssets = async (conn, userId, pageId) => {
|
||||
const [versions] = await conn.query(
|
||||
`SELECT pv.content_asset_id, pv.bundle_asset_id
|
||||
@@ -1024,6 +1143,7 @@ export function createPageService(pool, options = {}) {
|
||||
createPage: (userId, input) => createVersion(userId, input, { type: 'template' }),
|
||||
updatePage: (userId, pageId, input) =>
|
||||
createVersion(userId, { ...input, pageId }, { type: 'generated' }),
|
||||
localizePrivateResources,
|
||||
redactPage,
|
||||
createRedactedCopy: redactPage,
|
||||
listPages,
|
||||
|
||||
Reference in New Issue
Block a user