feat: async ai workbench tasks

This commit is contained in:
richjohnrambo
2026-09-30 11:37:49 +08:00
parent 47a10c93be
commit d2e70efd9d
4 changed files with 493 additions and 104 deletions
+44 -99
View File
@@ -118,48 +118,28 @@ func (a *API) aiDocumentGenerate(w http.ResponseWriter, r *http.Request, p *auth
return
}
taskID := "aitask_" + randomToken(12)
started := time.Now().UTC().Round(time.Millisecond)
snapshot, err := aiTaskSnapshot(aiDocumentTaskSnapshot{
SourceText: source,
TitleHint: strings.TrimSpace(body.TitleHint),
DocumentType: strings.TrimSpace(body.DocumentType),
Language: strings.TrimSpace(body.Language),
})
if err != nil {
writeStoreError(w, r, err)
return
}
if err := st.CreateAITask(r.Context(), store.AITask{
TaskID: taskID, TenantCode: p.TenantCode, UserID: strconv.FormatInt(p.UserID, 10), Feature: "smart_document",
Status: "processing", Progress: 10, SourceType: "text", InputHash: hashText(source),
Status: "queued", Progress: 0, SourceType: "text", InputHash: hashText(source),
InputSnapshot: snapshot,
PromptVersion: "smart_document_v1", Model: "",
StartedAt: &started,
}, nil); err != nil {
writeStoreError(w, r, err)
return
}
result, err := a.ai.GenerateDocument(r.Context(), source, strings.TrimSpace(body.TitleHint), strings.TrimSpace(body.DocumentType), strings.TrimSpace(body.Language))
if err != nil {
_ = st.UpdateAITaskResult(r.Context(), taskID, "failed", 100, "", nil, aiErrorReason(err), err.Error())
_ = st.InsertAIUsageLog(r.Context(), store.AIUsageLog{
TenantCode: p.TenantCode, UserID: formatUserID(p.UserID), Feature: "smart_document", TaskID: taskID,
Provider: "deepseek", Status: "failed", ErrorReason: aiErrorReason(err), InputHash: hashText(source),
})
a.writeAIError(w, r, err)
return
}
taskResult := map[string]any{
"title": result.Title,
"summary": result.Summary,
"content_markdown": result.ContentMarkdown,
"content_plain": result.ContentPlain,
"sections": result.Sections,
"source_excerpt": excerpt(source, 200),
"can_save": true,
}
if err := st.UpdateAITaskResult(r.Context(), taskID, "succeeded", 100, "", taskResult, "", ""); err != nil {
writeStoreError(w, r, err)
return
}
_ = st.InsertAIUsageLog(r.Context(), store.AIUsageLog{
TenantCode: p.TenantCode, UserID: formatUserID(p.UserID), Feature: "smart_document", TaskID: taskID,
Provider: "deepseek", Model: result.Usage.Model, ProviderRequestID: result.Usage.ProviderRequestID,
PromptTokens: result.Usage.PromptTokens, CompletionTokens: result.Usage.CompletionTokens, TotalTokens: result.Usage.TotalTokens,
LatencyMS: int(result.Usage.LatencyMS), Status: "succeeded", InputHash: hashText(source), OutputHash: hashText(result.ContentMarkdown),
})
writeSuccess(w, r, map[string]any{
"task_id": taskID,
"status": "succeeded",
"status": "queued",
"polling_interval_ms": 1500,
"expires_at": time.Now().UTC().Add(2 * time.Hour).Format(time.RFC3339Nano),
})
@@ -179,6 +159,13 @@ func (a *API) aiTaskResource(w http.ResponseWriter, r *http.Request, p *auth.Pri
writeStoreError(w, r, err)
return
}
feature := ""
if task, err := st.AITask(r.Context(), p.UserID, taskID); err == nil {
feature = task.Feature
}
a.publishAITaskEvent(r.Context(), p.TenantID, p.TenantCode, p.UserID, taskID, "ai", "AI 任务已取消", "", map[string]any{
"task_id": taskID, "feature": feature, "status": "canceled", "progress": 100, "result_type": "none",
})
writeSuccess(w, r, map[string]any{"task_id": taskID, "status": "canceled", "canceled_at": time.Now().UTC().Format(time.RFC3339Nano)})
return
}
@@ -370,50 +357,23 @@ func (a *API) aiTableAnalysisCreate(w http.ResponseWriter, r *http.Request, p *a
}
taskID := "aitask_tbl_" + randomToken(12)
recordID := "airec_tbl_" + randomToken(10)
started := time.Now().UTC().Round(time.Millisecond)
snapshot, err := aiTaskSnapshot(aiTableTaskSnapshot{
FileID: fileID, FileName: strings.TrimSpace(body.FileName), FileType: fileType, FileSize: fileSize,
Topic: topic, SheetName: strings.TrimSpace(body.SheetName), Language: strings.TrimSpace(body.Language), RecordID: recordID,
})
if err != nil {
writeStoreError(w, r, err)
return
}
if err := st.CreateAITask(r.Context(), store.AITask{
TaskID: taskID, TenantCode: p.TenantCode, UserID: strconv.FormatInt(p.UserID, 10), Feature: "smart_table",
Status: "processing", Progress: 20, SourceType: "file", SourceRefID: fileID, InputHash: hashText(fileID + ":" + topic),
PromptVersion: "smart_table_v1", StartedAt: &started,
Status: "queued", Progress: 0, SourceType: "file", SourceRefID: fileID, InputHash: hashText(fileID + ":" + topic),
InputSnapshot: snapshot, PromptVersion: "smart_table_v1",
}, nil); err != nil {
writeStoreError(w, r, err)
return
}
profile := a.tableProfileForAI(r, fileID, fileMeta, fileType)
result, err := a.ai.AnalyzeTable(r.Context(), ai.TableAnalysisInput{
Topic: topic, FileID: fileID, FileName: strings.TrimSpace(body.FileName), FileType: fileType, FileSize: fileSize, Profile: profile,
})
if err != nil {
_ = st.UpdateAITaskResult(r.Context(), taskID, "failed", 100, "", nil, aiErrorReason(err), err.Error())
a.writeAIError(w, r, err)
return
}
metricsJSON := mustJSON(result.Metrics, "[]")
previewJSON := mustJSON(result.Preview, "{}")
alertsJSON := mustJSON(result.Alerts, "[]")
if err := st.SaveAITableAnalysis(r.Context(), p.TenantCode, p.UserID, store.AIRecord{
RecordID: recordID, RecordType: "smart_table", Title: result.Title, Summary: excerpt(firstNonEmpty(result.Summary, result.Interpretation), 160),
Status: "succeeded", TaskID: taskID,
}, store.AITableAnalysis{
RecordID: recordID, FileID: fileID, FileName: strings.TrimSpace(body.FileName), FileType: fileType, FileSize: fileSize,
Topic: topic, MetricsJSON: metricsJSON, PreviewJSON: previewJSON, AlertsJSON: alertsJSON, Interpretation: result.Interpretation,
PromptVersion: "smart_table_v1", Model: result.Usage.Model,
}); err != nil {
writeStoreError(w, r, err)
return
}
taskResult := map[string]any{"record_id": recordID, "title": result.Title, "metrics": result.Metrics, "alerts": result.Alerts, "interpretation": result.Interpretation}
if err := st.UpdateAITaskResult(r.Context(), taskID, "succeeded", 100, recordID, taskResult, "", ""); err != nil {
writeStoreError(w, r, err)
return
}
_ = st.InsertAIUsageLog(r.Context(), store.AIUsageLog{
TenantCode: p.TenantCode, UserID: formatUserID(p.UserID), Feature: "smart_table", TaskID: taskID,
Provider: "deepseek", Model: result.Usage.Model, ProviderRequestID: result.Usage.ProviderRequestID,
PromptTokens: result.Usage.PromptTokens, CompletionTokens: result.Usage.CompletionTokens, TotalTokens: result.Usage.TotalTokens,
LatencyMS: int(result.Usage.LatencyMS), Status: "succeeded", InputHash: hashText(fileID + ":" + topic), OutputHash: hashText(result.Interpretation),
})
writeSuccess(w, r, map[string]any{"task_id": taskID, "record_id": recordID, "status": "succeeded", "polling_interval_ms": 1500})
writeSuccess(w, r, map[string]any{"task_id": taskID, "record_id": recordID, "status": "queued", "polling_interval_ms": 1500})
}
func (a *API) aiTableAnalysisResource(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store) {
@@ -532,8 +492,7 @@ func (a *API) aiMeetingSummaryCreate(w http.ResponseWriter, r *http.Request, p *
if _, ok := a.requireAICapability(w, r, st, "meeting_summary"); !ok {
return
}
meeting, err := st.Meeting(r.Context(), p.UserID, meetingID)
if err != nil {
if _, err := st.Meeting(r.Context(), p.UserID, meetingID); err != nil {
writeStoreError(w, r, err)
return
}
@@ -542,40 +501,26 @@ func (a *API) aiMeetingSummaryCreate(w http.ResponseWriter, r *http.Request, p *
writeStoreError(w, r, err)
return
}
fileNames, fileIDs := meetingSummaryFiles(files, body.FileIDs)
_, fileIDs := meetingSummaryFiles(files, body.FileIDs)
taskID := "aitask_mtg_" + randomToken(12)
summaryID := "aisum_" + randomToken(10)
started := time.Now().UTC().Round(time.Millisecond)
snapshot, err := aiTaskSnapshot(aiMeetingSummaryTaskSnapshot{
MeetingID: meetingID, SummaryID: summaryID, FileIDs: fileIDs,
SummaryType: strings.TrimSpace(body.SummaryType), ForceRegenerate: body.ForceRegenerate,
})
if err != nil {
writeStoreError(w, r, err)
return
}
if err := st.CreateAITask(r.Context(), store.AITask{
TaskID: taskID, TenantCode: p.TenantCode, UserID: strconv.FormatInt(p.UserID, 10), Feature: "meeting_summary",
Status: "processing", Progress: 20, SourceType: "meeting", SourceRefID: meetingID, InputHash: hashText(meetingID + strings.Join(fileIDs, ",")),
PromptVersion: "meeting_summary_v1", StartedAt: &started,
Status: "queued", Progress: 0, SourceType: "meeting", SourceRefID: meetingID, InputHash: hashText(meetingID + strings.Join(fileIDs, ",")),
InputSnapshot: snapshot, PromptVersion: "meeting_summary_v1",
}, nil); err != nil {
writeStoreError(w, r, err)
return
}
result, err := a.ai.SummarizeMeeting(r.Context(), ai.MeetingSummaryInput{
MeetingTitle: meeting.Title, StartAt: meeting.StartAt.UTC().Format(time.RFC3339Nano), EndAt: meeting.EndAt.UTC().Format(time.RFC3339Nano),
Participants: meetingSummaryParticipants(*meeting), Files: fileNames,
})
if err != nil {
_ = st.UpdateAITaskResult(r.Context(), taskID, "failed", 100, "", nil, aiErrorReason(err), err.Error())
a.writeAIError(w, r, err)
return
}
if err := st.UpsertAIMeetingSummary(r.Context(), p.TenantCode, p.UserID, store.AIMeetingSummary{
SummaryID: summaryID, MeetingID: meetingID, TaskID: taskID,
ConclusionsJSON: mustJSON(result.Conclusions, "[]"), DecisionsJSON: mustJSON(result.Decisions, "[]"),
ActionItemsJSON: mustJSON(result.ActionItems, "[]"), SourceFileIDsJSON: mustJSON(fileIDs, "[]"),
PromptVersion: "meeting_summary_v1", Model: result.Usage.Model,
}); err != nil {
writeStoreError(w, r, err)
return
}
_ = st.UpdateAITaskResult(r.Context(), taskID, "succeeded", 100, summaryID, map[string]any{
"summary_id": summaryID, "meeting_id": meetingID, "conclusions": result.Conclusions, "decisions": result.Decisions, "action_items": result.ActionItems,
}, "", "")
writeSuccess(w, r, map[string]any{"task_id": taskID, "meeting_id": meetingID, "status": "succeeded", "polling_interval_ms": 1500})
writeSuccess(w, r, map[string]any{"task_id": taskID, "meeting_id": meetingID, "summary_id": summaryID, "status": "queued", "polling_interval_ms": 1500})
}
func (a *API) aiMeetingSummaryUpdate(w http.ResponseWriter, r *http.Request, p *auth.Principal, st *store.Store, meetingID string) {