fix(ui): support legacy prompts and publication confirmation
This commit is contained in:
@@ -361,6 +361,17 @@ export function stripKnownChatSkillPrompt(text) {
|
|||||||
break;
|
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();
|
return next.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,19 @@ test('deriveUserFacingText removes page-data prompt persisted as display text',
|
|||||||
assert.equal(deriveUserFacingText(persistedDisplayText), userText);
|
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', () => {
|
test('deriveUserFacingText removes Memind task orchestration prefix', () => {
|
||||||
const userText = '帮我生成深度搜索报告';
|
const userText = '帮我生成深度搜索报告';
|
||||||
const agentPayload = [
|
const agentPayload = [
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ function publicationResponse(row) {
|
|||||||
viewCount: Number(row.view_count ?? 0),
|
viewCount: Number(row.view_count ?? 0),
|
||||||
publishedAt: Number(row.published_at),
|
publishedAt: Number(row.published_at),
|
||||||
offlineAt: row.offline_at == null ? null : Number(row.offline_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(
|
const [pubRows] = await pool.query(
|
||||||
`SELECT pr.id, pr.url_slug, pr.public_url, pr.page_version_id, pr.access_mode,
|
`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.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
|
pv.bundle_asset_id, av.id AS asset_version_id, av.storage_key
|
||||||
FROM h5_publish_records pr
|
FROM h5_publish_records pr
|
||||||
JOIN h5_page_records p ON p.id = pr.page_id AND p.user_id = pr.user_id
|
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 cleanupExpiredUnconfirmedPublications = async (now = Date.now()) => {
|
||||||
const [result] = await pool.query(
|
const [result] = await pool.query(
|
||||||
`UPDATE h5_publish_records
|
`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'
|
WHERE access_mode = 'public'
|
||||||
AND expires_at IS NOT NULL
|
AND expires_at IS NOT NULL
|
||||||
AND expires_at <= ?
|
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', () => {
|
test('hashes access passwords with a random salt', () => {
|
||||||
const first = publicationInternals.hashPassword('Publish-Password-2026');
|
const first = publicationInternals.hashPassword('Publish-Password-2026');
|
||||||
const second = 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');
|
const chatSkills = read('chat-skills.mjs');
|
||||||
assertIncludes(chatSkills, 'stripKnownChatSkillPrompt', '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');
|
const server = read('server.mjs');
|
||||||
assertIncludes(server, 'canUseSnapshotCache', 'server.mjs');
|
assertIncludes(server, 'canUseSnapshotCache', 'server.mjs');
|
||||||
|
|||||||
Reference in New Issue
Block a user