feat(page-data): add validation suggestions, repair path, and thinking fixes
Memind CI / Test, build, and release guards (push) Failing after 3m18s

Map Page Data failure codes to Chinese remediation hints, trigger one-shot
goosed repair for remediable cases, and recover poisoned thinking sessions.
Route local DeepSeek through the no-think proxy via host.docker.internal so
tool rounds no longer hit reasoning_content 400; document gate and case-study
scenarios for event registration repair.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-25 23:01:55 +08:00
parent 6b6f9ed01c
commit 8ccf2db05c
28 changed files with 1652 additions and 154 deletions
+277
View File
@@ -0,0 +1,277 @@
/**
* 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 注册',
'对每个公开页调用 private_data_bind_workspace_page(含 password 策略时口令 ≥8 位)',
'检查 .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([
'确认 Orchestrator8093)健康且 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([
'确认必选 checkcompletion / 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,
};