8ccf2db05c
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>
278 lines
10 KiB
JavaScript
278 lines
10 KiB
JavaScript
/**
|
||
* 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([
|
||
'确认 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,
|
||
};
|