修复错误报告审查中的次要问题
修复后台错误报告刷新竞态、下载并发与查询参数构建复用。 修复错误报告清理分页、归档大小限制、分页总数和下载响应头异常。 修复 OSS 流式读取上限与 Codex 高频调试日志阻塞。 同步错误报告技术方案与项目决策记录,并在 review.txt 保留待决 breaking 项。
This commit is contained in:
@@ -261,7 +261,16 @@ export function listAdminTrackingEvents(
|
||||
query: AdminTrackingEventListQuery = {},
|
||||
) {
|
||||
return request<AdminTrackingEventListResponse>(
|
||||
`/admin/api/tracking/events${buildQueryString(query)}`,
|
||||
`/admin/api/tracking/events${buildQueryString((params) => {
|
||||
appendQueryParam(params, 'eventKey', query.eventKey);
|
||||
appendQueryParam(params, 'userId', query.userId);
|
||||
appendQueryParam(params, 'scopeKind', query.scopeKind);
|
||||
appendQueryParam(params, 'scopeId', query.scopeId);
|
||||
appendQueryParam(params, 'startDate', query.startDate);
|
||||
appendQueryParam(params, 'endDate', query.endDate);
|
||||
appendNumericQueryParam(params, 'limit', query.limit);
|
||||
if (query.exportAll) params.set('exportAll', 'true');
|
||||
})}`,
|
||||
{ token },
|
||||
);
|
||||
}
|
||||
@@ -844,22 +853,10 @@ async function postAdminDirectUploadFile(
|
||||
}
|
||||
}
|
||||
|
||||
function buildQueryString(query: AdminTrackingEventListQuery) {
|
||||
function buildQueryString(append: (params: URLSearchParams) => void) {
|
||||
const params = new URLSearchParams();
|
||||
appendQueryParam(params, 'eventKey', query.eventKey);
|
||||
appendQueryParam(params, 'userId', query.userId);
|
||||
appendQueryParam(params, 'scopeKind', query.scopeKind);
|
||||
appendQueryParam(params, 'scopeId', query.scopeId);
|
||||
appendQueryParam(params, 'startDate', query.startDate);
|
||||
appendQueryParam(params, 'endDate', query.endDate);
|
||||
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
||||
params.set('limit', String(query.limit));
|
||||
}
|
||||
if (query.exportAll) {
|
||||
params.set('exportAll', 'true');
|
||||
}
|
||||
const queryString = params.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
append(params);
|
||||
return formatQueryString(params);
|
||||
}
|
||||
|
||||
function buildErrorReportQueryString(query: {
|
||||
@@ -869,39 +866,31 @@ function buildErrorReportQueryString(query: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}) {
|
||||
const params = new URLSearchParams();
|
||||
appendQueryParam(params, 'status', query.status);
|
||||
appendQueryParam(params, 'fingerprint', query.fingerprint);
|
||||
appendQueryParam(params, 'source', query.source);
|
||||
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
||||
params.set('limit', String(query.limit));
|
||||
}
|
||||
if (typeof query.offset === 'number' && Number.isFinite(query.offset)) {
|
||||
params.set('offset', String(query.offset));
|
||||
}
|
||||
const queryString = params.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
return buildQueryString((params) => {
|
||||
appendQueryParam(params, 'status', query.status);
|
||||
appendQueryParam(params, 'fingerprint', query.fingerprint);
|
||||
appendQueryParam(params, 'source', query.source);
|
||||
appendNumericQueryParam(params, 'limit', query.limit);
|
||||
appendNumericQueryParam(params, 'offset', query.offset);
|
||||
});
|
||||
}
|
||||
|
||||
function buildAdminRechargeOrderListQuery(query: AdminRechargeOrderListQuery) {
|
||||
const params = new URLSearchParams();
|
||||
appendQueryParam(params, 'orderId', query.orderId);
|
||||
appendQueryParam(
|
||||
params,
|
||||
'providerTransactionId',
|
||||
query.providerTransactionId,
|
||||
);
|
||||
appendQueryParam(params, 'userId', query.userId);
|
||||
appendQueryParam(params, 'publicUserCode', query.publicUserCode);
|
||||
appendQueryParam(params, 'paymentChannel', query.paymentChannel);
|
||||
appendQueryParam(params, 'status', query.status);
|
||||
appendQueryParam(params, 'createdAfter', query.createdAfter);
|
||||
appendQueryParam(params, 'createdBefore', query.createdBefore);
|
||||
if (typeof query.limit === 'number' && Number.isFinite(query.limit)) {
|
||||
params.set('limit', String(query.limit));
|
||||
}
|
||||
const queryString = params.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
return buildQueryString((params) => {
|
||||
appendQueryParam(params, 'orderId', query.orderId);
|
||||
appendQueryParam(
|
||||
params,
|
||||
'providerTransactionId',
|
||||
query.providerTransactionId,
|
||||
);
|
||||
appendQueryParam(params, 'userId', query.userId);
|
||||
appendQueryParam(params, 'publicUserCode', query.publicUserCode);
|
||||
appendQueryParam(params, 'paymentChannel', query.paymentChannel);
|
||||
appendQueryParam(params, 'status', query.status);
|
||||
appendQueryParam(params, 'createdAfter', query.createdAfter);
|
||||
appendQueryParam(params, 'createdBefore', query.createdBefore);
|
||||
appendNumericQueryParam(params, 'limit', query.limit);
|
||||
});
|
||||
}
|
||||
|
||||
function buildAdminUserDetailQuery(query: AdminUserDetailQuery) {
|
||||
@@ -977,6 +966,21 @@ function appendQueryParam(
|
||||
}
|
||||
}
|
||||
|
||||
function appendNumericQueryParam(
|
||||
params: URLSearchParams,
|
||||
key: string,
|
||||
value: number | null | undefined,
|
||||
) {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
function formatQueryString(params: URLSearchParams) {
|
||||
const queryString = params.toString();
|
||||
return queryString ? `?${queryString}` : '';
|
||||
}
|
||||
|
||||
function parseJsonResponse(responseText: string): unknown {
|
||||
if (!responseText.trim()) {
|
||||
return null;
|
||||
|
||||
@@ -22,6 +22,7 @@ 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);
|
||||
@@ -50,6 +51,11 @@ export function AdminErrorReportsPage({ token, onUnauthorized }: Props) {
|
||||
}
|
||||
}, [filterStatus, onUnauthorized, pageOffset, token]);
|
||||
|
||||
const loadRef = useRef(load);
|
||||
useEffect(() => {
|
||||
loadRef.current = load;
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
@@ -79,7 +85,7 @@ export function AdminErrorReportsPage({ token, onUnauthorized }: Props) {
|
||||
note: selected.note,
|
||||
}),
|
||||
);
|
||||
await load();
|
||||
await loadRef.current();
|
||||
} catch (error) {
|
||||
if (isAdminApiError(error) && error.status === 401) onUnauthorized();
|
||||
else setStatus(formatAdminApiError(error));
|
||||
@@ -89,6 +95,8 @@ export function AdminErrorReportsPage({ token, onUnauthorized }: Props) {
|
||||
}
|
||||
|
||||
async function download(batchId: string) {
|
||||
if (downloading) return;
|
||||
setDownloading(true);
|
||||
setStatus('');
|
||||
try {
|
||||
const blob = await downloadAdminErrorReport(token, batchId);
|
||||
@@ -99,10 +107,12 @@ export function AdminErrorReportsPage({ token, onUnauthorized }: Props) {
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
||||
} catch (error) {
|
||||
if (isAdminApiError(error) && error.status === 401) onUnauthorized();
|
||||
else setStatus(formatAdminApiError(error));
|
||||
} finally {
|
||||
setDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,8 +246,9 @@ export function AdminErrorReportsPage({ token, onUnauthorized }: Props) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void download(selected.batchId)}
|
||||
disabled={downloading}
|
||||
>
|
||||
下载诊断包
|
||||
{downloading ? '下载中…' : '下载诊断包'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2366,7 +2366,7 @@ async fn read_game_creator_codex_app_server_stdout(
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.map(|params| params.keys().cloned().collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
app_log!(
|
||||
eprintln!(
|
||||
"agent.direct_codex.event method={} paramsKeys={:?}",
|
||||
method, params_keys
|
||||
);
|
||||
@@ -2376,7 +2376,7 @@ async fn read_game_creator_codex_app_server_stdout(
|
||||
.map(serde_json::Value::to_string)
|
||||
.map(|value| value.len())
|
||||
.unwrap_or_default();
|
||||
app_log!(
|
||||
eprintln!(
|
||||
"agent.direct_codex.event.safeDetails method={} detailBytes={}",
|
||||
method, detail_bytes
|
||||
);
|
||||
@@ -2396,7 +2396,7 @@ async fn read_game_creator_codex_app_server_stdout(
|
||||
.pointer("/turn/status")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
app_log!(
|
||||
eprintln!(
|
||||
"agent.direct_codex.event notification={} status={}",
|
||||
event_name, status
|
||||
);
|
||||
@@ -2415,7 +2415,7 @@ async fn read_game_creator_codex_app_server_stdout(
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(Vec::len)
|
||||
.unwrap_or_default();
|
||||
app_log!(
|
||||
eprintln!(
|
||||
"agent.direct_codex.item event={} type={} commandPresent={} changeCount={}",
|
||||
event_name,
|
||||
item_type,
|
||||
@@ -2441,7 +2441,7 @@ async fn read_game_creator_codex_app_server_stdout(
|
||||
.and_then(|params| params.get("grantRoot"))
|
||||
.and_then(serde_json::Value::as_str);
|
||||
if direct_workspace && direct_debug {
|
||||
app_log!(
|
||||
eprintln!(
|
||||
"agent.direct_codex.server_request method={method} grantRootPresent={}",
|
||||
requested_grant_root.is_some()
|
||||
);
|
||||
@@ -2632,7 +2632,7 @@ async fn read_game_creator_codex_app_server_stderr(
|
||||
if std::env::var_os("GENARRATIVE_AGC_DIRECT_DEBUG").is_some()
|
||||
&& upgraded.workspace_mode.uses_direct_conversation()
|
||||
{
|
||||
app_log!("agent.direct_codex.stderr bytes={count}");
|
||||
eprintln!("agent.direct_codex.stderr bytes={count}");
|
||||
}
|
||||
let oversized_record = upgraded
|
||||
.stderr_summary
|
||||
|
||||
@@ -7867,3 +7867,4 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 2026-09-01 review 收口:错误报告修复详情请求竞态、下载 anchor 生命周期、客户端采集脱敏/指纹降级与 4xx 噪声、用户级幂等隔离、`agc` 私有 OSS 前缀越权、日志读取链接检查、ZIP 同名日志和元数据/归档清理一致性;同步在 `review.txt` 标注仍需产品/运维决定的架构项。
|
||||
- 2026-09-01 追加:错误报告不依赖 api-server 本地文件、目录锁或同步文件 I/O;ZIP 只在请求内存构建后上传 OSS。管理员详情路由不属于 External OpenAPI;不存在返回 404,归档损坏返回 500。
|
||||
- 2026-09-01 追加:错误报告不落本地文件;请求内存构建 ZIP 后直接上传 OSS,成功后写入 SpacetimeDB `error_report` 元数据。OSS key 固定为 `agc/error-reports/v1/{batchId}.zip`,不含日期;同一用户 `userId + submissionId` 幂等。管理员查询 DB,详情/下载按 object key 读取 OSS;每日清理先删 OSS,再删 DB,失败留待下次重试。
|
||||
- 2026-09-01 review minor 修复:错误报告每日清理改为完整分页扫描,DB 删除失败显式返回以便下周期重试;OSS 读取按 `Content-Length` 与流式累计执行大小上限;管理员归档解析限制解压后 `events.jsonl` 为 24 MiB / 100 条事件。
|
||||
|
||||
@@ -23,10 +23,10 @@ AI Game Creator Shell 采用 IDEA 风格的当前进程错误报告:错误事
|
||||
- 归档构建只在请求生命周期内使用受 20 MiB 上限约束的内存 `Vec<u8>`,随后直接 PUT 到私有 OSS;服务端不写本地报告文件,也不保留本地索引。OSS 上传失败不写入数据库,调用方可稍后重新提交。
|
||||
- 归档对象使用固定私有 OSS key:`agc/error-reports/v1/{batchId}.zip`;key 只由报告 UUID 决定,不包含时间戳。上传成功后才写入 SpacetimeDB `error_report` 元数据表;`userId + submissionId` 由唯一幂等键保证重放返回已有记录。完整事件、说明和日志只存在 OSS ZIP。
|
||||
- `agc` 是服务端专用私有前缀;公共直传票据、通用 object-key 规范化和 legacy 公开路径均拒绝该前缀。归档内同名日志会自动加数字后缀,读取本机诊断日志时拒绝符号链接/非普通文件。
|
||||
- 后台接口:`GET/PATCH /admin/api/error-reports/{batchId}`、`GET /admin/api/error-reports` 和受保护的 `/download`。列表支持 `limit`/`offset` 分页并返回 `total`、`hasMore`。
|
||||
- 后台接口:`GET/PATCH /admin/api/error-reports/{batchId}`、`GET /admin/api/error-reports` 和受保护的 `/download`。列表支持 `limit`/`offset` 分页并返回 `total`、`hasMore`;OSS 读取先检查 `Content-Length` 并在流式累计超过上限时立即中止,不把超限对象完整缓存在内存中。
|
||||
- 这些是 api-server 内部登录/管理员路由,不属于 `/api/external/v1`,不纳入 External OpenAPI;管理员详情对不存在返回 404,对归档/元数据损坏返回 500。
|
||||
- admin viewer 仅接受 error-reports Tab 权限,支持列表筛选、分页、详情、状态 `new/in-progress/resolved`、处理备注和受控下载;不存在的更新目标返回 404,存储损坏返回 500。列表行支持键盘 Enter/Space 打开详情,详情事件预览最多显示 20 条,完整内容通过诊断包下载获取。
|
||||
- 管理员列表、筛选、状态和备注全部读取/更新 SpacetimeDB;详情先读表再从 OSS 下载并解析 ZIP,下载接口直接从 OSS 返回 ZIP。无需新增管理员 DELETE HTTP 接口。每日清理任务删除过期 OSS 对象(成功或对象不存在后再删 DB;失败保留 DB 供下次重试)。旧本地报告不迁移。
|
||||
- 管理员列表、筛选、状态和备注全部读取/更新 SpacetimeDB;详情先读表再从 OSS 下载并解析 ZIP,下载接口直接从 OSS 返回 ZIP。无需新增管理员 DELETE HTTP 接口。每日清理任务按分页扫描全部报告,删除过期 OSS 对象后再删 DB;任一 DB 删除失败会保留错误并在后续周期重试。详情解析对解压后的 `events.jsonl` 设置 24 MiB(与请求体上限一致)与 100 条事件上限。旧本地报告不迁移。
|
||||
|
||||
SpacetimeDB `error_report` 表字段:`batch_id` 主键、`user_id`、`submission_id`、`idempotency_key` 唯一键、`object_key`、`archive_sha256`、`archive_size_bytes`、`event_count`、`log_count`、首个 fingerprint/source、`review_status`、`admin_note`、`created_at`、`updated_at`;索引为 `(user_id, submission_id)`、`created_at`、`review_status`。
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ const MAX_LOG_CHARS: usize = 2_000_000;
|
||||
const MAX_BATCH_BYTES: usize = 20 * 1024 * 1024;
|
||||
const MAX_REQUEST_BODY_BYTES: usize = 24 * 1024 * 1024;
|
||||
const MAX_EVENT_FIELD_CHARS: usize = 512;
|
||||
const MAX_EVENTS_JSONL_BYTES: usize = MAX_REQUEST_BODY_BYTES;
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -251,7 +252,7 @@ pub async fn admin_list_error_reports(
|
||||
total,
|
||||
offset,
|
||||
limit,
|
||||
has_more: offset.saturating_add(limit) < total as u32,
|
||||
has_more: (offset as u64).saturating_add(limit as u64) < total,
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -391,7 +392,8 @@ pub async fn admin_download_error_report(
|
||||
);
|
||||
resp.headers_mut().insert(
|
||||
header::CONTENT_DISPOSITION,
|
||||
HeaderValue::from_str(&format!("attachment; filename=\"{batch_id}.zip\"")).unwrap(),
|
||||
HeaderValue::from_str(&format!("attachment; filename=\"{batch_id}.zip\""))
|
||||
.map_err(internal)?,
|
||||
);
|
||||
Ok(resp)
|
||||
}
|
||||
@@ -513,15 +515,26 @@ pub(crate) fn build_error_report_archive(
|
||||
fn parse_archive(bytes: &[u8]) -> Result<(Vec<Event>, Option<String>, Vec<String>), String> {
|
||||
let mut a = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| e.to_string())?;
|
||||
let mut t = String::new();
|
||||
a.by_name("events.jsonl")
|
||||
.map_err(|_| "归档缺少 events.jsonl".to_string())?
|
||||
.read_to_string(&mut t)
|
||||
.map_err(|e| e.to_string())?;
|
||||
{
|
||||
let mut events_file = a
|
||||
.by_name("events.jsonl")
|
||||
.map_err(|_| "归档缺少 events.jsonl".to_string())?
|
||||
.take((MAX_EVENTS_JSONL_BYTES + 1) as u64);
|
||||
events_file
|
||||
.read_to_string(&mut t)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
if t.len() > MAX_EVENTS_JSONL_BYTES {
|
||||
return Err("events.jsonl 解压后超过大小上限".to_string());
|
||||
}
|
||||
let events = t
|
||||
.lines()
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.map(|l| serde_json::from_str(l).map_err(|e| e.to_string()))
|
||||
.collect::<Result<Vec<Event>, _>>()?;
|
||||
if events.len() > MAX_EVENTS {
|
||||
return Err("归档事件数量超过上限".to_string());
|
||||
}
|
||||
let desc = if let Ok(mut f) = a.by_name("user-description.txt") {
|
||||
let mut s = String::new();
|
||||
f.read_to_string(&mut s).map_err(|e| e.to_string())?;
|
||||
@@ -590,23 +603,32 @@ pub fn spawn_cleanup_worker(state: AppState) {
|
||||
}
|
||||
async fn cleanup_expired(state: &AppState) -> Result<(), String> {
|
||||
let cutoff = OffsetDateTime::now_utc() - time::Duration::days(30);
|
||||
let (rows, _) = state
|
||||
.spacetime_client()
|
||||
.list_error_reports(ErrorReportListRecordInput {
|
||||
status: None,
|
||||
fingerprint: None,
|
||||
source: None,
|
||||
limit: 500,
|
||||
offset: 0,
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
for r in rows {
|
||||
if let Ok(ts) = OffsetDateTime::parse(&r.created_at, &Rfc3339) {
|
||||
if ts < cutoff {
|
||||
let Some(oss) = state.oss_client() else {
|
||||
continue;
|
||||
};
|
||||
let Some(oss) = state.oss_client() else {
|
||||
return Ok(());
|
||||
};
|
||||
let mut offset = 0_u32;
|
||||
let mut first_error = None;
|
||||
loop {
|
||||
let (rows, _) = state
|
||||
.spacetime_client()
|
||||
.list_error_reports(ErrorReportListRecordInput {
|
||||
status: None,
|
||||
fingerprint: None,
|
||||
source: None,
|
||||
limit: 500,
|
||||
offset,
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
if rows.is_empty() {
|
||||
break;
|
||||
}
|
||||
let page_len = rows.len() as u32;
|
||||
let mut deleted = 0_u32;
|
||||
for r in rows {
|
||||
if let Ok(ts) = OffsetDateTime::parse(&r.created_at, &Rfc3339)
|
||||
&& ts < cutoff
|
||||
{
|
||||
if oss
|
||||
.delete_object(
|
||||
state.editor_oss_http_client(),
|
||||
@@ -619,12 +641,23 @@ async fn cleanup_expired(state: &AppState) -> Result<(), String> {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let _ = state
|
||||
if let Err(error) = state
|
||||
.spacetime_client()
|
||||
.delete_error_report(r.batch_id)
|
||||
.await;
|
||||
.await
|
||||
{
|
||||
first_error.get_or_insert_with(|| error.to_string());
|
||||
} else {
|
||||
deleted += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if page_len < 500 {
|
||||
break;
|
||||
}
|
||||
if deleted == 0 {
|
||||
offset = offset.saturating_add(page_len);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
first_error.map_or(Ok(()), Err)
|
||||
}
|
||||
|
||||
@@ -867,7 +867,7 @@ impl OssClient {
|
||||
let key = normalize_internal_object_key(&request.object_key)?;
|
||||
let target = build_object_url(&self.config.bucket, &self.config.endpoint, &key)
|
||||
.map_err(|e| request_error(OssRequestOperation::Get, &e.to_string()))?;
|
||||
let response = send_signed_request(
|
||||
let mut response = send_signed_request(
|
||||
client,
|
||||
&self.config,
|
||||
Method::GET,
|
||||
@@ -886,14 +886,24 @@ impl OssClient {
|
||||
format!("OSS GET Object 失败,状态码:{}", response.status()),
|
||||
));
|
||||
}
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| request_error_from_reqwest(OssRequestOperation::Get, e))?;
|
||||
if bytes.len() > request.max_bytes {
|
||||
if response
|
||||
.content_length()
|
||||
.is_some_and(|length| length > request.max_bytes as u64)
|
||||
{
|
||||
return Err(OssError::InvalidRequest("OSS 对象超过读取上限".to_string()));
|
||||
}
|
||||
Ok(bytes.to_vec())
|
||||
let mut bytes = Vec::new();
|
||||
while let Some(chunk) = response
|
||||
.chunk()
|
||||
.await
|
||||
.map_err(|e| request_error_from_reqwest(OssRequestOperation::Get, e))?
|
||||
{
|
||||
if bytes.len().saturating_add(chunk.len()) > request.max_bytes {
|
||||
return Err(OssError::InvalidRequest("OSS 对象超过读取上限".to_string()));
|
||||
}
|
||||
bytes.extend_from_slice(&chunk);
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub async fn delete_object(
|
||||
|
||||
@@ -19,6 +19,7 @@ pub struct ErrorReportRecord {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ErrorReportCreateRecordInput {
|
||||
pub batch_id: String,
|
||||
pub user_id: String,
|
||||
@@ -33,6 +34,7 @@ pub struct ErrorReportCreateRecordInput {
|
||||
pub first_source: Option<String>,
|
||||
pub now_micros: i64,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ErrorReportListRecordInput {
|
||||
pub status: Option<String>,
|
||||
pub fingerprint: Option<String>,
|
||||
@@ -40,6 +42,7 @@ pub struct ErrorReportListRecordInput {
|
||||
pub limit: u32,
|
||||
pub offset: u32,
|
||||
}
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ErrorReportUpdateRecordInput {
|
||||
pub batch_id: String,
|
||||
pub status: String,
|
||||
|
||||
Reference in New Issue
Block a user