Add conversation package store
This commit is contained in:
@@ -0,0 +1,166 @@
|
|||||||
|
import {
|
||||||
|
buildConversationPackageManifest,
|
||||||
|
createConversationArtifact,
|
||||||
|
createConversationPackageRecord,
|
||||||
|
} from './mindspace-conversation-package.mjs';
|
||||||
|
|
||||||
|
function storeError(message, code) {
|
||||||
|
return Object.assign(new Error(message), { code });
|
||||||
|
}
|
||||||
|
|
||||||
|
function requirePool(pool) {
|
||||||
|
if (!pool || typeof pool.query !== 'function') {
|
||||||
|
throw storeError('MindSpace conversation package store requires a queryable pool', 'missing_query_pool');
|
||||||
|
}
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
function packageFromRow(row) {
|
||||||
|
if (!row) return null;
|
||||||
|
return createConversationPackageRecord({
|
||||||
|
id: row.id,
|
||||||
|
userId: row.user_id,
|
||||||
|
sessionId: row.session_id,
|
||||||
|
title: row.title,
|
||||||
|
status: row.status,
|
||||||
|
storagePrefix: row.storage_prefix,
|
||||||
|
manifestAssetId: row.manifest_asset_id,
|
||||||
|
createdAt: Number(row.created_at),
|
||||||
|
updatedAt: Number(row.updated_at),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function artifactFromRow(row) {
|
||||||
|
if (!row) return null;
|
||||||
|
return createConversationArtifact({
|
||||||
|
id: row.id,
|
||||||
|
packageId: row.package_id,
|
||||||
|
artifactKind: row.artifact_kind,
|
||||||
|
role: row.role,
|
||||||
|
assetId: row.asset_id,
|
||||||
|
pageId: row.page_id,
|
||||||
|
publicationId: row.publication_id,
|
||||||
|
agentRunId: row.agent_run_id,
|
||||||
|
messageId: row.message_id,
|
||||||
|
displayName: row.display_name,
|
||||||
|
mimeType: row.mime_type,
|
||||||
|
sizeBytes: row.size_bytes == null ? null : Number(row.size_bytes),
|
||||||
|
storageKey: row.storage_key,
|
||||||
|
canonicalUrl: row.canonical_url,
|
||||||
|
sortOrder: Number(row.sort_order ?? 0),
|
||||||
|
createdAt: Number(row.created_at),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createConversationPackageStore(pool) {
|
||||||
|
const db = requirePool(pool);
|
||||||
|
|
||||||
|
return {
|
||||||
|
async upsertPackage(input) {
|
||||||
|
const record = createConversationPackageRecord(input);
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO h5_conversation_packages
|
||||||
|
(id, user_id, session_id, title, status, storage_prefix, manifest_asset_id, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
title = VALUES(title),
|
||||||
|
status = VALUES(status),
|
||||||
|
storage_prefix = VALUES(storage_prefix),
|
||||||
|
manifest_asset_id = VALUES(manifest_asset_id),
|
||||||
|
updated_at = VALUES(updated_at)`,
|
||||||
|
[
|
||||||
|
record.id,
|
||||||
|
record.userId,
|
||||||
|
record.sessionId,
|
||||||
|
record.title,
|
||||||
|
record.status,
|
||||||
|
record.storagePrefix,
|
||||||
|
record.manifestAssetId,
|
||||||
|
record.createdAt,
|
||||||
|
record.updatedAt,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return record;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getPackageBySession(userId, sessionId) {
|
||||||
|
const [rows] = await db.query(
|
||||||
|
`SELECT id, user_id, session_id, title, status, storage_prefix, manifest_asset_id, created_at, updated_at
|
||||||
|
FROM h5_conversation_packages
|
||||||
|
WHERE user_id = ? AND session_id = ?
|
||||||
|
LIMIT 1`,
|
||||||
|
[userId, sessionId],
|
||||||
|
);
|
||||||
|
return packageFromRow(rows?.[0]);
|
||||||
|
},
|
||||||
|
|
||||||
|
async upsertArtifact(input) {
|
||||||
|
const artifact = createConversationArtifact(input);
|
||||||
|
await db.query(
|
||||||
|
`INSERT INTO h5_conversation_artifacts
|
||||||
|
(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)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
artifact_kind = VALUES(artifact_kind),
|
||||||
|
role = VALUES(role),
|
||||||
|
asset_id = VALUES(asset_id),
|
||||||
|
page_id = VALUES(page_id),
|
||||||
|
publication_id = VALUES(publication_id),
|
||||||
|
agent_run_id = VALUES(agent_run_id),
|
||||||
|
message_id = VALUES(message_id),
|
||||||
|
display_name = VALUES(display_name),
|
||||||
|
mime_type = VALUES(mime_type),
|
||||||
|
size_bytes = VALUES(size_bytes),
|
||||||
|
storage_key = VALUES(storage_key),
|
||||||
|
canonical_url = VALUES(canonical_url),
|
||||||
|
sort_order = VALUES(sort_order)`,
|
||||||
|
[
|
||||||
|
artifact.id,
|
||||||
|
artifact.packageId,
|
||||||
|
artifact.artifactKind,
|
||||||
|
artifact.role,
|
||||||
|
artifact.assetId,
|
||||||
|
artifact.pageId,
|
||||||
|
artifact.publicationId,
|
||||||
|
artifact.agentRunId,
|
||||||
|
artifact.messageId,
|
||||||
|
artifact.displayName,
|
||||||
|
artifact.mimeType,
|
||||||
|
artifact.sizeBytes,
|
||||||
|
artifact.storageKey,
|
||||||
|
artifact.canonicalUrl,
|
||||||
|
artifact.sortOrder,
|
||||||
|
artifact.createdAt,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return artifact;
|
||||||
|
},
|
||||||
|
|
||||||
|
async listArtifacts(packageId) {
|
||||||
|
const [rows] = await db.query(
|
||||||
|
`SELECT 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
|
||||||
|
FROM h5_conversation_artifacts
|
||||||
|
WHERE package_id = ?
|
||||||
|
ORDER BY sort_order ASC, created_at ASC, id ASC`,
|
||||||
|
[packageId],
|
||||||
|
);
|
||||||
|
return (rows ?? []).map(artifactFromRow);
|
||||||
|
},
|
||||||
|
|
||||||
|
async buildManifestForSession({ userId, sessionId }) {
|
||||||
|
const packageRecord = await this.getPackageBySession(userId, sessionId);
|
||||||
|
if (!packageRecord) return null;
|
||||||
|
const artifacts = await this.listArtifacts(packageRecord.id);
|
||||||
|
return buildConversationPackageManifest({ packageRecord, artifacts });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const conversationPackageStoreInternals = {
|
||||||
|
artifactFromRow,
|
||||||
|
packageFromRow,
|
||||||
|
requirePool,
|
||||||
|
storeError,
|
||||||
|
};
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createConversationPackageStore } from './mindspace-conversation-package-store.mjs';
|
||||||
|
|
||||||
|
function createFakePool() {
|
||||||
|
const packages = new Map();
|
||||||
|
const artifacts = new Map();
|
||||||
|
const queries = [];
|
||||||
|
return {
|
||||||
|
queries,
|
||||||
|
async query(sql, params = []) {
|
||||||
|
queries.push({ 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)) {
|
||||||
|
const packageId = params[0];
|
||||||
|
const rows = [...artifacts.values()]
|
||||||
|
.filter((row) => row.package_id === packageId)
|
||||||
|
.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)),
|
||||||
|
);
|
||||||
|
return [rows];
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected query: ${sql}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('createConversationPackageStore requires a queryable pool', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => createConversationPackageStore({}),
|
||||||
|
(error) => error.code === 'missing_query_pool',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('conversation package store upserts package metadata and builds manifest', async () => {
|
||||||
|
const pool = createFakePool();
|
||||||
|
const store = createConversationPackageStore(pool);
|
||||||
|
const packageRecord = await store.upsertPackage({
|
||||||
|
userId: 'user-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
title: '旅行计划',
|
||||||
|
now: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(packageRecord.id, 'cp_session-1');
|
||||||
|
assert.equal(packageRecord.storagePrefix, 'users/user-1/conversations/session-1');
|
||||||
|
assert.match(pool.queries[0].sql, /ON DUPLICATE KEY UPDATE/);
|
||||||
|
|
||||||
|
await store.upsertArtifact({
|
||||||
|
id: 'ca_late',
|
||||||
|
packageId: packageRecord.id,
|
||||||
|
artifactKind: 'public_html',
|
||||||
|
displayName: 'plan.html',
|
||||||
|
storageKey: 'users/user-1/conversations/session-1/public/plan.html',
|
||||||
|
canonicalUrl: 'https://m.tkmind.cn/MindSpace/user-1/public/plan.html',
|
||||||
|
sortOrder: 2,
|
||||||
|
now: 3000,
|
||||||
|
});
|
||||||
|
await store.upsertArtifact({
|
||||||
|
id: 'ca_first',
|
||||||
|
packageId: packageRecord.id,
|
||||||
|
artifactKind: 'input_image',
|
||||||
|
displayName: 'cover.png',
|
||||||
|
sortOrder: 1,
|
||||||
|
now: 2000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const manifest = await store.buildManifestForSession({
|
||||||
|
userId: 'user-1',
|
||||||
|
sessionId: 'session-1',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(manifest.packageId, 'cp_session-1');
|
||||||
|
assert.equal(manifest.uri, 'mindspace://users/user-1/conversations/session-1');
|
||||||
|
assert.deepEqual(
|
||||||
|
manifest.artifacts.map((artifact) => artifact.artifactId),
|
||||||
|
['ca_first', 'ca_late'],
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
manifest.artifacts[1].canonicalUrl,
|
||||||
|
'https://m.tkmind.cn/MindSpace/user-1/public/plan.html',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('conversation package store returns null manifest for missing session', async () => {
|
||||||
|
const store = createConversationPackageStore(createFakePool());
|
||||||
|
assert.equal(await store.buildManifestForSession({ userId: 'user-1', sessionId: 'missing' }), null);
|
||||||
|
});
|
||||||
+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-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-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