fix: show tool parameters and skip duplicate calls

This commit is contained in:
2026-09-27 14:47:55 +08:00
parent 40fd07a1bc
commit e5bf1cf250
7 changed files with 192 additions and 42 deletions
+29 -3
View File
@@ -7,7 +7,7 @@ import { api, post, put, subscribeRun } from './api';
const { Title, Text, Paragraph } = Typography;
const kinds: Record<string, string> = { import: '导入学生材料', course_ingest: '课程资料入库', create_batch: '创建评分批次', start_batch: '启动/重试评分批次', review: '教师复核', export: '导出成绩或反馈', index_retry: '重试索引任务' };
const categories: Record<string, string> = { stage1: '阶段报告1', stage2: '阶段报告2', stage3: '阶段报告3', final_report: '最终报告', presentation: '汇报PPT/PDF' };
const statusColor = (status: string) => status === 'completed' || status === 'ready' ? 'green' : status === 'failed' || status === 'stale' ? 'red' : 'orange';
const statusColor = (status: string) => status === 'completed' || status === 'ready' ? 'green' : status === 'failed' || status === 'stale' ? 'red' : status === 'skipped' ? 'default' : 'orange';
const reportError = (error: unknown) => message.error(error instanceof Error ? error.message : String(error));
function ImportEditor({ action, conversationId, reload }: { action: any; conversationId: number; reload: () => void }) {
@@ -108,6 +108,32 @@ function Citations({ items }: { items: any[] }) {
if (!unique.length) return null;
return <section className="chat-citations" aria-label="引用资料"><details className="chat-citation-group"><summary><Text strong>引用 · {unique.length}</Text></summary><CitationCards items={unique} /></details></section>;
}
const argumentLabels: Record<string, string> = {
query: '查询词 (query)', course_version_id: '课程版本 ID', batch_id: '评分批次 ID', project_id: '学生项目 ID',
score_id: '评分 ID', task_id: '任务 ID', rubric_version_id: '量规版本 ID', criterion_id: '评分项 ID',
project_offset: '项目偏移量', criterion_offset: '评分项偏移量', offset: '偏移量', limit: '数量上限',
status: '状态筛选', student_no: '学号', relative_dir: '目录', score: '分数', format: '格式',
};
function visibleArguments(call: any) {
const args: Record<string, unknown> = call.arguments && typeof call.arguments === 'object' && !Array.isArray(call.arguments)
? { ...call.arguments } : {};
let missing = Number(args.omitted_text_fields || args.omitted_fields || 0);
delete args.omitted_text_fields;
delete args.omitted_fields;
// Older score calls kept their effective offsets in the result but dropped them from arguments.
if (call.name === 'get_scores') for (const field of ['project_offset', 'criterion_offset']) {
if (!(field in args) && typeof call.result?.[field] === 'number') {
args[field] = call.result[field];
missing = Math.max(0, missing - 1);
}
}
if (['search_course_material', 'search_project_material'].includes(call.name) && !('query' in args) && missing > 0) {
args.query = '历史调用未保存查询词';
missing -= 1;
}
if (missing > 0) args['历史未保存参数'] = `${missing} 项`;
return Object.fromEntries(Object.entries(args).map(([field, value]) => [argumentLabels[field] || field, value]));
}
function Tools({ calls }: { calls: any[] }) {
if (!calls?.length) return null;
const active = calls.filter(call => call.status === 'running').length;
@@ -115,9 +141,9 @@ function Tools({ calls }: { calls: any[] }) {
return <section className="chat-tools" aria-label="工具调用"><details className="chat-tool-group"><summary><Text strong>工具调用 · {calls.length}</Text>
{active > 0 && <Tag color="processing">{active} 项进行中</Tag>}{failed > 0 && <Tag color="red">{failed} 项失败</Tag>}</summary>
<div className="chat-tool-list">{calls.map((call, index) => <details key={call.id || index} className="chat-tool"><summary><Space wrap size="small"><Text code>{call.name || '未知工具'}</Text>
<Tag color={statusColor(call.status || 'completed')}>{call.status === 'running' ? '进行中' : call.status === 'failed' ? '失败' : '成功'}</Tag>
<Tag color={statusColor(call.status || 'completed')}>{call.status === 'running' ? '进行中' : call.status === 'failed' ? '失败' : call.status === 'skipped' ? '已跳过' : '成功'}</Tag>
<Text type="secondary">{call.status === 'running' ? '正在执行' : call.error ? String(call.error).slice(0, 80) : call.summary || (typeof call.result === 'string' ? call.result : '已完成')}</Text></Space></summary>
<div className="tool-detail"><Text strong>调用参数</Text><pre>{JSON.stringify(call.arguments || {}, null, 2)}</pre>
<div className="tool-detail"><Text strong>调用参数</Text><pre>{JSON.stringify(visibleArguments(call), null, 2)}</pre>
<Text strong>{call.error ? '错误' : '调用结果'}</Text><pre>{call.error || (typeof call.result === 'string' ? call.result : JSON.stringify(call.result ?? '运行中', null, 2))}</pre></div>
</details>)}</div></details></section>;
}
+14 -3
View File
@@ -8,10 +8,14 @@ async function mockApi(page: Page) {
content: '# 标题\n\n段落 [安全链接](https://example.com) [危险链接](javascript:alert(1)) <img src=x onerror=alert(1)>\n\n> 引用\n\n- 列表\n\n| A | B |\n|---|---|\n| 1 | 2 |\n\n```js\nconst x = 1;\n```',
citations: [{ title: '资料.pdf', page: 2, quote: '原文摘录', url: '/api/pages/2' }, { title: '资料.pdf', page: 2, quote: '原文摘录', url: '/api/pages/2' },
{ chunk_id: 2977, title: '综合报告.pdf', page: 48, quote: '第48页原文片段', url: '/api/pages/48' }],
tool_calls: [{ id: 'call-one', name: 'search_project_material', status: 'completed', arguments: { project_id: 1 },
tool_calls: [{ id: 'call-one', name: 'search_project_material', status: 'completed', arguments: { project_id: 1, omitted_text_fields: 1 },
summary: '找到 1 条引用', result: { found: true, citations: [{ page: 48, quote: '第48页原文片段' }] } },
{ id: 'call-two', name: 'get_scores', status: 'completed', arguments: { batch_id: 3, criterion_offset: 0 },
summary: '返回 1 项评分', result: { next_criterion_offset: 6, results: [{ rows: [{ criterion_id: 'C1', final_score: 8 }] }] } }] }],
{ id: 'call-two', name: 'get_scores', status: 'completed', arguments: { batch_id: 3, omitted_text_fields: 1 },
summary: '返回 1 项评分', result: { project_offset: 0, criterion_offset: 6, next_criterion_offset: 12, results: [{ rows: [{ criterion_id: 'C1', final_score: 8 }] }] } },
{ id: 'call-three', name: 'search_project_material', status: 'completed', arguments: { project_id: 1, query: '视觉检测' },
summary: '找到 1 条引用', result: { found: true, citations: [] } },
{ id: 'call-four', name: 'search_project_material', status: 'skipped', arguments: { project_id: 1, query: '视觉检测' },
summary: '重复调用已跳过', result: { previous_call_id: 'call-three' } }] }],
actions: [], runs: [{ id: 1, status: 'completed', tool_calls: [] }] },
};
let nextId = 8;
@@ -103,6 +107,13 @@ test('markdown, citations, tool folding and narrow layout are safe', async ({ pa
await expect(page.locator('.tool-detail').first()).toContainText('第48页原文片段');
await page.locator('.chat-tool').nth(1).locator('summary').click();
await expect(page.locator('.chat-tool').nth(1).locator('.tool-detail')).toContainText('C1');
await expect(page.locator('.chat-tool').nth(1).locator('.tool-detail')).toContainText('评分项偏移量');
await expect(page.locator('.chat-tool').nth(1).locator('.tool-detail')).toContainText('6');
await expect(page.locator('.chat-tool').first().locator('.tool-detail')).toContainText('历史调用未保存查询词');
await page.locator('.chat-tool').nth(2).locator('summary').click();
await expect(page.locator('.chat-tool').nth(2).locator('.tool-detail')).toContainText('查询词 (query)');
await expect(page.locator('.chat-tool').nth(2).locator('.tool-detail')).toContainText('视觉检测');
await expect(page.locator('.chat-tool').nth(3).locator('summary')).toContainText('已跳过');
await page.locator('.chat-citation-group > summary').click();
await expect(page.locator('.chat-citation-group')).toHaveAttribute('open', '');
await expect(page.getByText('资料.pdf')).toBeVisible();