Record published pages in conversation packages

This commit is contained in:
john
2026-07-02 21:24:40 +08:00
parent 39e4fffe91
commit c2308a6ead
3 changed files with 131 additions and 1 deletions
+62 -1
View File
@@ -296,6 +296,7 @@ export function createPublicationService(pool, options = {}) {
const h5Root = options.h5Root ?? null;
const idFactory = options.idFactory ?? (() => crypto.randomUUID());
const publicPageLimitFallback = Number(options.publicPageLimit ?? 5);
const conversationPackageRegistry = options.conversationPackageRegistry ?? null;
let imgproxySigner = null;
if (process.env.IMGPROXY_BASE_URL && process.env.IMGPROXY_SIGNING_KEY && process.env.IMGPROXY_SIGNING_SALT) {
@@ -362,6 +363,7 @@ export function createPublicationService(pool, options = {}) {
const loadVersion = async (userId, pageId, pageVersionId) => {
const [rows] = await pool.query(
`SELECT p.id AS page_id, p.title, p.summary, p.page_type, p.template_id, p.current_version_id,
p.source_session_id, p.source_message_id,
p.user_id, p.space_id, pv.id AS page_version_id, pv.version_no,
pv.bundle_asset_id, av.storage_key,
JSON_UNQUOTE(JSON_EXTRACT(pv.source_snapshot_json, '$.relative_path')) AS source_relative_path
@@ -729,7 +731,7 @@ export function createPublicationService(pool, options = {}) {
}
await conn.commit();
return {
const publication = {
...publicationResponse({
id: publishId,
page_id: pageId,
@@ -745,6 +747,17 @@ export function createPublicationService(pool, options = {}) {
}),
findings: result.findings,
};
await registerPublicationArtifactForConversation({
registry: conversationPackageRegistry,
userId,
page,
publication,
bundleAssetId,
storageKey,
htmlBytes,
now,
});
return publication;
} catch (error) {
await conn.rollback();
if (writtenPath) await fs.rm(writtenPath, { force: true }).catch(() => {});
@@ -1046,6 +1059,53 @@ export function createPublicationService(pool, options = {}) {
};
}
async function registerPublicationArtifactForConversation({
registry,
userId,
page,
publication,
bundleAssetId,
storageKey,
htmlBytes,
now,
}) {
const sessionId = String(page?.source_session_id ?? '').trim();
if (!registry || !sessionId || !publication?.id) return null;
try {
const packageRecord = await registry.ensurePackage({
userId,
sessionId,
title: page.title,
now,
});
const artifact = await registry.recordArtifact({
id: `ca_${publication.id}`,
packageId: packageRecord.id,
artifactKind: 'public_html',
role: 'assistant',
assetId: bundleAssetId,
pageId: page.page_id,
publicationId: publication.id,
messageId: page.source_message_id ?? null,
displayName: `${page.title}.html`,
mimeType: 'text/html',
sizeBytes: htmlBytes,
storageKey,
canonicalUrl: publication.publicUrl,
sortOrder: Number.isFinite(Number(publication.publishedAt)) ? Number(publication.publishedAt) : 0,
now,
});
await registry.writeManifestForSession({ userId, sessionId });
return artifact;
} catch (error) {
console.warn(
'[MindSpace] conversation package publication artifact registration failed:',
error instanceof Error ? error.message : error,
);
return null;
}
}
export const publicationInternals = {
normalizeSlug,
normalizeAccessMode,
@@ -1062,5 +1122,6 @@ export const publicationInternals = {
rewriteWorkspacePublicAssetReferences,
localizePrivateImageReferences,
prepareHtmlPublishContent,
registerPublicationArtifactForConversation,
scanContent, // re-exported from mindspace-content-scan.mjs
};
+68
View File
@@ -219,6 +219,74 @@ test('prepareHtmlPublishContent rewrites imgproxy local image urls to public sta
assert.doesNotMatch(prepared, /plain\/local:\/\//);
});
test('registerPublicationArtifactForConversation records public html artifact', async () => {
const calls = [];
const registry = {
async ensurePackage(input) {
calls.push(['ensurePackage', input]);
return { id: 'cp_session-1' };
},
async recordArtifact(input) {
calls.push(['recordArtifact', input]);
return input;
},
async writeManifestForSession(input) {
calls.push(['writeManifestForSession', input]);
return { storageKey: 'users/user-1/conversations/session-1/manifest.json' };
},
};
const result = await publicationInternals.registerPublicationArtifactForConversation({
registry,
userId: 'user-1',
page: {
page_id: 'page-1',
title: '发布页',
source_session_id: 'session-1',
source_message_id: 'message-1',
},
publication: {
id: 'pub-1',
publicUrl: 'https://m.tkmind.cn/u/john/pages/published',
publishedAt: 3000,
},
bundleAssetId: 'asset-pub',
storageKey: 'users/user-1/publications/pub-1/index.html',
htmlBytes: 256,
now: 3000,
});
assert.equal(result.id, 'ca_pub-1');
assert.deepEqual(calls[0], [
'ensurePackage',
{
userId: 'user-1',
sessionId: 'session-1',
title: '发布页',
now: 3000,
},
]);
assert.equal(calls[1][1].artifactKind, 'public_html');
assert.equal(calls[1][1].publicationId, 'pub-1');
assert.equal(calls[1][1].canonicalUrl, 'https://m.tkmind.cn/u/john/pages/published');
assert.deepEqual(calls[2], [
'writeManifestForSession',
{ userId: 'user-1', sessionId: 'session-1' },
]);
});
test('registerPublicationArtifactForConversation skips pages without source session', async () => {
assert.equal(
await publicationInternals.registerPublicationArtifactForConversation({
registry: { ensurePackage: async () => assert.fail('should not be called') },
userId: 'user-1',
page: { page_id: 'page-1', title: '发布页' },
publication: { id: 'pub-1' },
}),
null,
);
});
test('prepareHtmlPublishContent rewrites imgproxy urls in srcset and css url contexts', async () => {
const prepared = await publicationInternals.prepareHtmlPublishContent({
pool: {
+1
View File
@@ -339,6 +339,7 @@ async function bootstrapUserAuth() {
h5Root: __dirname,
storageRoot: mindSpaceStorageRoot,
publicPageLimit: Number(process.env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5),
conversationPackageRegistry,
});
setInterval(async () => {
try {