mindspace: close authority boundaries

This commit is contained in:
john
2026-07-27 15:34:35 +08:00
parent dfab78c75a
commit e94052ff24
78 changed files with 11962 additions and 2162 deletions
+332 -405
View File
@@ -1,18 +1,11 @@
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { fetch as undiciFetch } from 'undici';
import { developerToolsFromPolicy } from './capabilities.mjs';
import { mergeMessageContent } from './message-stream.mjs';
import { reconcileAgentSession } from './session-reconcile.mjs';
import { resolveSessionAccess } from './session-broker.mjs';
import {
extractPublicHtmlWriteArtifacts,
isStubPublicHtmlContent,
materializeMissingPublicHtmlWrites,
} from './mindspace-public-finish-sync.mjs';
import { loadWechatMpConfig } from './wechat-mp-config.mjs';
import { buildPublicUrl, PUBLISH_ROOT_DIR } from './user-publish.mjs';
import {
downloadTemporaryMedia,
persistWechatAttachment,
@@ -46,22 +39,17 @@ import {
buildPageGenerateAgentPrompt,
buildPagePublishFailureText,
} from './wechat/prompts/page-generate.mjs';
import { selectSendableHtmlArtifacts } from './wechat/verify/page-artifact.mjs';
import {
artifactFileExists,
isStubPublicHtmlArtifact,
selectSendableHtmlArtifacts,
} from './wechat/verify/page-artifact.mjs';
import {
collectWechatGeneratedImages,
repairUnambiguousFreshWechatPageThumbnail,
summarizeFreshWechatThumbnailVerification,
verifyFreshWechatPageThumbnails,
} from './wechat/verify/generated-thumbnail.mjs';
import { resolveBillingTokenState } from './billing-token-state.mjs';
import { ensureWorkspaceHtmlThumbnail } from './mindspace-workspace-thumbnails.mjs';
import {
buildPageDataCollectFailureText,
buildPageDataDeliveryArtifactsFromBindResult,
ensurePageDataDeliveryReady,
maybeAutoBindPageDataHtmlPages,
resolvePageDataCollectOutcomeAsync,
rewritePageDataDeliveryLinks,
} from './mindspace-page-data-finish-guard.mjs';
export { buildWechatAgentPrompt };
@@ -538,71 +526,6 @@ function isMissingRequiredPublishSkill(reply, intent) {
return !usedStaticPagePublishSkill(replyRequestMessages(reply));
}
function buildPublicHtmlUrl(workingDir, relativePath, publicBaseUrl) {
const owner = path.basename(path.resolve(workingDir));
const normalized = relativePath.split(path.sep).map(encodeURIComponent).join('/');
return buildPublicUrl(publicBaseUrl, owner, normalized);
}
function ensurePublicHtmlArtifact(htmlPath, workingDir) {
const workspaceRoot = path.resolve(workingDir);
const source = path.isAbsolute(String(htmlPath ?? ''))
? path.resolve(String(htmlPath))
: path.resolve(workspaceRoot, String(htmlPath ?? ''));
if (source !== workspaceRoot && !source.startsWith(`${workspaceRoot}${path.sep}`)) return null;
if (!fs.existsSync(source) || !fs.statSync(source).isFile()) return null;
const publicRoot = path.join(workspaceRoot, 'public');
let publishedPath = source;
if (source !== publicRoot && !source.startsWith(`${publicRoot}${path.sep}`)) {
fs.mkdirSync(publicRoot, { recursive: true });
publishedPath = path.join(publicRoot, path.basename(source));
if (publishedPath !== source) fs.copyFileSync(source, publishedPath);
}
const relativePath = path.relative(workspaceRoot, publishedPath);
if (!relativePath || relativePath.startsWith('..')) return null;
return {
localPath: publishedPath,
relativePath,
};
}
function collectPublishedHtmlArtifacts(reply, { workingDir, publicBaseUrl }) {
const artifacts = [];
const htmlTargets = extractHtmlWriteTargets(replyRequestMessages(reply));
for (const target of htmlTargets) {
const artifact = ensurePublicHtmlArtifact(target, workingDir);
if (!artifact) continue;
artifacts.push({
...artifact,
url: buildPublicHtmlUrl(workingDir, artifact.relativePath, publicBaseUrl),
});
}
return artifacts;
}
function extractRequestedHtmlTarget(text) {
const value = String(text ?? '');
if (!value) return '';
const explicitPublicMatch = value.match(/(?:^|[\s`'"])(public\/[a-z0-9._-]+\.html)\b/i);
if (explicitPublicMatch?.[1]) return explicitPublicMatch[1];
const bareMatch = value.match(/(?:^|[\s`'"])([a-z0-9._-]+\.html)\b/i);
if (bareMatch?.[1]) return `public/${bareMatch[1]}`;
return '';
}
function collectExpectedHtmlArtifacts(intent, { workingDir, publicBaseUrl }) {
const target = extractRequestedHtmlTarget(intent?.agentText ?? intent?.content ?? '');
if (!target) return [];
const artifact = ensurePublicHtmlArtifact(target, workingDir);
if (!artifact) return [];
return [{
...artifact,
url: buildPublicHtmlUrl(workingDir, artifact.relativePath, publicBaseUrl),
}];
}
function sanitizeFilenameSlug(value) {
const normalized = String(value ?? '')
.toLowerCase()
@@ -629,66 +552,6 @@ function collectPublicHtmlLinkFilenames(text) {
return filenames;
}
function collectRecentPublishedHtmlArtifacts(
intent,
{ workingDir, publicBaseUrl, replyText = '', sinceMs = 0, limit = 20 } = {},
) {
const publicRoot = path.join(path.resolve(workingDir), 'public');
if (!fs.existsSync(publicRoot) || !fs.statSync(publicRoot).isDirectory()) return [];
const expectedTarget = extractRequestedHtmlTarget(intent?.agentText ?? intent?.content ?? '');
const expectedRelativePath = expectedTarget ? expectedTarget.replace(/^\/+/, '').replace(/\\/g, '/') : '';
const linkedFilenames = collectPublicHtmlLinkFilenames(replyText);
const artifacts = [];
const stack = [publicRoot];
while (stack.length > 0 && artifacts.length < limit) {
const current = stack.pop();
let entries = [];
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
const absolutePath = path.join(current, entry.name);
if (entry.isDirectory()) {
stack.push(absolutePath);
continue;
}
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.html')) continue;
let stat;
try {
stat = fs.statSync(absolutePath);
} catch {
continue;
}
if (sinceMs > 0 && Number(stat.mtimeMs ?? 0) + 1 < sinceMs) continue;
const relativePath = path.relative(path.resolve(workingDir), absolutePath);
if (!relativePath || relativePath.startsWith('..')) continue;
const normalizedRelativePath = relativePath.replace(/\\/g, '/');
const filename = path.posix.basename(normalizedRelativePath);
if (linkedFilenames.size > 0 && filename && !linkedFilenames.has(filename)) continue;
artifacts.push({
localPath: absolutePath,
relativePath: normalizedRelativePath,
url: buildPublicHtmlUrl(workingDir, normalizedRelativePath, publicBaseUrl),
mtimeMs: Number(stat.mtimeMs ?? 0),
matchesExpected: expectedRelativePath
? normalizedRelativePath === expectedRelativePath
: false,
});
if (artifacts.length >= limit) break;
}
}
return artifacts
.sort((left, right) => {
if (left.matchesExpected !== right.matchesExpected) return left.matchesExpected ? -1 : 1;
return right.mtimeMs - left.mtimeMs;
})
.map(({ mtimeMs, matchesExpected, ...artifact }) => artifact);
}
function rewritePublishedHtmlLinks(text, artifacts = []) {
const value = String(text ?? '');
if (!value || artifacts.length === 0) return value;
@@ -719,16 +582,14 @@ function rewritePublishedHtmlLinks(text, artifacts = []) {
async function maybeAttachPublishedHtmlLink(
reply,
{
workingDir,
publicBaseUrl,
artifacts: providedArtifacts = null,
artifacts: providedArtifacts = [],
allowAttachment = true,
},
) {
if (!allowAttachment) return String(reply?.text ?? '').trim();
const artifacts = Array.isArray(providedArtifacts)
? providedArtifacts
: collectPublishedHtmlArtifacts(reply, { workingDir, publicBaseUrl });
: [];
const baseText = rewritePublishedHtmlLinks(String(reply?.text ?? '').trim(), artifacts).trim();
for (const artifact of artifacts) {
const url = artifact.url;
@@ -826,65 +687,73 @@ function formatWechatOutboundText(text, user = null) {
}
function defaultPublicHtmlLinkExists(urlText) {
let url;
try {
url = new URL(urlText);
const url = new URL(urlText);
return !PUBLIC_HTML_LINK_PATTERN.test(
url.toString(),
);
} catch {
return true;
} finally {
PUBLIC_HTML_LINK_PATTERN.lastIndex = 0;
}
const parts = url.pathname.split('/').filter(Boolean).map((part) => {
try {
return decodeURIComponent(part);
} catch {
return part;
}
});
if (parts.length < 4 || parts[0] !== PUBLISH_ROOT_DIR || parts[2] !== 'public') return true;
const owner = parts[1];
const rest = parts.slice(3);
if (!owner || rest.some((part) => !part || part === '.' || part === '..')) return false;
const root = path.resolve(process.cwd(), PUBLISH_ROOT_DIR, owner, 'public');
const target = path.resolve(root, ...rest);
if (target !== root && !target.startsWith(`${root}${path.sep}`)) return false;
return fs.existsSync(target) && fs.statSync(target).isFile();
}
export function createPublicHtmlLinkExists(workingDir) {
const workspaceRoot = path.resolve(String(workingDir ?? ''));
const workspaceKey = path.basename(workspaceRoot);
return (urlText) => {
let url;
try {
url = new URL(urlText);
} catch {
return true;
function publicHtmlLinkOwner(urlText) {
try {
const url = new URL(urlText);
const parts = url.pathname
.split('/')
.filter(Boolean)
.map((part) => {
try {
return decodeURIComponent(part);
} catch {
return part;
}
});
if (
parts.length < 4 ||
parts[0] !== 'MindSpace' ||
parts[2] !== 'public'
) {
return null;
}
const parts = url.pathname.split('/').filter(Boolean).map((part) => {
try {
return decodeURIComponent(part);
} catch {
return part;
}
});
if (parts.length < 4 || parts[0] !== PUBLISH_ROOT_DIR || parts[2] !== 'public') return true;
const owner = parts[1];
const rest = parts.slice(3);
if (!owner || rest.some((part) => !part || part === '.' || part === '..')) return false;
if (owner === workspaceKey) {
const root = path.join(workspaceRoot, 'public');
const target = path.resolve(root, ...rest);
if (target !== root && !target.startsWith(`${root}${path.sep}`)) return false;
return fs.existsSync(target) && fs.statSync(target).isFile();
return parts[1] || null;
} catch {
return null;
}
}
function createPreparedPublicHtmlLinkExists({
userId,
prepared,
fallback = defaultPublicHtmlLinkExists,
}) {
const validUrls = new Set([
...(prepared?.validReplyUrls ?? []),
...(prepared?.confirmedArtifacts ?? [])
.flatMap((artifact) => [
artifact?.url,
artifact?.canonicalUrl,
]),
].map((value) => String(value ?? '').trim())
.filter(Boolean));
const normalizedUserId =
String(userId ?? '').trim();
return async (urlText) => {
const normalized =
String(urlText ?? '').trim();
if (validUrls.has(normalized)) return true;
const owner =
publicHtmlLinkOwner(normalized);
if (owner && owner === normalizedUserId) {
return false;
}
return defaultPublicHtmlLinkExists(urlText);
return fallback(normalized);
};
}
function resolveLinkExistsForWorkingDir(workingDir, linkExists = defaultPublicHtmlLinkExists) {
if (linkExists !== defaultPublicHtmlLinkExists) return linkExists;
return createPublicHtmlLinkExists(workingDir);
}
export async function guardMissingPublicHtmlLinks(
text,
{ linkExists = defaultPublicHtmlLinkExists } = {},
@@ -937,16 +806,6 @@ export function isHtmlPublishFailureMessage(message) {
export { maybeAttachPublishedHtmlLink };
function artifactFileExists(artifact) {
const localPath = String(artifact?.localPath ?? '').trim();
if (!localPath) return false;
try {
return fs.existsSync(localPath) && fs.statSync(localPath).isFile();
} catch {
return false;
}
}
function uniqueArtifactsByUrl(artifacts = []) {
const byUrl = new Map();
for (const artifact of artifacts) {
@@ -956,74 +815,96 @@ function uniqueArtifactsByUrl(artifacts = []) {
return [...byUrl.values()];
}
function allExistingHtmlArtifacts({ publishedArtifacts = [], expectedArtifacts = [], recentArtifacts = [] } = {}) {
return uniqueArtifactsByUrl([
...publishedArtifacts,
...expectedArtifacts,
...recentArtifacts,
]).filter((artifact) => artifactFileExists(artifact));
}
function buildVerifiedHtmlArtifacts({
publishedArtifacts = [],
expectedArtifacts = [],
recentArtifacts = [],
replyText = '',
} = {}) {
const existing = allExistingHtmlArtifacts({ publishedArtifacts, expectedArtifacts, recentArtifacts });
const linkedFilenames = collectPublicHtmlLinkFilenames(replyText);
if (linkedFilenames.size === 0) return existing;
const matched = existing.filter((artifact) => {
const filename = path.posix.basename(String(artifact.relativePath ?? '').replace(/\\/g, '/'));
return filename && linkedFilenames.has(filename);
});
return matched;
}
function resolveHtmlPublishArtifacts({
async function resolveHtmlPublishArtifacts({
reply,
intent,
workingDir,
publicBaseUrl,
requestStartedAt,
userId = '',
sessionId = '',
onPageGenerated = null,
allowRecentArtifacts = true,
htmlDeliveryAuthority = null,
}) {
const artifactMessages = reply?.requestMessages ?? reply?.messages ?? [];
const artifactReply = { ...reply, messages: artifactMessages };
materializeMissingPublicHtmlWrites({
messages: artifactMessages,
publishDir: workingDir,
const prepareDelivery =
htmlDeliveryAuthority
?.prepareWechatHtmlDelivery;
const deliveryRequired =
allowRecentArtifacts === true ||
looksLikeHtmlGenerationIntent(
intent?.agentText,
) ||
isPageDataIntent(intent?.agentText) ||
hasAnyPublicHtmlLink(reply?.text) ||
extractHtmlWriteTargets(
replyRequestMessages(reply),
).length > 0;
if (
typeof prepareDelivery !== 'function'
) {
if (!deliveryRequired) {
return {
publishedArtifacts: [],
expectedArtifacts: [],
recentArtifacts: [],
confirmedArtifacts: [],
verifiedArtifacts: [],
validReplyUrls: [],
hasValidReplyLink: false,
};
}
throw Object.assign(
new Error(
'MindSpace WeChat HTML delivery authority is unavailable',
),
{
code:
'MINDSPACE_WECHAT_HTML_AUTHORITY_UNAVAILABLE',
},
);
}
const prepared = await prepareDelivery({
userId,
sessionId,
reply: {
text: String(reply?.text ?? ''),
messages: replyRequestMessages(reply),
},
intent: {
agentText: String(
intent?.agentText ?? '',
),
displayText: String(
intent?.displayText ?? '',
),
},
requestStartedAt,
allowRecentArtifacts,
});
const publishedArtifacts = collectPublishedHtmlArtifacts(artifactReply, {
workingDir,
publicBaseUrl,
});
const expectedArtifacts = collectExpectedHtmlArtifacts(intent, {
workingDir,
publicBaseUrl,
});
const recentArtifacts = allowRecentArtifacts
? collectRecentPublishedHtmlArtifacts(intent, {
workingDir,
publicBaseUrl,
replyText: reply?.text,
sinceMs: requestStartedAt,
})
const publishedArtifacts = Array.isArray(
prepared?.publishedArtifacts,
)
? prepared.publishedArtifacts
: [];
const expectedArtifacts = Array.isArray(
prepared?.expectedArtifacts,
)
? prepared.expectedArtifacts
: [];
const recentArtifacts = Array.isArray(
prepared?.recentArtifacts,
)
? prepared.recentArtifacts
: [];
const confirmedArtifacts = Array.isArray(
prepared?.confirmedArtifacts,
)
? prepared.confirmedArtifacts
: [];
const verifiedArtifacts = Array.isArray(
prepared?.verifiedArtifacts,
)
? prepared.verifiedArtifacts
: [];
const confirmedArtifacts = allExistingHtmlArtifacts({
publishedArtifacts,
expectedArtifacts,
recentArtifacts,
});
const verifiedArtifacts = buildVerifiedHtmlArtifacts({
publishedArtifacts,
expectedArtifacts,
recentArtifacts,
replyText: reply?.text,
});
if (
typeof onPageGenerated === 'function'
&& confirmedArtifacts.length > 0
@@ -1037,23 +918,16 @@ function resolveHtmlPublishArtifacts({
recentArtifacts,
confirmedArtifacts,
verifiedArtifacts,
validReplyUrls: Array.isArray(
prepared?.validReplyUrls,
)
? prepared.validReplyUrls
: [],
hasValidReplyLink:
prepared?.hasValidReplyLink === true,
};
}
function selectHtmlPublishArtifacts({ verifiedArtifacts = [], confirmedArtifacts = [] } = {}) {
return selectSendableHtmlArtifacts({ verifiedArtifacts, confirmedArtifacts });
}
function isStubPublicHtmlArtifact(artifact) {
const localPath = String(artifact?.localPath ?? '').trim();
if (!localPath) return false;
try {
return isStubPublicHtmlContent(fs.readFileSync(localPath, 'utf8'));
} catch {
return false;
}
}
function nonStubHtmlArtifacts(artifacts = []) {
return artifacts.filter((artifact) => artifactFileExists(artifact) && !isStubPublicHtmlArtifact(artifact));
}
@@ -1197,52 +1071,55 @@ function markWechatUserNotified(err) {
async function enforcePageDataCollectDelivery({
reply,
intent,
workingDir,
userId,
pageDataFinishGuard,
publicBaseUrl,
requestStartedAt = 0,
notifyFailure,
}) {
let outcome = await resolvePageDataCollectOutcomeAsync({
reply,
intent,
publishDir: workingDir,
requestStartedAt,
pool: pageDataFinishGuard?.pool ?? null,
userId,
findPageByRelativePath: null,
apiBase: publicBaseUrl,
});
let autoBind = null;
if (outcome.action === 'skip') return outcome;
if (pageDataFinishGuard?.pool) {
const { createPageService } = await import('./mindspace-pages.mjs');
const pageService = createPageService(pageDataFinishGuard.pool, {
h5Root: pageDataFinishGuard.h5Root,
storageRoot: pageDataFinishGuard.storageRoot,
});
autoBind = await maybeAutoBindPageDataHtmlPages({
pool: pageDataFinishGuard.pool,
userId,
publishDir: workingDir,
h5Root: pageDataFinishGuard.h5Root,
storageRoot: pageDataFinishGuard.storageRoot,
findPageByRelativePath: pageService.findPageByRelativePath.bind(pageService),
});
outcome = await resolvePageDataCollectOutcomeAsync({
reply,
intent,
publishDir: workingDir,
requestStartedAt,
pool: pageDataFinishGuard.pool,
userId,
findPageByRelativePath: pageService.findPageByRelativePath.bind(pageService),
apiBase: publicBaseUrl,
});
const prepareDelivery =
pageDataFinishGuard
?.prepareWechatPageDataDelivery;
if (typeof prepareDelivery !== 'function') {
const agentText = String(
intent?.agentText ??
intent?.displayText ??
'',
);
if (!isPageDataIntent(agentText)) {
return { action: 'skip' };
}
const text = buildPageDataCollectFailureText();
if (typeof notifyFailure === 'function') {
await notifyFailure(text);
}
throw markWechatUserNotified(new Error(text));
}
const prepared = await prepareDelivery({
userId,
reply: {
text: String(reply?.text ?? ''),
messages: Array.isArray(reply?.messages)
? reply.messages
: [],
},
intent: {
agentText: String(intent?.agentText ?? ''),
displayText: String(
intent?.displayText ?? '',
),
},
publicBaseUrl,
requestStartedAt,
});
const outcome = prepared?.outcome ?? {
action: 'fail',
failureText:
buildPageDataCollectFailureText(),
};
const autoBind = prepared?.autoBind ?? null;
if (outcome.action === 'skip') return outcome;
if (outcome.action === 'retry') {
throw new Error('stale_session_poisoned_completion');
}
@@ -1254,34 +1131,27 @@ async function enforcePageDataCollectDelivery({
throw markWechatUserNotified(new Error(text));
}
const deliveryArtifacts = buildPageDataDeliveryArtifactsFromBindResult(autoBind, workingDir, {
publicBaseUrl,
});
if (deliveryArtifacts.length > 0 && pageDataFinishGuard?.pool) {
const { createPageService } = await import('./mindspace-pages.mjs');
const pageService = createPageService(pageDataFinishGuard.pool, {
h5Root: pageDataFinishGuard.h5Root,
storageRoot: pageDataFinishGuard.storageRoot,
});
const deliveryCheck = await ensurePageDataDeliveryReady({
publishDir: workingDir,
userId,
pool: pageDataFinishGuard.pool,
h5Root: pageDataFinishGuard.h5Root,
storageRoot: pageDataFinishGuard.storageRoot,
apiBase: publicBaseUrl,
artifacts: deliveryArtifacts,
});
if (!deliveryCheck.ok) {
const deliveryArtifacts = Array.isArray(
prepared?.deliveryArtifacts,
)
? prepared.deliveryArtifacts
: [];
if (
prepared?.deliveryCheck &&
!prepared.deliveryCheck.ok
) {
const text = buildPageDataCollectFailureText();
if (typeof notifyFailure === 'function') {
await notifyFailure(text);
}
throw markWechatUserNotified(new Error(text));
}
}
if (deliveryArtifacts.length > 0 && reply && typeof reply.text === 'string') {
reply.text = rewritePageDataDeliveryLinks(reply.text, deliveryArtifacts);
if (
deliveryArtifacts.length > 0 &&
reply &&
typeof prepared?.rewrittenText === 'string'
) {
reply.text = prepared.rewrittenText;
}
return { ...outcome, deliveryArtifacts, autoBind };
}
@@ -1585,6 +1455,7 @@ export function createWechatMpService({
applySessionLlmProvider = null,
refreshSessionSnapshot = null,
sessionIntentClassifier = null,
htmlDeliveryAuthority = null,
pageDataFinishGuard = null,
wechatFetch = undiciFetch,
linkExists = defaultPublicHtmlLinkExists,
@@ -1932,62 +1803,74 @@ export function createWechatMpService({
reply,
openid,
user,
userId,
sessionId,
imagePolicy,
publishDir,
notifyFailure = true,
}) => {
if (imagePolicy?.pageThumbnailMode !== WECHAT_PAGE_THUMBNAIL_MODE.REQUIRED_FRESH) return;
const images = collectWechatGeneratedImages(replyRequestMessages(reply));
let verification = verifyFreshWechatPageThumbnails(artifacts, images);
if (!verification.ok) {
logger.warn?.(
'WeChat MP fresh thumbnail verification failed:',
summarizeFreshWechatThumbnailVerification(verification, images),
);
if (config.repairFreshPageThumbnail) {
const repair = repairUnambiguousFreshWechatPageThumbnail({
const ensureFresh =
htmlDeliveryAuthority
?.ensureWechatFreshPageThumbnails;
let verification;
if (typeof ensureFresh !== 'function') {
verification = {
ok: false,
reason:
'mindspace_thumbnail_authority_unavailable',
};
} else {
try {
verification = await ensureFresh({
userId,
sessionId,
artifacts,
images,
currentRunHtmlArtifacts: extractPublicHtmlWriteArtifacts(replyRequestMessages(reply), {
publishDir,
}),
verificationReason: verification.reason,
messages:
replyRequestMessages(reply),
repairEnabled:
config.repairFreshPageThumbnail,
});
if (repair.ok) {
verification = verifyFreshWechatPageThumbnails(artifacts, images);
logger.info?.('WeChat MP fresh thumbnail repaired:', {
relativePath: String(repair.artifact?.relativePath ?? ''),
jobId: String(repair.image?.jobId ?? ''),
cover: repair.cover,
verified: verification.ok,
});
} else {
logger.warn?.('WeChat MP fresh thumbnail repair skipped:', {
reason: repair.reason,
artifactCount: repair.artifactCount,
imageCount: repair.imageCount,
});
}
}
}
if (verification.ok) {
try {
for (const match of verification.matches) {
const htmlRelativePath = path.relative(publishDir, match.artifact.localPath);
if (!htmlRelativePath || htmlRelativePath.startsWith('..') || path.isAbsolute(htmlRelativePath)) {
throw new Error('缩略图页面路径不在用户发布目录内');
}
await ensureWorkspaceHtmlThumbnail(publishDir, htmlRelativePath);
}
return;
} catch (error) {
verification = {
...verification,
ok: false,
reason: `thumbnail_render_failed:${error instanceof Error ? error.message : String(error)}`,
reason:
`mindspace_thumbnail_authority_failed:${
error instanceof Error
? error.message
: String(error)
}`,
};
}
}
if (verification?.repair?.ok) {
logger.info?.(
'WeChat MP fresh thumbnail repaired by MindSpace:',
{
relativePath:
verification.repair.relativePath,
cover: verification.repair.cover,
},
);
}
if (verification?.ok) {
return;
}
logger.warn?.(
'WeChat MP fresh thumbnail verification failed:',
{
reason: String(
verification?.reason ?? 'unknown',
),
artifact:
verification?.artifact ?? null,
images: images.map((image) => ({
jobId: image.jobId,
purpose: image.purpose,
})),
},
);
const text = buildPagePublishFailureText({ missingFreshThumbnail: true });
if (notifyFailure) {
try {
@@ -1996,7 +1879,11 @@ export function createWechatMpService({
logger.error?.('WeChat MP fresh thumbnail failure notice failed:', sendErr);
}
}
const error = new Error(`wechat_page_fresh_thumbnail_required:${verification.reason}`);
const error = new Error(
`wechat_page_fresh_thumbnail_required:${
verification?.reason ?? 'unknown'
}`,
);
error.code = 'WECHAT_PAGE_FRESH_THUMBNAIL_REQUIRED';
throw notifyFailure ? markWechatUserNotified(error) : error;
};
@@ -2050,7 +1937,8 @@ export function createWechatMpService({
await userAuth.clearWechatAgentRoute(config.appId, openid);
}
const workingDir = await userAuth.resolveWorkingDir(userId);
const sessionPolicy = await userAuth.getAgentSessionPolicy(userId);
let sessionPolicy =
await userAuth.getAgentSessionPolicy(userId);
const publishLayout = await userAuth.getUserPublishLayout(userId);
const addressName = resolveWechatAddressName(userContext);
const existingRoute = await userAuth.getWechatAgentRoute(config.appId, openid);
@@ -2100,6 +1988,18 @@ export function createWechatMpService({
await userAuth.clearWechatAgentRoute(config.appId, openid);
rememberedWechatContexts.delete(existingRoute.agentSessionId);
} else {
sessionPolicy =
await userAuth
.getAgentSessionPolicy(
userId,
{
sessionId:
existingRoute
.agentSessionId,
packageId:
`cp_${existingRoute.agentSessionId}`,
},
);
await reconcileAgentSession(
(pathname, init) => fetchForSession(existingRoute.agentSessionId, pathname, init),
existingRoute.agentSessionId,
@@ -2160,6 +2060,14 @@ export function createWechatMpService({
if (!sessionId) {
throw new Error('公众号专属 Agent 会话创建失败');
}
sessionPolicy =
await userAuth.getAgentSessionPolicy(
userId,
{
sessionId,
packageId: `cp_${sessionId}`,
},
);
// `/agent/start` already persists the owning user and goosed node via the
// portal proxy. Re-registering here without the node can overwrite the
@@ -2385,7 +2293,6 @@ export function createWechatMpService({
forceNew,
});
let sessionId = route.sessionId;
const publishLayout = await userAuth.getUserPublishLayout(user.userId);
await ensureSessionProvider(sessionId);
await rememberWechatUserContext(sessionId, user, { forceBootstrap: route.isNewSession });
let finished = false;
@@ -2449,28 +2356,40 @@ export function createWechatMpService({
error.code = 'WECHAT_IMAGE_GENERATION_REQUIRED';
throw error;
}
const workingDir = publishLayout?.publishDir ?? (await userAuth.resolveWorkingDir(user.userId));
const linkExistsForRequest = resolveLinkExistsForWorkingDir(workingDir, linkExists);
const {
publishedArtifacts,
verifiedArtifacts,
expectedArtifacts,
recentArtifacts,
confirmedArtifacts,
} = resolveHtmlPublishArtifacts({
validReplyUrls,
hasValidReplyLink,
} = await resolveHtmlPublishArtifacts({
reply,
intent,
workingDir,
publicBaseUrl: config.publicBaseUrl,
requestStartedAt,
userId: user.userId,
sessionId,
onPageGenerated,
allowRecentArtifacts: htmlArtifactDeliveryExpected,
htmlDeliveryAuthority,
});
const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, {
confirmedArtifacts,
});
const linkExistsForRequest =
createPreparedPublicHtmlLinkExists({
userId: user.userId,
prepared: {
validReplyUrls,
confirmedArtifacts,
},
fallback: linkExists,
});
const hasValidLinkInReply =
hasValidReplyLink ||
await hasAnyValidPublishedHtmlLink(
reply?.text,
linkExistsForRequest,
{ confirmedArtifacts },
);
const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text);
const suspiciousPublishClaim =
expectedArtifacts.length === 0 &&
@@ -2537,15 +2456,15 @@ export function createWechatMpService({
reply,
openid: inbound.fromUserName,
user,
userId: user.userId,
sessionId,
imagePolicy,
publishDir: workingDir,
notifyFailure: false,
});
}
const pageDataOutcome = await enforcePageDataCollectDelivery({
reply,
intent,
workingDir,
userId: user.userId,
pageDataFinishGuard,
publicBaseUrl: config.publicBaseUrl,
@@ -2569,8 +2488,6 @@ export function createWechatMpService({
await userAuth.billSessionUsage(user.userId, sessionId, tokenState, requestId);
}
let finalizedReply = await maybeAttachPublishedHtmlLink(reply, {
workingDir,
publicBaseUrl: config.publicBaseUrl,
artifacts: publishArtifacts,
allowAttachment: publishArtifacts.length > 0,
});
@@ -2670,28 +2587,40 @@ export function createWechatMpService({
error.code = 'WECHAT_IMAGE_GENERATION_REQUIRED';
throw error;
}
const workingDir = publishLayout?.publishDir ?? (await userAuth.resolveWorkingDir(user.userId));
const linkExistsForRequest = resolveLinkExistsForWorkingDir(workingDir, linkExists);
const {
publishedArtifacts,
verifiedArtifacts,
expectedArtifacts,
recentArtifacts,
confirmedArtifacts,
} = resolveHtmlPublishArtifacts({
validReplyUrls,
hasValidReplyLink,
} = await resolveHtmlPublishArtifacts({
reply,
intent,
workingDir,
publicBaseUrl: config.publicBaseUrl,
requestStartedAt: retryStartedAt,
userId: user.userId,
sessionId,
onPageGenerated,
allowRecentArtifacts: htmlArtifactDeliveryExpected,
htmlDeliveryAuthority,
});
const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, {
confirmedArtifacts,
});
const linkExistsForRequest =
createPreparedPublicHtmlLinkExists({
userId: user.userId,
prepared: {
validReplyUrls,
confirmedArtifacts,
},
fallback: linkExists,
});
const hasValidLinkInReply =
hasValidReplyLink ||
await hasAnyValidPublishedHtmlLink(
reply?.text,
linkExistsForRequest,
{ confirmedArtifacts },
);
const replyHasPublicLinks = hasAnyPublicHtmlLink(reply?.text);
const suspiciousPublishClaim =
expectedArtifacts.length === 0 &&
@@ -2755,14 +2684,14 @@ export function createWechatMpService({
reply,
openid: inbound.fromUserName,
user,
userId: user.userId,
sessionId,
imagePolicy,
publishDir: workingDir,
});
}
const pageDataOutcome = await enforcePageDataCollectDelivery({
reply,
intent,
workingDir,
userId: user.userId,
pageDataFinishGuard,
publicBaseUrl: config.publicBaseUrl,
@@ -2786,8 +2715,6 @@ export function createWechatMpService({
await userAuth.billSessionUsage(user.userId, sessionId, tokenState, retryId);
}
let finalizedReply = await maybeAttachPublishedHtmlLink(reply, {
workingDir,
publicBaseUrl: config.publicBaseUrl,
artifacts: publishArtifacts,
allowAttachment: publishArtifacts.length > 0,
});