fix(ui): support legacy prompts and publication confirmation

This commit is contained in:
john
2026-07-14 15:05:37 +08:00
parent 77f1ea8350
commit f2d0c99f6c
5 changed files with 105 additions and 1 deletions
+11
View File
@@ -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();
}
+13
View File
@@ -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 = [
+3 -1
View File
@@ -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 <= ?
+66
View File
@@ -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');
+12
View File
@@ -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, '<VoiceInputButton', 'ChatPanel.tsx');
assertIncludes(chatPanel, 'onLiveTranscript={handleVoiceLiveTranscript}', 'ChatPanel.tsx');
assertIncludes(chatPanel, 'onTranscript={handleVoiceTranscript}', 'ChatPanel.tsx');
assertIncludes(chatPanel, 'setInput(mergeVoiceText(text))', 'ChatPanel.tsx voice transcript wiring');
const voiceInputButton = read('src/components/VoiceInputButton.tsx');
assertIncludes(voiceInputButton, 'useVoiceSession({', 'VoiceInputButton.tsx');
assertIncludes(voiceInputButton, 'onTranscript?.(transcript)', 'VoiceInputButton.tsx');
const server = read('server.mjs');
assertIncludes(server, 'canUseSnapshotCache', 'server.mjs');