Files
memind/page-data-validation-suggestions.mjs
john 8ccf2db05c
Memind CI / Test, build, and release guards (push) Failing after 3m18s
feat(page-data): add validation suggestions, repair path, and thinking fixes
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>
2026-07-25 23:01:55 +08:00

278 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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,
};