feat(api): enqueue material parse on upload and expose parse_state

Async-trigger MinerU after CAS store and surface parse status in
assets API plus a lightweight preview badge.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-21 08:29:10 +00:00
co-authored by Cursor
parent 9ee890950a
commit 79c4c7c496
3 changed files with 71 additions and 16 deletions
+40 -16
View File
@@ -7,16 +7,18 @@ import (
"strconv"
"strings"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/upload"
)
type projectAssetResponse struct {
AssetID int64 `json:"asset_id"`
Filename string `json:"filename"`
SHA256 string `json:"sha256"`
MimeType string `json:"mime_type"`
SizeBytes int64 `json:"size_bytes"`
ProjectID int64 `json:"project_id"`
AssetID int64 `json:"asset_id"`
Filename string `json:"filename"`
SHA256 string `json:"sha256"`
MimeType string `json:"mime_type"`
SizeBytes int64 `json:"size_bytes"`
ProjectID int64 `json:"project_id"`
ParseState string `json:"parse_state,omitempty"`
}
func (s *Server) handleUploadProjectAsset(w http.ResponseWriter, r *http.Request) {
@@ -77,13 +79,22 @@ func (s *Server) handleUploadProjectAsset(w http.ResponseWriter, r *http.Request
return
}
parseState := ""
if matIdx := bizdev.Snapshot(s.svc).MaterialIndex; matIdx != nil {
matIdx.EnqueueAsync(ref.FileName, ref.BlobSHA256, data, projectID)
if matIdx.Enabled() {
parseState = "pending"
}
}
writeJSONResponse(w, http.StatusOK, projectAssetResponse{
AssetID: ref.ID,
Filename: ref.FileName,
SHA256: ref.BlobSHA256,
MimeType: mimeType,
SizeBytes: int64(len(data)),
ProjectID: projectID,
AssetID: ref.ID,
Filename: ref.FileName,
SHA256: ref.BlobSHA256,
MimeType: mimeType,
SizeBytes: int64(len(data)),
ProjectID: projectID,
ParseState: parseState,
})
}
@@ -106,13 +117,26 @@ func (s *Server) handleListProjectAssets(w http.ResponseWriter, r *http.Request)
writeSafeError(w, r, http.StatusInternalServerError, err.Error())
return
}
shas := make([]string, 0, len(refs))
for _, ref := range refs {
if ref != nil && ref.BlobSHA256 != "" {
shas = append(shas, ref.BlobSHA256)
}
}
states := map[string]string{}
if matIdx := bizdev.Snapshot(s.svc).MaterialIndex; matIdx != nil {
states = matIdx.GetParseStates(r.Context(), shas)
}
out := make([]projectAssetResponse, 0, len(refs))
for _, ref := range refs {
out = append(out, projectAssetResponse{
AssetID: ref.ID,
Filename: ref.FileName,
SHA256: ref.BlobSHA256,
ProjectID: ref.ProjectID,
AssetID: ref.ID,
Filename: ref.FileName,
SHA256: ref.BlobSHA256,
ProjectID: ref.ProjectID,
ParseState: states[ref.BlobSHA256],
})
}
writeJSONResponse(w, http.StatusOK, out)
+2
View File
@@ -7,6 +7,8 @@ export type ProjectAsset = {
mime_type?: string;
size_bytes?: number;
project_id: number;
/** MinerU 解析状态:pending | running | done | failed */
parse_state?: string;
};
export async function uploadProjectAsset(
@@ -38,6 +38,7 @@ export const FilePreviewPanel: React.FC<FilePreviewPanelProps> = ({
const [isFullscreen, setIsFullscreen] = useState(false);
const [recoveredBlob, setRecoveredBlob] = useState<Blob | null>(null);
const [recoverState, setRecoverState] = useState<'idle' | 'loading' | 'done' | 'miss'>('idle');
const [parseState, setParseState] = useState<string>('');
const fileMeta = deliverable.fileMeta;
const fileName = fileMeta?.originalName || deliverable.title;
@@ -71,6 +72,9 @@ export const FilePreviewPanel: React.FC<FilePreviewPanelProps> = ({
const match = assets.find((a) => (a.filename || '').trim().toLowerCase() === want)
|| assets.find((a) => want && (a.filename || '').toLowerCase().includes(want.replace(/\.[^.]+$/, '')));
assetId = match?.asset_id ?? 0;
if (match?.parse_state) {
setParseState(match.parse_state);
}
}
if (!assetId) {
if (!cancelled) setRecoverState('miss');
@@ -503,6 +507,31 @@ export const FilePreviewPanel: React.FC<FilePreviewPanelProps> = ({
<span>{badge.label}</span>
<span>·</span>
<span>{fileSize}</span>
{parseState ? (
<>
<span>·</span>
<span
title="MinerU 文档解析状态"
style={{
color:
parseState === 'done'
? '#059669'
: parseState === 'failed'
? '#dc2626'
: '#d97706',
fontWeight: 600,
}}
>
{parseState === 'done'
? '已解析'
: parseState === 'failed'
? '解析失败'
: parseState === 'running'
? '解析中'
: '待解析'}
</span>
</>
) : null}
<span>·</span>
<span
style={{