feat: complete mindspace conversation packages

This commit is contained in:
john
2026-07-02 23:46:05 +08:00
parent b084f6d23c
commit b258004dad
31 changed files with 1990 additions and 118 deletions
+165 -1
View File
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { publicationInternals } from './mindspace-publications.mjs';
import { createPublicationService, publicationInternals } from './mindspace-publications.mjs';
test('normalizes safe long-page slugs', () => {
assert.equal(publicationInternals.normalizeSlug(' product-intro-2026 '), 'product-intro-2026');
@@ -150,6 +150,55 @@ test('buildPublicationThumbnailFallback points private resources at the public p
);
});
test('resolvePublic returns page provenance for package artifact registration', async () => {
const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-public-resolve-'));
const storageKey = 'users/user-1/publications/pub-1/index.html';
await fs.mkdir(path.dirname(path.join(storageRoot, storageKey)), { recursive: true });
await fs.writeFile(path.join(storageRoot, storageKey), '<!doctype html><title>发布页</title>');
const service = createPublicationService(
{
async query(sql) {
if (sql.includes('FROM h5_publish_records pr') && sql.includes('COALESCE(u.slug, u.username)')) {
return [[
{
id: 'pub-1',
user_id: 'user-1',
owner_id: 'user-1',
page_id: 'page-1',
page_version_id: 'version-1',
title: '发布页',
source_session_id: 'session-1',
source_message_id: 'message-1',
url_slug: 'published',
public_url: 'https://m.tkmind.cn/u/john/pages/published',
access_mode: 'public',
status: 'online',
view_count: 0,
published_at: 3000,
storage_key: storageKey,
},
]];
}
return [[]];
},
},
{
storageRoot,
idFactory: () => 'view-1',
},
);
const result = await service.resolvePublic('john', 'published', null, null, {});
assert.equal(result.ownerId, 'user-1');
assert.deepEqual(result.pageSource, {
pageId: 'page-1',
title: '发布页',
sourceSessionId: 'session-1',
sourceMessageId: 'message-1',
});
});
test('prepareHtmlPublishContent inlines owned private image assets before scanning', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-publish-'));
const storageKey = 'users/user-1/assets/asset-1/current.png';
@@ -348,6 +397,121 @@ test('registerPublicationArtifactForConversation records public companions', asy
assert.match(docx.canonicalUrl, /\/MindSpace\/user-1\/public\/report\.docx$/);
});
test('publish registers public html and companion artifacts for chat-sourced pages', async () => {
const storageRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-publish-flow-'));
const h5Root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-h5-root-'));
const pageStorageKey = 'users/user-1/pages/page-1/versions/v1.html';
const pageHtml = '<!doctype html><title>Report</title><main>发布内容</main>';
await fs.mkdir(path.dirname(path.join(storageRoot, pageStorageKey)), { recursive: true });
await fs.writeFile(path.join(storageRoot, pageStorageKey), pageHtml);
const publishDir = path.join(h5Root, 'MindSpace', 'user-1');
await fs.mkdir(path.join(publishDir, 'public'), { recursive: true });
await fs.writeFile(path.join(publishDir, 'public', 'report.docx'), 'docx');
await fs.writeFile(path.join(publishDir, 'public', 'report.long.png'), 'png');
const registryCalls = [];
let idCounter = 0;
const pageRow = {
page_id: 'page-1',
title: 'Report',
summary: '',
page_type: 'html',
template_id: 'static-html',
current_version_id: 'version-1',
source_session_id: 'session-1',
source_message_id: 'message-1',
user_id: 'user-1',
space_id: 'space-1',
page_version_id: 'version-1',
version_no: 1,
bundle_asset_id: null,
storage_key: pageStorageKey,
source_relative_path: 'public/report.html',
};
const pool = {
async query(sql, params = []) {
if (sql.includes('FROM h5_page_records p') && sql.includes('JOIN h5_page_versions')) {
assert.deepEqual(params, ['page-1', 'user-1', 'version-1']);
return [[pageRow]];
}
if (sql.includes('FROM h5_publish_records pr') && sql.includes('pr.page_id <>')) {
return [[]];
}
if (sql.includes('FROM h5_users')) {
return [[{ public_slug: 'john' }]];
}
if (sql.includes('FROM h5_assets') || sql.includes('FROM h5_asset_versions')) {
return [[]];
}
return [[]];
},
async getConnection() {
return {
async beginTransaction() {},
async commit() {},
async rollback() {},
release() {},
async query(sql) {
if (sql.includes('FROM h5_user_spaces') && sql.includes('FOR UPDATE')) {
return [[{ quota_bytes: 1_000_000, used_bytes: 0, reserved_bytes: 0 }]];
}
if (sql.includes('COUNT(DISTINCT page_id)')) {
return [[{ public_page_used: 0, page_already_online: 0 }]];
}
if (sql.includes('FROM h5_space_categories')) {
return [[{ id: 'cat-public' }]];
}
if (sql.includes('SELECT id, page_version_id FROM h5_publish_records')) {
return [[]];
}
return [[]];
},
};
},
};
const service = createPublicationService(pool, {
storageRoot,
h5Root,
idFactory: () => `id-${++idCounter}`,
conversationPackageRegistry: {
async ensurePackage(input) {
registryCalls.push(['ensurePackage', input]);
return { id: 'cp-session-1' };
},
async recordArtifact(input) {
registryCalls.push(['recordArtifact', input]);
return input;
},
async writeManifestForSession(input) {
registryCalls.push(['writeManifestForSession', input]);
return input;
},
},
});
try {
const publication = await service.publish('user-1', 'page-1', {
pageVersionId: 'version-1',
accessMode: 'public',
urlSlug: 'report',
acknowledgedFindingIds: [],
});
assert.equal(publication.id, 'id-4');
const artifactKinds = registryCalls
.filter(([name]) => name === 'recordArtifact')
.map(([, input]) => input.artifactKind)
.sort();
assert.deepEqual(artifactKinds, ['docx', 'long_image', 'public_html']);
const companion = registryCalls.find(([, input]) => input?.artifactKind === 'docx')?.[1];
assert.equal(companion.messageId, 'message-1');
assert.equal(companion.publicationId, 'id-4');
assert.equal(registryCalls.at(-1)[0], 'writeManifestForSession');
} finally {
await fs.rm(storageRoot, { recursive: true, force: true });
await fs.rm(h5Root, { recursive: true, force: true });
}
});
test('registerPublicationArtifactForConversation skips pages without source session', async () => {
assert.equal(
await publicationInternals.registerPublicationArtifactForConversation({