2026-07-25 23:01:55 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Page Data review / correction rollout (审查纠错影响分析):
|
|
|
|
|
|
*
|
|
|
|
|
|
* - Tier A (this module): map failure codes → structured correction suggestions.
|
|
|
|
|
|
* Does not change Agent Run success/failure semantics by itself.
|
|
|
|
|
|
* - Tier B: MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED
|
|
|
|
|
|
* Ops fail-closed switch; local/dev default stays observe-only (0).
|
|
|
|
|
|
* - Tier C (minimal): remediable suggestion codes can be turned into a single
|
|
|
|
|
|
* goosed repair prompt (finish-guard / agent-run one-shot). No unbounded loops.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const HTML_ISSUE_TO_CODE = Object.freeze({
|
|
|
|
|
|
forbidden_local_storage: 'FORBIDDEN_LOCAL_STORAGE',
|
|
|
|
|
|
forbidden_local_storage_fallback: 'FORBIDDEN_LOCAL_STORAGE',
|
|
|
|
|
|
forbidden_browser_storage: 'FORBIDDEN_BROWSER_STORAGE',
|
|
|
|
|
|
forbidden_legacy_page_data_api: 'FORBIDDEN_LEGACY_PAGE_DATA_API',
|
|
|
|
|
|
forbidden_legacy_owner_api: 'FORBIDDEN_LEGACY_PAGE_DATA_API',
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** Codes goosed can reasonably fix in one repair turn. */
|
|
|
|
|
|
const REMEDIABLE_CODES = new Set([
|
|
|
|
|
|
'PAGE_DATA_DELIVERABLE_MISSING',
|
|
|
|
|
|
'PAGE_DATA_DELIVERY_FAILED',
|
|
|
|
|
|
'DELIVERABLE_DATA_STORAGE_FORBIDDEN',
|
|
|
|
|
|
'FORBIDDEN_LOCAL_STORAGE',
|
|
|
|
|
|
'FORBIDDEN_BROWSER_STORAGE',
|
|
|
|
|
|
'FORBIDDEN_LEGACY_PAGE_DATA_API',
|
|
|
|
|
|
'COLUMNS_NOT_ALLOWED',
|
|
|
|
|
|
'TOOL_GATEWAY_VALIDATION_FAILED',
|
|
|
|
|
|
'PAGE_DATA_AIDER_FINISH_UNAVAILABLE',
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
const SUGGESTION_BY_CODE = Object.freeze({
|
|
|
|
|
|
PAGE_DATA_DELIVERABLE_MISSING: Object.freeze({
|
|
|
|
|
|
code: 'PAGE_DATA_DELIVERABLE_MISSING',
|
|
|
|
|
|
title: '缺少可交付的公开页面',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'确认已用 page-data-collect 写出 public/*-survey.html 与(如需要)public/*-admin.html',
|
|
|
|
|
|
'检查页面是否落在 workspace 的 public/ 目录,而不是仅聊天草稿',
|
|
|
|
|
|
'重新说明需求并要求「完整创建并发布」',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
PAGE_DATA_DELIVERY_FAILED: Object.freeze({
|
|
|
|
|
|
code: 'PAGE_DATA_DELIVERY_FAILED',
|
|
|
|
|
|
title: '页面数据绑定或发布失败',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'确认 dataset 已 private_data_register_dataset 注册',
|
2026-08-27 09:54:46 +08:00
|
|
|
|
'对每个公开页调用 private_data_bind_workspace_page(含 password 策略时口令 ≥6 位)',
|
2026-07-25 23:01:55 +08:00
|
|
|
|
'检查 .mindspace/page-data-policies 是否生成且列白名单与表单字段一致',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
DELIVERABLE_DATA_STORAGE_FORBIDDEN: Object.freeze({
|
|
|
|
|
|
code: 'DELIVERABLE_DATA_STORAGE_FORBIDDEN',
|
|
|
|
|
|
title: '页面使用了禁止的数据存储方式',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'删除 localStorage / sessionStorage / IndexedDB 读写',
|
|
|
|
|
|
'改用 /assets/page-data-client.js 与 Page Data 公开 API',
|
|
|
|
|
|
'禁止回退到 /api/page-data 旧 Owner API',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
FORBIDDEN_LOCAL_STORAGE: Object.freeze({
|
|
|
|
|
|
code: 'FORBIDDEN_LOCAL_STORAGE',
|
|
|
|
|
|
title: '检测到 localStorage 回退',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'移除 localStorage.setItem / getItem',
|
|
|
|
|
|
'报名/问卷提交改为 MindSpacePageData.createClient insert',
|
|
|
|
|
|
'后台列表改为 Page Data read(口令页先 authenticate)',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
FORBIDDEN_BROWSER_STORAGE: Object.freeze({
|
|
|
|
|
|
code: 'FORBIDDEN_BROWSER_STORAGE',
|
|
|
|
|
|
title: '检测到浏览器本地存储',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'移除 sessionStorage / IndexedDB 等浏览器存储',
|
|
|
|
|
|
'结构化数据只走 Page Data API',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
FORBIDDEN_LEGACY_PAGE_DATA_API: Object.freeze({
|
|
|
|
|
|
code: 'FORBIDDEN_LEGACY_PAGE_DATA_API',
|
|
|
|
|
|
title: '使用了已移除的 /api/page-data',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'改用 /api/public/pages/:pageId/data/... 公开 API',
|
|
|
|
|
|
'通过 page-data-client.js,不要直连已删除的 Owner API',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
COLUMNS_NOT_ALLOWED: Object.freeze({
|
|
|
|
|
|
code: 'COLUMNS_NOT_ALLOWED',
|
|
|
|
|
|
title: '字段不在策略白名单中',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'对齐 HTML 表单 name 与 dataset 列名',
|
|
|
|
|
|
'更新 policy 的 insert/read 白名单后重新 bind',
|
|
|
|
|
|
'必要时先 ALTER 表并重新 register_dataset',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
TOOL_GATEWAY_VALIDATION_FAILED: Object.freeze({
|
|
|
|
|
|
code: 'TOOL_GATEWAY_VALIDATION_FAILED',
|
|
|
|
|
|
title: '代码任务校验未通过',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'根据工具网关校验输出修 HTML / 策略不一致项',
|
|
|
|
|
|
'简单 Page Data 修复可走 code run + aider;复杂改动再升级执行器',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
REQUIRED_REVIEW_EXECUTOR_MISMATCH: Object.freeze({
|
|
|
|
|
|
code: 'REQUIRED_REVIEW_EXECUTOR_MISMATCH',
|
|
|
|
|
|
title: '独立审查执行器不匹配',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'确认 run 要求的 reviewExecutor 与实际执行器一致',
|
|
|
|
|
|
'在能力与 executor binding 中启用对应审查执行器',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
REQUIRED_EXECUTOR_UNAVAILABLE: Object.freeze({
|
|
|
|
|
|
code: 'REQUIRED_EXECUTOR_UNAVAILABLE',
|
|
|
|
|
|
title: '所需执行器不可用',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'检查 aider / openhands / goose 绑定与 API Key',
|
|
|
|
|
|
'确认 memindadm 执行器绑定已启用且模型可用',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
PAGE_DATA_AIDER_FINISH_UNAVAILABLE: Object.freeze({
|
|
|
|
|
|
code: 'PAGE_DATA_AIDER_FINISH_UNAVAILABLE',
|
|
|
|
|
|
title: 'Aider Page Data 收尾不可用',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'改回 Goose + page-data-collect 完成 bind/发布',
|
|
|
|
|
|
'或检查 code run / Aider 配置后重试',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
WORKFLOW_VALIDATION_GATE_UNAVAILABLE: Object.freeze({
|
|
|
|
|
|
code: 'WORKFLOW_VALIDATION_GATE_UNAVAILABLE',
|
|
|
|
|
|
title: 'Page Data 审查服务不可用',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'确认 Orchestrator(8093)健康且 Shadow wiring 已开',
|
|
|
|
|
|
'本地开发可将 MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED=0 改为仅观察',
|
|
|
|
|
|
'生产强制闸门需 Ops 批准;故障时应先恢复 Orchestrator 再重试交付',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
WORKFLOW_VALIDATION_GATE_REJECTED: Object.freeze({
|
|
|
|
|
|
code: 'WORKFLOW_VALIDATION_GATE_REJECTED',
|
|
|
|
|
|
title: 'Page Data 审查未通过',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'根据 Native 守卫与失败 check codes 修复页面/策略',
|
|
|
|
|
|
'修复后重新发起交付,不要绕过审查闸门',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
WORKFLOW_VALIDATION_GATE_INCONCLUSIVE: Object.freeze({
|
|
|
|
|
|
code: 'WORKFLOW_VALIDATION_GATE_INCONCLUSIVE',
|
|
|
|
|
|
title: 'Page Data 审查结果不明确',
|
|
|
|
|
|
actions: Object.freeze([
|
|
|
|
|
|
'确认必选 check(completion / binding / storage_policy)均已实际执行而非 skipped',
|
|
|
|
|
|
'补齐 syncUserPagesOnSuccess 与 validateRunDeliverables 后再交付',
|
|
|
|
|
|
'若仅本地调试,可临时关闭强制闸门改为观察模式',
|
|
|
|
|
|
]),
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function normalizeCode(value) {
|
|
|
|
|
|
return String(value ?? '')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
.toUpperCase()
|
|
|
|
|
|
.replace(/[^A-Z0-9]+/g, '_');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function listPageDataValidationSuggestionCodes() {
|
|
|
|
|
|
return Object.keys(SUGGESTION_BY_CODE);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function resolvePageDataValidationSuggestions(codeOrCodes) {
|
|
|
|
|
|
const values = Array.isArray(codeOrCodes) ? codeOrCodes : [codeOrCodes];
|
|
|
|
|
|
const seen = new Set();
|
|
|
|
|
|
const suggestions = [];
|
|
|
|
|
|
for (const raw of values) {
|
|
|
|
|
|
const code = normalizeCode(raw);
|
|
|
|
|
|
if (!code || seen.has(code)) continue;
|
|
|
|
|
|
const item = SUGGESTION_BY_CODE[code];
|
|
|
|
|
|
if (!item) continue;
|
|
|
|
|
|
seen.add(code);
|
|
|
|
|
|
suggestions.push({
|
|
|
|
|
|
code: item.code,
|
|
|
|
|
|
title: item.title,
|
|
|
|
|
|
actions: [...item.actions],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return suggestions;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function formatPageDataValidationUserMessage({
|
|
|
|
|
|
code = null,
|
|
|
|
|
|
codes = null,
|
|
|
|
|
|
message = '',
|
|
|
|
|
|
} = {}) {
|
|
|
|
|
|
const base = String(message ?? '').trim();
|
|
|
|
|
|
const resolvedCodes = [
|
|
|
|
|
|
...(Array.isArray(codes) ? codes : []),
|
|
|
|
|
|
...(code ? [code] : []),
|
|
|
|
|
|
];
|
|
|
|
|
|
const suggestions = resolvePageDataValidationSuggestions(resolvedCodes);
|
|
|
|
|
|
if (!suggestions.length) return base;
|
|
|
|
|
|
const blocks = suggestions.map((item) => {
|
|
|
|
|
|
const lines = item.actions.map((action) => `- ${action}`);
|
|
|
|
|
|
return `建议(${item.title}):\n${lines.join('\n')}`;
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!base) return blocks.join('\n\n');
|
|
|
|
|
|
return `${base}\n\n${blocks.join('\n\n')}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function suggestionCodesFromPageDataHtmlIssues(htmlIssues = []) {
|
|
|
|
|
|
const codes = [];
|
|
|
|
|
|
for (const item of Array.isArray(htmlIssues) ? htmlIssues : []) {
|
|
|
|
|
|
const issue = String(item?.issue ?? item ?? '').trim().toLowerCase();
|
|
|
|
|
|
const mapped = HTML_ISSUE_TO_CODE[issue];
|
|
|
|
|
|
if (mapped) codes.push(mapped);
|
|
|
|
|
|
}
|
|
|
|
|
|
return codes;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function isPageDataSuggestionRemediable(codeOrCodes) {
|
|
|
|
|
|
const values = Array.isArray(codeOrCodes) ? codeOrCodes : [codeOrCodes];
|
|
|
|
|
|
return values.some((value) => REMEDIABLE_CODES.has(normalizeCode(value)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Build a goosed-facing repair prompt from Tier A suggestions.
|
|
|
|
|
|
* Intended for one-shot finish-guard / agent-run repair, not unbounded retries.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function buildPageDataValidationRepairPrompt({
|
|
|
|
|
|
code = null,
|
|
|
|
|
|
codes = null,
|
|
|
|
|
|
htmlIssues = [],
|
|
|
|
|
|
unboundFiles = [],
|
|
|
|
|
|
message = '',
|
|
|
|
|
|
} = {}) {
|
|
|
|
|
|
const resolvedCodes = [
|
|
|
|
|
|
...(Array.isArray(codes) ? codes : []),
|
|
|
|
|
|
...(code ? [code] : []),
|
|
|
|
|
|
...suggestionCodesFromPageDataHtmlIssues(htmlIssues),
|
|
|
|
|
|
];
|
|
|
|
|
|
const suggestions = resolvePageDataValidationSuggestions(resolvedCodes);
|
|
|
|
|
|
const lines = [
|
|
|
|
|
|
'【系统纠正建议 → 请立即修复】Page Data 交付未通过。请按 page-data-collect 技能执行下列纠正建议,完成后不要声称已发布,除非 bind 与公开页已就绪:',
|
|
|
|
|
|
'1. load_skill → page-data-collect',
|
|
|
|
|
|
'2. 确保 public/*.html 使用 /assets/page-data-client.js 与 MindSpacePageData.createClient({ apiBase: "/api" })',
|
|
|
|
|
|
'3. 禁止 localStorage / sessionStorage / IndexedDB 与 /api/page-data 旁路',
|
|
|
|
|
|
'4. 对每个 public/*.html 调用 private_data_bind_workspace_page',
|
|
|
|
|
|
];
|
|
|
|
|
|
if (String(message ?? '').trim()) {
|
|
|
|
|
|
lines.push('', `失败摘要:${String(message).trim().slice(0, 500)}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (suggestions.length) {
|
|
|
|
|
|
lines.push('', '纠正建议:');
|
|
|
|
|
|
for (const item of suggestions) {
|
|
|
|
|
|
lines.push(`- ${item.title}(${item.code})`);
|
|
|
|
|
|
for (const action of item.actions) {
|
|
|
|
|
|
lines.push(` · ${action}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Array.isArray(htmlIssues) && htmlIssues.length) {
|
|
|
|
|
|
lines.push('', 'HTML 问题:');
|
|
|
|
|
|
for (const item of htmlIssues) {
|
|
|
|
|
|
lines.push(`- ${item.relativePath ?? '?'}: ${item.issue ?? item}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Array.isArray(unboundFiles) && unboundFiles.length) {
|
|
|
|
|
|
lines.push('', '尚未 bind 的页面:');
|
|
|
|
|
|
for (const file of unboundFiles) {
|
|
|
|
|
|
lines.push(`- ${file.relativePath ?? file}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.push('', '修复完成前不要告诉用户“已发布/已可提交”。');
|
|
|
|
|
|
return lines.join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const pageDataValidationSuggestionsInternals = {
|
|
|
|
|
|
SUGGESTION_BY_CODE,
|
|
|
|
|
|
HTML_ISSUE_TO_CODE,
|
|
|
|
|
|
REMEDIABLE_CODES,
|
|
|
|
|
|
normalizeCode,
|
|
|
|
|
|
};
|