fix(ui): support legacy prompts and publication confirmation
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 <= ?
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user