From f2d0c99f6c61a0dbe8785ce83e8d91075f166b76 Mon Sep 17 00:00:00 2001 From: john Date: Tue, 14 Jul 2026 15:05:37 +0800 Subject: [PATCH] fix(ui): support legacy prompts and publication confirmation --- chat-skills.mjs | 11 +++++ conversation-display.test.mjs | 13 ++++++ mindspace-publications.mjs | 4 +- mindspace-publications.test.mjs | 66 +++++++++++++++++++++++++++++ scripts/verify-chat-finish-sync.mjs | 12 ++++++ 5 files changed, 105 insertions(+), 1 deletion(-) diff --git a/chat-skills.mjs b/chat-skills.mjs index eed4cfb..4d64c01 100644 --- a/chat-skills.mjs +++ b/chat-skills.mjs @@ -361,6 +361,17 @@ export function stripKnownChatSkillPrompt(text) { break; } } + // Older persisted Page Data messages can contain a prompt from a previous + // template revision. Keep this compatibility path anchored to the stable + // skill header and final delivery sentence so the user's request is not + // mistaken for executor-only instructions. + if (/^请使用\s+page-data-collect\s+技能[::]/u.test(next)) { + const legacyEndMarker = '并说明后台入口与口令。'; + const markerIndex = next.indexOf(legacyEndMarker); + if (markerIndex >= 0) { + next = next.slice(markerIndex + legacyEndMarker.length); + } + } return next.trim(); } diff --git a/conversation-display.test.mjs b/conversation-display.test.mjs index 45c0703..8b2ba8a 100644 --- a/conversation-display.test.mjs +++ b/conversation-display.test.mjs @@ -32,6 +32,19 @@ test('deriveUserFacingText removes page-data prompt persisted as display text', assert.equal(deriveUserFacingText(persistedDisplayText), userText); }); +test('deriveUserFacingText removes a legacy page-data prompt after the template changes', () => { + const userText = '帮我做一个日记页面,可以每天写日记,其他人可以评价'; + const persistedDisplayText = [ + '请使用 page-data-collect 技能:在 MindSpace 页面中实现可提交、可持久化的数据收集。', + '流程:loadskill → privatedataexecute 建表 → privatedataregisterdataset → writefile/editfile。', + '完成后只返回 workspaceUrl,并说明后台入口与口令。', + userText, + ].join(''); + + assert.equal(stripKnownChatSkillPrompt(persistedDisplayText), userText); + assert.equal(deriveUserFacingText(persistedDisplayText), userText); +}); + test('deriveUserFacingText removes Memind task orchestration prefix', () => { const userText = '帮我生成深度搜索报告'; const agentPayload = [ diff --git a/mindspace-publications.mjs b/mindspace-publications.mjs index 6117ad0..5dc2ae1 100644 --- a/mindspace-publications.mjs +++ b/mindspace-publications.mjs @@ -140,6 +140,7 @@ function publicationResponse(row) { viewCount: Number(row.view_count ?? 0), publishedAt: Number(row.published_at), offlineAt: row.offline_at == null ? null : Number(row.offline_at), + userConfirmedAt: row.user_confirmed_at == null ? null : Number(row.user_confirmed_at), }; } @@ -982,6 +983,7 @@ export function createPublicationService(pool, options = {}) { const [pubRows] = await pool.query( `SELECT pr.id, pr.url_slug, pr.public_url, pr.page_version_id, pr.access_mode, pr.status, pr.view_count, pr.published_at, pr.offline_at, pr.expires_at, + pr.user_confirmed_at, pv.bundle_asset_id, av.id AS asset_version_id, av.storage_key FROM h5_publish_records pr JOIN h5_page_records p ON p.id = pr.page_id AND p.user_id = pr.user_id @@ -1371,7 +1373,7 @@ export function createPublicationService(pool, options = {}) { const cleanupExpiredUnconfirmedPublications = async (now = Date.now()) => { const [result] = await pool.query( `UPDATE h5_publish_records - SET access_mode = 'private', expires_at = NULL, updated_at = ? + SET access_mode = 'owner_only', expires_at = NULL, updated_at = ? WHERE access_mode = 'public' AND expires_at IS NOT NULL AND expires_at <= ? diff --git a/mindspace-publications.test.mjs b/mindspace-publications.test.mjs index 5122d5e..55260a9 100644 --- a/mindspace-publications.test.mjs +++ b/mindspace-publications.test.mjs @@ -34,6 +34,72 @@ test('accepts all documented access modes', () => { ); }); +test('getCurrent exposes whether the owner confirmed publication visibility', async () => { + const service = createPublicationService({ + async query(sql, params) { + assert.match(sql, /SELECT pr\.\*/); + assert.deepEqual(params, ['page-1', 'user-1']); + return [[{ + id: 'pub-1', + page_id: 'page-1', + page_version_id: 'version-1', + url_slug: 'journal', + public_url: '/u/john/pages/journal', + access_mode: 'public', + expires_at: null, + status: 'online', + view_count: 2, + published_at: 1000, + offline_at: null, + user_confirmed_at: 2000, + }]]; + }, + }); + + const publication = await service.getCurrent('user-1', 'page-1'); + assert.equal(publication.userConfirmedAt, 2000); +}); + +test('getCurrent returns null confirmation for an unconfirmed publication', async () => { + const service = createPublicationService({ + async query() { + return [[{ + id: 'pub-1', + page_id: 'page-1', + page_version_id: 'version-1', + url_slug: 'journal', + public_url: '/u/john/pages/journal', + access_mode: 'public', + expires_at: null, + status: 'online', + view_count: 0, + published_at: 1000, + offline_at: null, + user_confirmed_at: null, + }]]; + }, + }); + + const publication = await service.getCurrent('user-1', 'page-1'); + assert.equal(publication.userConfirmedAt, null); +}); + +test('cleanupExpiredUnconfirmedPublications falls back to owner-only access', async () => { + let executedSql = ''; + const service = createPublicationService({ + async query(sql, params) { + executedSql = sql; + assert.deepEqual(params, [3000, 3000]); + return [{ affectedRows: 1 }]; + }, + }); + + const result = await service.cleanupExpiredUnconfirmedPublications(3000); + assert.deepEqual(result, { cleaned: 1 }); + assert.match(executedSql, /SET access_mode = 'owner_only'/); + assert.doesNotMatch(executedSql, /SET access_mode = 'private'/); +}); + test('hashes access passwords with a random salt', () => { const first = publicationInternals.hashPassword('Publish-Password-2026'); const second = publicationInternals.hashPassword('Publish-Password-2026'); diff --git a/scripts/verify-chat-finish-sync.mjs b/scripts/verify-chat-finish-sync.mjs index 3366b1c..a876513 100644 --- a/scripts/verify-chat-finish-sync.mjs +++ b/scripts/verify-chat-finish-sync.mjs @@ -49,6 +49,18 @@ assertIncludes(conversationDisplay, 'deriveAssistantFacingText', 'conversation-d const chatSkills = read('chat-skills.mjs'); assertIncludes(chatSkills, 'stripKnownChatSkillPrompt', 'chat-skills.mjs'); +assertIncludes(chatSkills, "legacyEndMarker = '并说明后台入口与口令。'", 'chat-skills.mjs'); + +const chatPanel = read('src/components/ChatPanel.tsx'); +assertIncludes(chatPanel, "import { VoiceInputButton } from './VoiceInputButton'", 'ChatPanel.tsx'); +assertIncludes(chatPanel, '