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:
@@ -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)
|
||||
|
||||
@@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user