mindspace: close authority boundaries
This commit is contained in:
+332
-405
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user