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:
@@ -19,6 +19,7 @@ import {
|
||||
snapshotPublicHtml,
|
||||
verifyPageAccess,
|
||||
verifySurveyDelivery,
|
||||
verifyChildrenHobbyDietSurvey,
|
||||
waitForAssistantGrowth,
|
||||
waitForRunTerminal,
|
||||
extractAssistantTexts,
|
||||
@@ -200,6 +201,15 @@ async function runScenario(scenario, port) {
|
||||
});
|
||||
}
|
||||
|
||||
if (step.expect?.surveyDietUpdate) {
|
||||
await verifyChildrenHobbyDietSurvey({
|
||||
publishKey,
|
||||
baseUrl,
|
||||
reporter,
|
||||
testInsert: step.expect.testInsert !== false,
|
||||
});
|
||||
}
|
||||
|
||||
const forbidReply = step.expect?.forbidReplyPatterns ?? [];
|
||||
for (const pattern of forbidReply) {
|
||||
if (pattern && reply.combined.includes(pattern)) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 验证 john4 儿童爱好问卷「饮食偏好 / 每日几顿」增量改造链路(无需重新聊天)。
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/verify-children-hobby-diet-survey.mjs
|
||||
* node scripts/verify-children-hobby-diet-survey.mjs --no-insert
|
||||
* node scripts/verify-children-hobby-diet-survey.mjs --user-id <uuid> --port 8081
|
||||
*/
|
||||
import { execSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { loadH5Environment } from './load-env.mjs';
|
||||
import {
|
||||
CHILDREN_HOBBY_DIET_SURVEY,
|
||||
createReporter,
|
||||
resolvePortalBase,
|
||||
verifyChildrenHobbyDietSurvey,
|
||||
} from './scenario-test-lib.mjs';
|
||||
import { PUBLISH_ROOT_DIR } from '../user-publish.mjs';
|
||||
import { createDbPool } from '../db.mjs';
|
||||
|
||||
const repoRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
loadH5Environment(import.meta.dirname);
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
userId: CHILDREN_HOBBY_DIET_SURVEY.userId,
|
||||
port: Number(process.env.H5_PORT ?? 8081),
|
||||
testInsert: true,
|
||||
checkRuns: true,
|
||||
};
|
||||
for (let i = 2; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === '--no-insert') options.testInsert = false;
|
||||
else if (arg === '--no-runs') options.checkRuns = false;
|
||||
else if (arg === '--user-id' && argv[i + 1]) options.userId = argv[++i];
|
||||
else if (arg === '--port' && argv[i + 1]) options.port = Number(argv[++i]);
|
||||
else if (arg === '-h' || arg === '--help') {
|
||||
console.log(`Usage: node scripts/verify-children-hobby-diet-survey.mjs [--no-insert] [--no-runs] [--user-id <uuid>] [--port <port>]`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
throw new Error(`未知参数: ${arg}`);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
async function ensurePortalReady(baseUrl) {
|
||||
const response = await fetch(`${baseUrl}/auth/status`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Portal 未就绪: ${baseUrl}/auth/status -> ${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function verifySqliteColumn(sqlitePath, tableName, columnName, reporter) {
|
||||
try {
|
||||
const output = execSync(
|
||||
`sqlite3 ${JSON.stringify(sqlitePath)} "PRAGMA table_info(${tableName});"`,
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
if (!output.includes(`|${columnName}|`)) {
|
||||
reporter.fail('SQLite 列', `${tableName}.${columnName} 不存在`);
|
||||
return false;
|
||||
}
|
||||
reporter.pass('SQLite 列', `${tableName}.${columnName}`);
|
||||
const datasetJson = execSync(
|
||||
`sqlite3 ${JSON.stringify(sqlitePath)} "SELECT config_json FROM __page_data_datasets WHERE name='${CHILDREN_HOBBY_DIET_SURVEY.dataset}';"`,
|
||||
{ encoding: 'utf8' },
|
||||
).trim();
|
||||
const config = JSON.parse(datasetJson);
|
||||
const insertCols = config?.columns?.insert ?? [];
|
||||
if (!insertCols.includes(columnName)) {
|
||||
reporter.fail('dataset insert 列', `${columnName} 不在 ${insertCols.join(', ')}`);
|
||||
return false;
|
||||
}
|
||||
reporter.pass('dataset insert 列', insertCols.join(', '));
|
||||
return true;
|
||||
} catch (error) {
|
||||
reporter.fail('SQLite 检查', error instanceof Error ? error.message : String(error));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyAgentRunHealth(userId, reporter) {
|
||||
if (!process.env.DATABASE_URL && !fs.existsSync(path.join(repoRoot, '.env'))) {
|
||||
reporter.pass('Agent run 健康', '跳过(无 DATABASE_URL)');
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
for (const line of fs.readFileSync(path.join(repoRoot, '.env'), 'utf8').split('\n')) {
|
||||
const t = line.trim();
|
||||
if (!t || t.startsWith('#')) continue;
|
||||
const eq = t.indexOf('=');
|
||||
if (eq < 0) continue;
|
||||
const k = t.slice(0, eq).trim();
|
||||
const v = t.slice(eq + 1).trim();
|
||||
if (!process.env[k]) process.env[k] = v;
|
||||
}
|
||||
const pool = createDbPool();
|
||||
const [runningRows] = await pool.query(
|
||||
`SELECT id, agent_session_id, updated_at
|
||||
FROM h5_agent_runs
|
||||
WHERE user_id = ? AND status = 'running'
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 5`,
|
||||
[userId],
|
||||
);
|
||||
await pool.end();
|
||||
if (runningRows.length > 0) {
|
||||
reporter.fail('Agent run 健康', `${runningRows.length} 条 running(可能前端仍在 loading)`);
|
||||
for (const row of runningRows) {
|
||||
console.error(` - ${row.id} session=${row.agent_session_id}`);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
reporter.pass('Agent run 健康', '无 orphan running');
|
||||
return true;
|
||||
} catch (error) {
|
||||
reporter.fail('Agent run 健康', error instanceof Error ? error.message : String(error));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv);
|
||||
const reporter = createReporter();
|
||||
const baseUrl = resolvePortalBase(options.port);
|
||||
const publishKey = options.userId;
|
||||
const sqlitePath = path.join(
|
||||
repoRoot,
|
||||
PUBLISH_ROOT_DIR,
|
||||
publishKey,
|
||||
'.mindspace',
|
||||
'private-data.sqlite',
|
||||
);
|
||||
|
||||
console.log('==> 儿童爱好问卷 · 饮食偏好链路验证');
|
||||
console.log(` Portal: ${baseUrl}`);
|
||||
console.log(` 用户: ${publishKey}\n`);
|
||||
|
||||
await ensurePortalReady(baseUrl);
|
||||
|
||||
let ok = true;
|
||||
ok = verifySqliteColumn(
|
||||
sqlitePath,
|
||||
CHILDREN_HOBBY_DIET_SURVEY.dataset,
|
||||
CHILDREN_HOBBY_DIET_SURVEY.dietField,
|
||||
reporter,
|
||||
) && ok;
|
||||
|
||||
ok = await verifyChildrenHobbyDietSurvey({
|
||||
publishKey,
|
||||
baseUrl,
|
||||
reporter,
|
||||
testInsert: options.testInsert,
|
||||
}) && ok;
|
||||
|
||||
if (options.checkRuns) {
|
||||
ok = await verifyAgentRunHealth(publishKey, reporter) && ok;
|
||||
}
|
||||
|
||||
const code = reporter.summary();
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack ?? error.message : error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Verify OA selected Excel analysis chain (john4 面条区域销售数据_示例.xlsx).
|
||||
* Checks injected context + optional live agent run.
|
||||
*/
|
||||
import crypto from 'node:crypto';
|
||||
import {
|
||||
buildContextPrefix,
|
||||
buildMindSpaceChatContext,
|
||||
} from '../mindspace-chat-context.mjs';
|
||||
import { USER_COOKIE } from '../user-auth.mjs';
|
||||
import {
|
||||
createReporter,
|
||||
resolvePortalBase,
|
||||
waitForAssistantGrowth,
|
||||
waitForRunTerminal,
|
||||
} from './scenario-test-lib.mjs';
|
||||
|
||||
const BASE = resolvePortalBase(process.env.PORTAL_PORT ?? process.env.PORT ?? 8081);
|
||||
const ASSET_ID = process.env.OA_EXCEL_ASSET_ID ?? '597c0177-becd-4b89-be72-1b5fd3496f17';
|
||||
const LIVE_RUN = process.env.VERIFY_OA_EXCEL_LIVE !== '0';
|
||||
const TIMEOUT_MS = Number(process.env.VERIFY_OA_EXCEL_TIMEOUT_MS ?? 120000);
|
||||
|
||||
function buildUserAddressPrefix(username) {
|
||||
return `[用户身份]
|
||||
- 当前登录用户称呼:${username}
|
||||
- 你是 TKMind 助手;与用户对话时用「${username}」称呼对方(如「${username},你好」),禁止把用户叫作 TKMind。
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
async function login(reporter) {
|
||||
const password = process.env.JOHN4_PASSWORD ?? '888888';
|
||||
const response = await fetch(`${BASE}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: 'john4', password }),
|
||||
});
|
||||
const body = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !body?.authenticated) {
|
||||
throw new Error(`登录失败: ${response.status} ${JSON.stringify(body)}`);
|
||||
}
|
||||
const setCookie = response.headers.getSetCookie?.() ?? [];
|
||||
const cookieLine = setCookie.find((line) => line.startsWith(`${USER_COOKIE}=`))
|
||||
?? response.headers.get('set-cookie');
|
||||
const match = String(cookieLine ?? '').match(new RegExp(`${USER_COOKIE}=([^;]+)`));
|
||||
const token = match?.[1] ? decodeURIComponent(match[1]) : null;
|
||||
if (!token) throw new Error('未解析到会话 Cookie');
|
||||
reporter.pass('登录', `john4 (${body.user?.id ?? 'unknown'})`);
|
||||
return { cookie: `${USER_COOKIE}=${token}`, user: body.user };
|
||||
}
|
||||
|
||||
async function apiFetch(cookie, path) {
|
||||
const response = await fetch(`${BASE}${path}`, { headers: { Cookie: cookie } });
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(`${path} ${response.status}: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function countProbeNarration(text) {
|
||||
const patterns = [
|
||||
/Let me first look/i,
|
||||
/没有 [`']?oa\/?[`']? 文件夹/i,
|
||||
/文件不在当前工作区/i,
|
||||
/确认工作目录/i,
|
||||
/工作目录跳转/i,
|
||||
/list_dir oa/i,
|
||||
];
|
||||
return patterns.filter((re) => re.test(text)).length;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const reporter = createReporter();
|
||||
const { cookie, user } = await login(reporter);
|
||||
|
||||
const spacePayload = await apiFetch(cookie, '/api/mindspace/v1/space');
|
||||
const space = spacePayload.data;
|
||||
const oaCategory = space.categories.find((item) => item.code === 'oa');
|
||||
if (!oaCategory) {
|
||||
reporter.fail('OA 分类', 'space 中未找到 oa 分类');
|
||||
process.exit(reporter.summary());
|
||||
}
|
||||
|
||||
const assetsPayload = await apiFetch(cookie, '/api/mindspace/v1/assets?category=oa');
|
||||
const assets = assetsPayload.data ?? [];
|
||||
const asset = assets.find((item) => item.id === ASSET_ID)
|
||||
?? assets.find((item) => /面条区域销售数据/.test(item.displayName ?? ''));
|
||||
if (!asset) {
|
||||
reporter.fail('Excel 资料', `未找到 asset ${ASSET_ID}`);
|
||||
process.exit(reporter.summary());
|
||||
}
|
||||
reporter.pass('Excel 资料', `${asset.displayName} (${asset.id})`);
|
||||
|
||||
if (asset.workspaceRelativePath) {
|
||||
reporter.pass('workspaceRelativePath', asset.workspaceRelativePath);
|
||||
} else {
|
||||
reporter.fail('workspaceRelativePath', 'API 未返回落盘路径');
|
||||
}
|
||||
|
||||
const staleCategory = { ...oaCategory, itemCount: 0 };
|
||||
const visibleAssets = [asset];
|
||||
const context = buildMindSpaceChatContext({
|
||||
space: { ...space, categories: space.categories.map((c) => (c.code === 'oa' ? staleCategory : c)) },
|
||||
ownerUsername: user.username ?? 'john4',
|
||||
selectedCategory: staleCategory,
|
||||
selectedPageId: null,
|
||||
pages: [],
|
||||
assets: visibleAssets,
|
||||
selectedAssets: [asset],
|
||||
route: '/space?category=oa',
|
||||
agentSessionId: 'verify-session',
|
||||
h5ApiBase: BASE,
|
||||
});
|
||||
|
||||
if (context.category?.itemCount === 1) {
|
||||
reporter.pass('itemCount 兜底', 'stale 0 → effective 1');
|
||||
} else {
|
||||
reporter.fail('itemCount 兜底', `expected 1 got ${context.category?.itemCount}`);
|
||||
}
|
||||
|
||||
const prefix = buildContextPrefix(context);
|
||||
const prefixChecks = [
|
||||
...(asset.workspaceRelativePath
|
||||
? [
|
||||
['已落盘路径', /已落盘路径/],
|
||||
['read_file 路径', new RegExp(`read_file ${asset.workspaceRelativePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)],
|
||||
['禁止 list_dir', /禁止.*list_dir/],
|
||||
]
|
||||
: [
|
||||
['优先 list_dir oa', /优先.*list_dir oa/],
|
||||
]),
|
||||
['数据表汇总校验', /数据表汇总校验/],
|
||||
['共 1 项', /共 1 项/],
|
||||
];
|
||||
for (const [label, pattern] of prefixChecks) {
|
||||
if (pattern.test(prefix)) reporter.pass(`上下文 · ${label}`, 'ok');
|
||||
else reporter.fail(`上下文 · ${label}`, 'prefix 缺少预期片段');
|
||||
}
|
||||
|
||||
if (!LIVE_RUN) {
|
||||
console.log('\n跳过 live run(VERIFY_OA_EXCEL_LIVE=0)');
|
||||
process.exit(reporter.summary());
|
||||
}
|
||||
|
||||
const userText = '帮我分析一下这个数据吧';
|
||||
const agentText = `${buildUserAddressPrefix(user.username ?? 'john4')}${prefix}${userText}`;
|
||||
const requestId = crypto.randomUUID();
|
||||
const runResponse = await fetch(`${BASE}/api/agent/runs`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: cookie,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
request_id: requestId,
|
||||
user_message: {
|
||||
id: crypto.randomUUID(),
|
||||
role: 'user',
|
||||
content: [{ type: 'text', text: agentText }],
|
||||
metadata: { userVisible: true, displayText: userText },
|
||||
},
|
||||
selected_asset_ids: [asset.id],
|
||||
}),
|
||||
});
|
||||
const runPayload = await runResponse.json().catch(() => ({}));
|
||||
if (!runResponse.ok) {
|
||||
reporter.fail('创建 run', `${runResponse.status} ${JSON.stringify(runPayload)}`);
|
||||
process.exit(reporter.summary());
|
||||
}
|
||||
const run = runPayload.run ?? runPayload;
|
||||
const runId = run.id;
|
||||
let resolvedSessionId = run.sessionId ?? run.agent_session_id ?? run.agentSessionId ?? null;
|
||||
reporter.pass('创建 run', `${runId.slice(0, 8)}…${resolvedSessionId ? ` session=${resolvedSessionId}` : ''}`);
|
||||
|
||||
const terminal = await waitForRunTerminal(BASE, cookie, runId, TIMEOUT_MS);
|
||||
resolvedSessionId = resolvedSessionId
|
||||
?? terminal.agent_session_id
|
||||
?? terminal.agentSessionId
|
||||
?? terminal.sessionId
|
||||
?? null;
|
||||
if (!resolvedSessionId) {
|
||||
reporter.fail('sessionId', 'run 完成后仍无法解析 sessionId');
|
||||
process.exit(reporter.summary());
|
||||
}
|
||||
reporter.pass('run 终态', `${terminal.status} · session=${resolvedSessionId}`);
|
||||
|
||||
const growth = await waitForAssistantGrowth(BASE, cookie, resolvedSessionId, {
|
||||
previousCount: 0,
|
||||
minChars: 80,
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
if (!growth?.combined) {
|
||||
reporter.fail('assistant 回复', '未拿到可见回复');
|
||||
process.exit(reporter.summary());
|
||||
}
|
||||
|
||||
const combined = growth.combined;
|
||||
reporter.pass('assistant 回复长度', `${combined.length} chars / ${growth.count} msgs`);
|
||||
|
||||
const probeHits = countProbeNarration(combined);
|
||||
if (probeHits === 0) {
|
||||
reporter.pass('路径探测旁白', '未检测到典型 probe 句式');
|
||||
} else {
|
||||
reporter.fail('路径探测旁白', `仍检测到 ${probeHits} 类 probe 句式`);
|
||||
}
|
||||
|
||||
if (/4[,.]?872[,.]?000|4872000|487\.2\s*万/.test(combined)) {
|
||||
reporter.pass('总销售额', '包含正确总计 ~487.2万');
|
||||
} else if (/4[,.]?470[,.]?500|4470500/.test(combined)) {
|
||||
reporter.fail('总销售额', '仍输出错误的 447.05 万');
|
||||
} else {
|
||||
reporter.fail('总销售额', '回复中未识别到明确总计(需人工查看)');
|
||||
}
|
||||
|
||||
if (/54[,.]?200/.test(combined) && !/55[,.]?400|55400/.test(combined)) {
|
||||
reporter.fail('总销量', '仍输出错误的 54200 箱');
|
||||
} else if (/55[,.]?400|55400/.test(combined)) {
|
||||
reporter.pass('总销量', '包含正确总计 55400 箱');
|
||||
}
|
||||
|
||||
console.log('\n--- assistant 摘要 ---');
|
||||
console.log(combined.slice(0, 1200));
|
||||
process.exit(reporter.summary());
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user