feat: chat uploads, vision turn isolation, and MindSpace agent improvements

Add chat file/image upload UX, attachment proxying, vision thumbnails, and per-turn image scoping so agents only use the current upload. Extend MindSpace asset context, billing token state, OA/scenario verify scripts, and related runtime config.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-11 00:23:01 +08:00
parent e3063ea806
commit 32fb2cdeaf
51 changed files with 4588 additions and 186 deletions
+163
View File
@@ -449,6 +449,169 @@ export async function verifySurveyDelivery({
return true;
}
export const CHILDREN_HOBBY_DIET_SURVEY = {
userId: '32035858-9a20-425b-89da-c118ef0779aa',
username: 'john4',
surveyHtml: 'children-hobby-survey.html',
adminHtml: 'children-hobby-admin.html',
dataset: 'children_hobby_survey',
dietField: 'q4_diet_meals',
dietKeywords: ['饮食', '几顿', 'q4_diet_meals'],
};
async function readJsonIfExists(filePath) {
try {
const raw = await fs.readFile(filePath, 'utf8');
return JSON.parse(raw);
} catch {
return null;
}
}
async function findPublicSurveyPolicy(publishKey, datasetName) {
const policyDir = path.join(repoRoot, PUBLISH_ROOT_DIR, publishKey, '.mindspace', 'page-data-policies');
let entries = [];
try {
entries = await fs.readdir(policyDir);
} catch {
return null;
}
for (const name of entries.filter((item) => item.endsWith('.json'))) {
const policy = await readJsonIfExists(path.join(policyDir, name));
const dataset = policy?.datasets?.[datasetName];
if (policy?.accessMode === 'public' && dataset?.insert) {
return { pageId: policy.pageId, policy, fileName: name };
}
}
return null;
}
export async function verifyChildrenHobbyDietSurvey({
publishKey = CHILDREN_HOBBY_DIET_SURVEY.userId,
baseUrl = resolvePortalBase(Number(process.env.H5_PORT ?? 8081)),
reporter,
testInsert = true,
spec = CHILDREN_HOBBY_DIET_SURVEY,
} = {}) {
const publishDir = path.join(repoRoot, PUBLISH_ROOT_DIR, publishKey);
const publicDir = path.join(publishDir, 'public');
const sqlitePath = path.join(publishDir, '.mindspace', 'private-data.sqlite');
let ok = true;
const surveyPath = path.join(publicDir, spec.surveyHtml);
const adminPath = path.join(publicDir, spec.adminHtml);
try {
const [surveyHtml, adminHtml] = await Promise.all([
fs.readFile(surveyPath, 'utf8'),
fs.readFile(adminPath, 'utf8'),
]);
reporter.pass('问卷 HTML 文件', spec.surveyHtml);
reporter.pass('后台 HTML 文件', spec.adminHtml);
const dietHits = spec.dietKeywords.filter((keyword) => surveyHtml.includes(keyword));
if (dietHits.length !== spec.dietKeywords.length) {
reporter.fail(
'问卷饮食字段',
`未命中: ${spec.dietKeywords.filter((keyword) => !surveyHtml.includes(keyword)).join(', ')}`,
);
ok = false;
} else {
reporter.pass('问卷饮食字段', dietHits.join(', '));
}
if (!surveyHtml.includes(`insertRow('${spec.dataset}'`) || !surveyHtml.includes(spec.dietField)) {
reporter.fail('问卷提交字段', `insertRow 未包含 ${spec.dietField}`);
ok = false;
} else {
reporter.pass('问卷提交字段', `${spec.dataset}.${spec.dietField}`);
}
if (!adminHtml.includes('每日饮食') || !adminHtml.includes(spec.dietField)) {
reporter.fail('后台饮食列', `未展示 ${spec.dietField}`);
ok = false;
} else {
reporter.pass('后台饮食列', spec.dietField);
}
if (!surveyHtml.includes('page-data-client.js') || !adminHtml.includes('page-data-client.js')) {
reporter.fail('Page Data 客户端', '问卷/后台未引用 page-data-client.js');
ok = false;
} else {
reporter.pass('Page Data 客户端', 'page-data-client.js');
}
} catch (error) {
reporter.fail('HTML 读取', error instanceof Error ? error.message : String(error));
ok = false;
}
const policyInfo = await findPublicSurveyPolicy(publishKey, spec.dataset);
if (!policyInfo) {
reporter.fail('公开问卷策略', '未找到 public insert policy');
ok = false;
} else {
reporter.pass('公开问卷策略', `${policyInfo.fileName} (${policyInfo.pageId})`);
const insertCols = policyInfo.policy?.datasets?.[spec.dataset]?.columns?.insert ?? [];
if (!insertCols.includes(spec.dietField)) {
reporter.fail('策略 insert 列', `${spec.dietField} 不在白名单: ${insertCols.join(', ')}`);
ok = false;
} else {
reporter.pass('策略 insert 列', insertCols.join(', '));
}
}
try {
await fs.stat(sqlitePath);
reporter.pass('私有 SQLite', '.mindspace/private-data.sqlite');
} catch {
reporter.fail('私有 SQLite', 'private-data.sqlite 不存在');
ok = false;
}
const surveyUrl = new URL(
`/MindSpace/${publishKey}/public/${spec.surveyHtml}`,
baseUrl,
).href;
const adminUrl = new URL(
`/MindSpace/${publishKey}/public/${spec.adminHtml}`,
baseUrl,
).href;
for (const [label, url] of [['问卷页 HTTP', surveyUrl], ['后台页 HTTP', adminUrl]]) {
const response = await fetch(url);
if (response.status !== 200) {
reporter.fail(label, `${response.status} ${url}`);
ok = false;
} else {
reporter.pass(label, url);
}
}
if (testInsert && policyInfo?.pageId) {
const payload = {
q1_age_group: '7-9岁',
q2_favorite_hobby: '阅读写作',
q3_development_area: 'automated verify insert',
[spec.dietField]: '4顿(3正餐+1加餐)',
};
const response = await fetch(
`${baseUrl}/api/public/pages/${policyInfo.pageId}/data/${spec.dataset}/rows`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
},
);
const body = await response.json().catch(() => ({}));
if (!response.ok) {
reporter.fail('公开 insert API', `${response.status} ${JSON.stringify(body)}`);
ok = false;
} else if (!body?.data?.row?.[spec.dietField]) {
reporter.fail('公开 insert API', `响应缺少 ${spec.dietField}`);
ok = false;
} else {
reporter.pass('公开 insert API', `${spec.dietField}=${body.data.row[spec.dietField]}`);
}
}
return ok;
}
export async function loadScenario(scenarioId) {
const scenarioPath = path.join(repoRoot, 'scenarios', `${scenarioId}.json`);
const raw = await fs.readFile(scenarioPath, 'utf8');