Add conversation package registry
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
function registryError(message, code) {
|
||||||
|
return Object.assign(new Error(message), { code });
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireMethod(target, methodName, code) {
|
||||||
|
if (!target || typeof target[methodName] !== 'function') {
|
||||||
|
throw registryError(`MindSpace conversation package registry requires ${methodName}`, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createConversationPackageRegistry({ store, service } = {}) {
|
||||||
|
requireMethod(store, 'upsertPackage', 'missing_conversation_package_store');
|
||||||
|
requireMethod(store, 'upsertArtifact', 'missing_conversation_package_store');
|
||||||
|
requireMethod(store, 'getPackageBySession', 'missing_conversation_package_store');
|
||||||
|
requireMethod(store, 'listArtifacts', 'missing_conversation_package_store');
|
||||||
|
requireMethod(service, 'putConversationManifest', 'missing_mindspace_service');
|
||||||
|
|
||||||
|
return {
|
||||||
|
async ensurePackage(input) {
|
||||||
|
return store.upsertPackage(input);
|
||||||
|
},
|
||||||
|
|
||||||
|
async recordArtifact(input) {
|
||||||
|
return store.upsertArtifact(input);
|
||||||
|
},
|
||||||
|
|
||||||
|
async writeManifestForSession({ userId, sessionId }) {
|
||||||
|
const packageRecord = await store.getPackageBySession(userId, sessionId);
|
||||||
|
if (!packageRecord) return null;
|
||||||
|
const artifacts = await store.listArtifacts(packageRecord.id);
|
||||||
|
return service.putConversationManifest({ packageRecord, artifacts });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const conversationPackageRegistryInternals = {
|
||||||
|
registryError,
|
||||||
|
requireMethod,
|
||||||
|
};
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createConversationPackageRegistry } from './mindspace-conversation-package-registry.mjs';
|
||||||
|
import { createConversationPackageStore } from './mindspace-conversation-package-store.mjs';
|
||||||
|
import { createMindSpaceServiceFacade } from './mindspace-service.mjs';
|
||||||
|
import { createLocalMindSpaceStorageAdapter } from './mindspace-storage-adapter.mjs';
|
||||||
|
|
||||||
|
function createFakePool() {
|
||||||
|
const packages = new Map();
|
||||||
|
const artifacts = new Map();
|
||||||
|
return {
|
||||||
|
async query(sql, params = []) {
|
||||||
|
if (/INSERT INTO h5_conversation_packages/i.test(sql)) {
|
||||||
|
const [
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
session_id,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
storage_prefix,
|
||||||
|
manifest_asset_id,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
] = params;
|
||||||
|
packages.set(`${user_id}:${session_id}`, {
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
session_id,
|
||||||
|
title,
|
||||||
|
status,
|
||||||
|
storage_prefix,
|
||||||
|
manifest_asset_id,
|
||||||
|
created_at,
|
||||||
|
updated_at,
|
||||||
|
});
|
||||||
|
return [{ affectedRows: 1 }];
|
||||||
|
}
|
||||||
|
if (/FROM h5_conversation_packages/i.test(sql)) {
|
||||||
|
return [[packages.get(`${params[0]}:${params[1]}`)].filter(Boolean)];
|
||||||
|
}
|
||||||
|
if (/INSERT INTO h5_conversation_artifacts/i.test(sql)) {
|
||||||
|
const [
|
||||||
|
id,
|
||||||
|
package_id,
|
||||||
|
artifact_kind,
|
||||||
|
role,
|
||||||
|
asset_id,
|
||||||
|
page_id,
|
||||||
|
publication_id,
|
||||||
|
agent_run_id,
|
||||||
|
message_id,
|
||||||
|
display_name,
|
||||||
|
mime_type,
|
||||||
|
size_bytes,
|
||||||
|
storage_key,
|
||||||
|
canonical_url,
|
||||||
|
sort_order,
|
||||||
|
created_at,
|
||||||
|
] = params;
|
||||||
|
artifacts.set(id, {
|
||||||
|
id,
|
||||||
|
package_id,
|
||||||
|
artifact_kind,
|
||||||
|
role,
|
||||||
|
asset_id,
|
||||||
|
page_id,
|
||||||
|
publication_id,
|
||||||
|
agent_run_id,
|
||||||
|
message_id,
|
||||||
|
display_name,
|
||||||
|
mime_type,
|
||||||
|
size_bytes,
|
||||||
|
storage_key,
|
||||||
|
canonical_url,
|
||||||
|
sort_order,
|
||||||
|
created_at,
|
||||||
|
});
|
||||||
|
return [{ affectedRows: 1 }];
|
||||||
|
}
|
||||||
|
if (/FROM h5_conversation_artifacts/i.test(sql)) {
|
||||||
|
return [
|
||||||
|
[...artifacts.values()]
|
||||||
|
.filter((row) => row.package_id === params[0])
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
Number(a.sort_order) - Number(b.sort_order) ||
|
||||||
|
Number(a.created_at) - Number(b.created_at) ||
|
||||||
|
String(a.id).localeCompare(String(b.id)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected query: ${sql}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('createConversationPackageRegistry validates required collaborators', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => createConversationPackageRegistry({ store: {}, service: {} }),
|
||||||
|
(error) => error.code === 'missing_conversation_package_store',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('conversation package registry records artifacts and writes manifest through storage facade', async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-registry-'));
|
||||||
|
const service = createMindSpaceServiceFacade({
|
||||||
|
storageAdapter: createLocalMindSpaceStorageAdapter(root),
|
||||||
|
publicBaseUrl: 'https://m.tkmind.cn',
|
||||||
|
});
|
||||||
|
const store = createConversationPackageStore(createFakePool());
|
||||||
|
const registry = createConversationPackageRegistry({ store, service });
|
||||||
|
|
||||||
|
const packageRecord = await registry.ensurePackage({
|
||||||
|
userId: 'user-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
title: '研究简报',
|
||||||
|
now: 1000,
|
||||||
|
});
|
||||||
|
await registry.recordArtifact({
|
||||||
|
id: 'ca_page',
|
||||||
|
packageId: packageRecord.id,
|
||||||
|
artifactKind: 'public_html',
|
||||||
|
displayName: 'brief.html',
|
||||||
|
canonicalUrl: 'https://m.tkmind.cn/MindSpace/user-1/public/brief.html',
|
||||||
|
sortOrder: 1,
|
||||||
|
now: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await registry.writeManifestForSession({
|
||||||
|
userId: 'user-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.storageKey, 'users/user-1/conversations/session-1/manifest.json');
|
||||||
|
const manifestJson = await service.getPackageObject({ packageRecord, relativePath: 'manifest.json' });
|
||||||
|
const manifest = JSON.parse(manifestJson.toString('utf8'));
|
||||||
|
assert.equal(manifest.title, '研究简报');
|
||||||
|
assert.deepEqual(manifest.artifacts, [
|
||||||
|
{
|
||||||
|
artifactId: 'ca_page',
|
||||||
|
kind: 'public_html',
|
||||||
|
displayName: 'brief.html',
|
||||||
|
canonicalUrl: 'https://m.tkmind.cn/MindSpace/user-1/public/brief.html',
|
||||||
|
sortOrder: 1,
|
||||||
|
createdAt: 2000,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('conversation package registry skips manifest writes for unknown sessions', async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mindspace-registry-'));
|
||||||
|
const registry = createConversationPackageRegistry({
|
||||||
|
store: createConversationPackageStore(createFakePool()),
|
||||||
|
service: createMindSpaceServiceFacade({
|
||||||
|
storageAdapter: createLocalMindSpaceStorageAdapter(root),
|
||||||
|
publicBaseUrl: 'https://m.tkmind.cn',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(await registry.writeManifestForSession({ userId: 'user-1', sessionId: 'missing' }), null);
|
||||||
|
});
|
||||||
@@ -32,6 +32,12 @@ function optionalString(value) {
|
|||||||
return normalized || null;
|
return normalized || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optionalNumber(value) {
|
||||||
|
if (value == null || value === '') return null;
|
||||||
|
const number = Number(value);
|
||||||
|
return Number.isFinite(number) ? number : null;
|
||||||
|
}
|
||||||
|
|
||||||
function safeStorageSegment(value, field) {
|
function safeStorageSegment(value, field) {
|
||||||
const normalized = requiredString(value, field);
|
const normalized = requiredString(value, field);
|
||||||
const encoded = encodeURIComponent(normalized);
|
const encoded = encodeURIComponent(normalized);
|
||||||
@@ -108,7 +114,7 @@ export function createConversationArtifact(input) {
|
|||||||
messageId: optionalString(input?.messageId),
|
messageId: optionalString(input?.messageId),
|
||||||
displayName: optionalString(input?.displayName),
|
displayName: optionalString(input?.displayName),
|
||||||
mimeType: optionalString(input?.mimeType),
|
mimeType: optionalString(input?.mimeType),
|
||||||
sizeBytes: Number.isFinite(Number(input?.sizeBytes)) ? Number(input.sizeBytes) : null,
|
sizeBytes: optionalNumber(input?.sizeBytes),
|
||||||
storageKey: optionalString(input?.storageKey),
|
storageKey: optionalString(input?.storageKey),
|
||||||
canonicalUrl: optionalString(input?.canonicalUrl),
|
canonicalUrl: optionalString(input?.canonicalUrl),
|
||||||
sortOrder: Number.isFinite(Number(input?.sortOrder)) ? Number(input.sortOrder) : 0,
|
sortOrder: Number.isFinite(Number(input?.sortOrder)) ? Number(input.sortOrder) : 0,
|
||||||
@@ -160,6 +166,7 @@ export function buildConversationPackageManifest({ packageRecord, artifacts = []
|
|||||||
|
|
||||||
export const mindspaceConversationPackageInternals = {
|
export const mindspaceConversationPackageInternals = {
|
||||||
requiredString,
|
requiredString,
|
||||||
|
optionalNumber,
|
||||||
optionalString,
|
optionalString,
|
||||||
safeStorageSegment,
|
safeStorageSegment,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -87,6 +87,17 @@ test('createConversationArtifact validates artifact kind and preserves provenanc
|
|||||||
assert.equal(artifact.messageId, 'msg-1');
|
assert.equal(artifact.messageId, 'msg-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('createConversationArtifact preserves unknown size as null', () => {
|
||||||
|
const artifact = createConversationArtifact({
|
||||||
|
packageId: 'cp_1',
|
||||||
|
artifactKind: 'generated_file',
|
||||||
|
sizeBytes: null,
|
||||||
|
now: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(artifact.sizeBytes, null);
|
||||||
|
});
|
||||||
|
|
||||||
test('buildConversationPackageManifest sorts artifacts and emits stable public shape', () => {
|
test('buildConversationPackageManifest sorts artifacts and emits stable public shape', () => {
|
||||||
const manifest = buildConversationPackageManifest({
|
const manifest = buildConversationPackageManifest({
|
||||||
packageRecord: {
|
packageRecord: {
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@
|
|||||||
"build:portal-runtime": "node scripts/build-portal-runtime.mjs",
|
"build:portal-runtime": "node scripts/build-portal-runtime.mjs",
|
||||||
"check:mindspace-public-links": "node scripts/check-mindspace-public-links.mjs --downloads-only",
|
"check:mindspace-public-links": "node scripts/check-mindspace-public-links.mjs --downloads-only",
|
||||||
"check:mindspace-public-links:all": "node scripts/check-mindspace-public-links.mjs --all-links",
|
"check:mindspace-public-links:all": "node scripts/check-mindspace-public-links.mjs --all-links",
|
||||||
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-finish-sync.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-pages.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs message-stream.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs",
|
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-finish-sync.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-pages.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs message-stream.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs admin-guard.test.mjs user-feedback.test.mjs",
|
||||||
"verify:chat-finish-sync": "node scripts/verify-chat-finish-sync.mjs",
|
"verify:chat-finish-sync": "node scripts/verify-chat-finish-sync.mjs",
|
||||||
"verify:public-finish-sync-runtime": "node scripts/verify-public-finish-sync-runtime.mjs",
|
"verify:public-finish-sync-runtime": "node scripts/verify-public-finish-sync-runtime.mjs",
|
||||||
"verify:mindspace-publish-guards": "node scripts/verify-mindspace-publish-guards.mjs",
|
"verify:mindspace-publish-guards": "node scripts/verify-mindspace-publish-guards.mjs",
|
||||||
|
|||||||
Reference in New Issue
Block a user