Files
Genarrative/apps/admin-web/src/pages/AdminErrorReportsPage.tsx
T
k88936 520b9344ef
Project CI / Repository checks (push) Successful in 3m1s
Project CI / Frontend tests (push) Successful in 3m27s
Project CI / Backend tests (push) Successful in 7m20s
Project CI / Native shell tests (push) Has been cancelled
Fix/AGC上下文丢失问题 (#247)
权衡之后选择附加历史消息构造一个巨大的prompt
before:
```
1. user:  aabb
2. assistant: bbaa 这里codex崩溃了或者重启上下文丢失
```
after:
重启之后发送新消息abab
 codex看到:
```
user:  aabb
assistant: bbaa
这里崩溃了!
user: abab
```

移除了原来的"会话最后只有用户消息就自动重新发送用户消息"功能, 因为存在冲突: 现在的实现是每次用户手动发送新消息才做以上的步骤

before and after:
![shotmd-1788345830.jpg](/attachments/28549e4d-9913-459a-bae2-a195b8ee9e04)

Close #249

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/247
Co-authored-by: 王德宇 <kvtodev@outlook.com>
Co-committed-by: 王德宇 <kvtodev@outlook.com>
2026-09-03 19:30:21 +08:00

293 lines
9.2 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import {
downloadAdminErrorReport,
formatAdminApiError,
getAdminErrorReport,
isAdminApiError,
listAdminErrorReports,
updateAdminErrorReport,
} from '../api/adminApiClient';
import type {
AdminErrorReportDetail,
AdminErrorReportEntry,
} from '../api/adminApiTypes';
type Props = { token: string; onUnauthorized: (message?: string) => void };
const ADMIN_ERROR_REPORT_STATUSES = ['new', 'in-progress', 'resolved'] as const;
const ERROR_REPORT_PAGE_SIZE = 50;
export function AdminErrorReportsPage({ token, onUnauthorized }: Props) {
const [reports, setReports] = useState<AdminErrorReportEntry[]>([]);
const [selected, setSelected] = useState<AdminErrorReportDetail | null>(null);
const [status, setStatus] = useState('');
const [downloading, setDownloading] = useState(false);
const [busy, setBusy] = useState(false);
const [filterStatus, setFilterStatus] = useState('');
const [pageOffset, setPageOffset] = useState(0);
const [pageInfo, setPageInfo] = useState({ total: 0, hasMore: false });
const openReportRequestId = useRef(0);
const loadRequestId = useRef(0);
const load = useCallback(async () => {
const requestId = ++loadRequestId.current;
setStatus('');
try {
const response = await listAdminErrorReports(token, {
status: filterStatus || undefined,
limit: ERROR_REPORT_PAGE_SIZE,
offset: pageOffset,
});
if (requestId === loadRequestId.current) {
const lastValidOffset = response.total
? Math.floor((response.total - 1) / ERROR_REPORT_PAGE_SIZE) *
ERROR_REPORT_PAGE_SIZE
: 0;
if (pageOffset > lastValidOffset) {
setPageOffset(lastValidOffset);
return;
}
setReports(response.reports);
setPageInfo({ total: response.total, hasMore: response.hasMore });
}
} catch (error) {
if (requestId !== loadRequestId.current) return;
if (isAdminApiError(error) && error.status === 401)
return onUnauthorized();
setStatus(formatAdminApiError(error));
}
}, [filterStatus, onUnauthorized, pageOffset, token]);
const loadRef = useRef(load);
useEffect(() => {
loadRef.current = load;
}, [load]);
useEffect(() => {
void load();
}, [load]);
async function openReport(batchId: string) {
const requestId = ++openReportRequestId.current;
setStatus('');
try {
const detail = await getAdminErrorReport(token, batchId);
if (requestId === openReportRequestId.current) setSelected(detail);
} catch (error) {
if (requestId !== openReportRequestId.current) return;
if (isAdminApiError(error) && error.status === 401)
return onUnauthorized();
setStatus(formatAdminApiError(error));
}
}
async function saveStatus(nextStatus: string) {
if (!selected || busy) return;
setBusy(true);
setStatus('');
try {
const updated = await updateAdminErrorReport(token, selected.batchId, {
status: nextStatus,
note: selected.note,
});
setSelected((current) =>
current ? { ...current, ...updated } : current,
);
await loadRef.current();
} catch (error) {
if (isAdminApiError(error) && error.status === 401) onUnauthorized();
else setStatus(formatAdminApiError(error));
} finally {
setBusy(false);
}
}
async function download(batchId: string) {
if (downloading) return;
setDownloading(true);
setStatus('');
try {
const blob = await downloadAdminErrorReport(token, batchId);
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = `${batchId}.zip`;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
setTimeout(() => URL.revokeObjectURL(url), 10_000);
} catch (error) {
if (isAdminApiError(error) && error.status === 401) onUnauthorized();
else setStatus(formatAdminApiError(error));
} finally {
setDownloading(false);
}
}
return (
<section className="admin-panel">
<div className="admin-panel-header">
<div>
<h1>错误报告</h1>
<p>查看用户提交的脱敏诊断包。</p>
</div>
<label>
状态{' '}
<select
value={filterStatus}
onChange={(event) => {
setFilterStatus(event.target.value);
setPageOffset(0);
}}
>
<option value="">全部</option>
{ADMIN_ERROR_REPORT_STATUSES.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
</label>
</div>
{status ? (
<p className="admin-alert" role="status">
{status}
</p>
) : null}
<div className="admin-table-wrap">
<table className="admin-table">
<thead>
<tr>
<th>报告</th>
<th>事件</th>
<th>来源</th>
<th>状态</th>
<th>用户</th>
<th>时间</th>
</tr>
</thead>
<tbody>
{reports.map((report) => (
<tr
key={report.batchId}
onClick={() => void openReport(report.batchId)}
role="button"
tabIndex={0}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
void openReport(report.batchId);
}
}}
>
<td>{report.batchId}</td>
<td>{report.eventCount}</td>
<td>{report.source ?? '-'}</td>
<td>{report.status}</td>
<td>{report.userId}</td>
<td>{new Date(report.createdAt).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="admin-detail-modal__actions" aria-label="错误报告分页">
<span>
{pageInfo.total === 0
? '暂无报告'
: `第 ${pageOffset + 1}-${Math.min(pageOffset + reports.length, pageInfo.total)} 条,共 ${pageInfo.total} 条`}
</span>
<button
type="button"
onClick={() =>
setPageOffset((offset) =>
Math.max(0, offset - ERROR_REPORT_PAGE_SIZE),
)
}
disabled={pageOffset === 0}
>
上一页
</button>
<button
type="button"
onClick={() =>
setPageOffset((offset) => offset + ERROR_REPORT_PAGE_SIZE)
}
disabled={!pageInfo.hasMore}
>
下一页
</button>
</div>
{selected ? (
<div
className="admin-detail-modal"
role="dialog"
aria-label="错误报告详情"
>
<div className="admin-detail-modal__panel">
<header>
<h2>{selected.batchId}</h2>
<button type="button" onClick={() => setSelected(null)}>
关闭
</button>
</header>
<p>
用户:{selected.userId} · 事件:{selected.eventCount} · 日志:
{selected.logCount}
</p>
<pre>{JSON.stringify(selected.events.slice(0, 20), null, 2)}</pre>
{selected.userDescription ? (
<p>用户描述:{selected.userDescription}</p>
) : null}
<label className="admin-detail-modal__note">
处理备注
<textarea
value={selected.note ?? ''}
onChange={(event) =>
setSelected((current) =>
current
? { ...current, note: event.target.value }
: current,
)
}
maxLength={2000}
rows={4}
placeholder="记录处理结论或后续跟进事项"
disabled={busy}
/>
</label>
<div className="admin-detail-modal__actions">
<select
value={selected.status}
onChange={(event) => void saveStatus(event.target.value)}
disabled={busy}
>
{ADMIN_ERROR_REPORT_STATUSES.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
<button
type="button"
onClick={() => void saveStatus(selected.status)}
disabled={busy}
>
保存备注
</button>
<button
type="button"
onClick={() => void download(selected.batchId)}
disabled={downloading}
>
{downloading ? '下载中…' : '下载诊断包'}
</button>
</div>
</div>
</div>
) : null}
</section>
);
}