Add page data delivery and publication guards

This commit is contained in:
john
2026-07-08 21:10:47 +08:00
parent 8d629e7a4e
commit eea14c1855
66 changed files with 3035 additions and 413 deletions
+104 -4
View File
@@ -75,20 +75,24 @@ export async function loginViaApi(baseUrl, { username, password }, reporter) {
};
}
function buildUserMessage(text) {
function buildUserMessage(text, { selectedChatSkill = null } = {}) {
const metadata = { userVisible: true, displayText: text };
if (selectedChatSkill) {
metadata.memindRun = { selectedChatSkill };
}
return {
id: crypto.randomUUID(),
role: 'user',
content: [{ type: 'text', text }],
metadata: { userVisible: true, displayText: text },
metadata,
};
}
export async function createAgentRun(baseUrl, cookie, { message, sessionId = null }) {
export async function createAgentRun(baseUrl, cookie, { message, sessionId = null, selectedChatSkill = null }) {
const requestId = crypto.randomUUID();
const body = {
request_id: requestId,
user_message: buildUserMessage(message),
user_message: buildUserMessage(message, { selectedChatSkill }),
};
if (sessionId) body.session_id = sessionId;
@@ -301,6 +305,102 @@ export async function verifyPageAccess({
return true;
}
export async function verifySurveyDelivery({
publishKey,
replyText = '',
expect = {},
reporter,
}) {
const publishDir = path.join(repoRoot, PUBLISH_ROOT_DIR, publishKey);
const publicDir = path.join(publishDir, 'public');
const policyDir = path.join(publishDir, '.mindspace', 'page-data-policies');
const sqlitePath = path.join(publishDir, '.mindspace', 'private-data.sqlite');
const forbidReply = expect.forbidReplyPatterns ?? [];
for (const pattern of forbidReply) {
if (pattern && replyText.includes(pattern)) {
reporter.fail('回复禁用模式', `命中 ${pattern}`);
}
}
if (forbidReply.length && !forbidReply.some((pattern) => pattern && replyText.includes(pattern))) {
reporter.pass('回复禁用模式', '未出现旁路 API / PLACEHOLDER');
}
let htmlFiles = [];
try {
const entries = await fs.readdir(publicDir, { withFileTypes: true });
htmlFiles = entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.html'))
.map((entry) => entry.name);
} catch {
reporter.fail('问卷 HTML', 'public/ 目录不存在');
return false;
}
const surveyLike = htmlFiles.filter((name) => /survey|问卷|feature/i.test(name));
const adminLike = htmlFiles.filter((name) => /admin|后台|manage/i.test(name));
if (surveyLike.length === 0) {
reporter.fail('问卷 HTML', `public/ 中未找到问卷页,现有: ${htmlFiles.join(', ') || '(空)'}`);
} else {
reporter.pass('问卷 HTML', surveyLike.join(', '));
}
if (adminLike.length === 0) {
reporter.fail('后台 HTML', `public/ 中未找到后台页,现有: ${htmlFiles.join(', ') || '(空)'}`);
} else {
reporter.pass('后台 HTML', adminLike.join(', '));
}
const forbidHtml = expect.forbidHtmlPatterns ?? [];
for (const name of [...surveyLike, ...adminLike]) {
const html = await fs.readFile(path.join(publicDir, name), 'utf8');
if (!html.includes('page-data-client.js')) {
reporter.fail(`${name} 脚本`, '未引用 page-data-client.js');
}
for (const pattern of forbidHtml) {
if (pattern && html.includes(pattern)) {
reporter.fail(`${name} 禁用模式`, `命中 ${pattern}`);
}
}
}
if (surveyLike.length && adminLike.length) {
reporter.pass('Page Data 客户端', '问卷/后台 HTML 已引用 page-data-client.js');
}
if (expect.requirePolicy) {
try {
const policies = await fs.readdir(policyDir);
const jsonPolicies = policies.filter((name) => name.endsWith('.json'));
if (jsonPolicies.length === 0) {
reporter.fail('Page Data 策略', 'page-data-policies/ 为空');
} else {
reporter.pass('Page Data 策略', `${jsonPolicies.length} 个 policy 文件`);
}
} catch {
reporter.fail('Page Data 策略', '缺少 .mindspace/page-data-policies/');
}
}
if (expect.requireDataset) {
try {
await fs.stat(sqlitePath);
reporter.pass('私有 SQLite', 'private-data.sqlite 存在');
} catch {
reporter.fail('私有 SQLite', 'private-data.sqlite 不存在');
}
}
const links = extractPublicLinks(replyText, 'http://127.0.0.1:8081');
if (links.length >= 2) {
reporter.pass('交付链接', `${links.length} 个链接`);
} else if (links.length === 1) {
reporter.fail('交付链接', '仅 1 个链接,期望问卷 + 后台');
} else {
reporter.fail('交付链接', '回复中未找到 MindSpace 链接');
}
return true;
}
export async function loadScenario(scenarioId) {
const scenarioPath = path.join(repoRoot, 'scenarios', `${scenarioId}.json`);
const raw = await fs.readFile(scenarioPath, 'utf8');