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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user