feat(t207): audit local procurement results
This commit is contained in:
@@ -756,6 +756,7 @@ type taskDetailView struct {
|
||||
UpdatedAt time.Time
|
||||
CanCancel bool
|
||||
CancelRequiresAck bool
|
||||
ExecutionReport *ExecutionReport
|
||||
}
|
||||
|
||||
type taskDetailPage struct {
|
||||
@@ -789,6 +790,7 @@ func taskDetailViewFrom(task Task) taskDetailView {
|
||||
CanCancel: canCancelTaskStatus(task.Status),
|
||||
CancelRequiresAck: task.Status == "RUNNING" ||
|
||||
task.Status == "WAITING_CONFIRMATION",
|
||||
ExecutionReport: task.ExecutionReport,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,62 @@ textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.execution-audit {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.audit-summary {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.audit-list {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.audit-list li {
|
||||
margin: 6px 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.audit-evidence-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.audit-evidence {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.audit-evidence img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 240px;
|
||||
object-fit: contain;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface-soft);
|
||||
}
|
||||
|
||||
.audit-evidence figcaption {
|
||||
margin-top: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.audit-json {
|
||||
max-height: 360px;
|
||||
margin: 8px 0 16px;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--surface-soft);
|
||||
font: 12px/1.45 ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
|
||||
@@ -73,6 +73,39 @@
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{{with .Task.ExecutionReport}}
|
||||
<section class="content-section execution-audit" aria-labelledby="execution-audit-heading">
|
||||
<h2 id="execution-audit-heading">执行审计</h2>
|
||||
{{if .Outcome}}
|
||||
<dl class="definition-list audit-summary">
|
||||
<dt>结果类型</dt><dd>{{.Outcome.ResultType}}</dd>
|
||||
<dt>人工理由</dt><dd>{{if .Outcome.OperatorReason}}{{.Outcome.OperatorReason}}{{else}}未提供{{end}}</dd>
|
||||
<dt>订单提交</dt><dd>{{if .Outcome.OrderSubmitted}}是{{else}}否{{end}}</dd>
|
||||
{{if .Outcome.ErrorCode}}<dt>失败信息</dt><dd>{{.Outcome.ErrorCode}}:{{.Outcome.ErrorMessage}}</dd>{{end}}
|
||||
</dl>
|
||||
{{end}}
|
||||
{{if .Mode}}<p class="section-note">模式:{{.Mode}} · 搜索词:{{.SearchQuery}}</p>{{end}}
|
||||
{{if .Provenance}}<h3>本地模型出处</h3><pre class="audit-json">{{.Provenance}}</pre>{{end}}
|
||||
{{if .Candidates}}<h3>候选与评估</h3><pre class="audit-json">{{.Candidates}}</pre>{{end}}
|
||||
{{if .Recommendation}}<h3>本地推荐</h3><pre class="audit-json">{{.Recommendation}}</pre>{{end}}
|
||||
{{if .Evidence}}
|
||||
<h3>证据截图</h3>
|
||||
<div class="audit-evidence-grid">
|
||||
{{range .Evidence}}<figure class="audit-evidence">
|
||||
<img src="{{.ContentURL}}" alt="执行证据截图">
|
||||
<figcaption>SHA-256 {{.SHA256}}({{.SizeBytes}} bytes){{if .ReceivedAfterExecutionExpiry}},授权到期后补报{{end}}</figcaption>
|
||||
</figure>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Events}}
|
||||
<h3>执行事件</h3>
|
||||
<ul class="audit-list">
|
||||
{{range .Events}}<li><time datetime="{{machineTime .OccurredAt}}">{{displayTime .OccurredAt}}</time> · {{.Step}} · {{.Message}}{{if .ReceivedAfterExecutionExpiry}}(授权到期后补报){{end}}</li>{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
{{if .Task.CanCancel}}
|
||||
<noscript>
|
||||
<section class="noscript-cancel" aria-labelledby="noscript-cancel-heading">
|
||||
|
||||
@@ -58,6 +58,47 @@ type Task struct {
|
||||
ReferenceAssetID string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ExecutionReport *ExecutionReport
|
||||
}
|
||||
|
||||
type ExecutionReport struct {
|
||||
Events []ExecutionReportEvent
|
||||
Evidence []ExecutionReportEvidence
|
||||
Mode string
|
||||
SearchQuery string
|
||||
Provenance string
|
||||
Candidates string
|
||||
Recommendation string
|
||||
Outcome *ExecutionReportOutcome
|
||||
}
|
||||
|
||||
type ExecutionReportEvent struct {
|
||||
Step string
|
||||
Type string
|
||||
Message string
|
||||
OccurredAt time.Time
|
||||
ReceivedAfterExecutionExpiry bool
|
||||
}
|
||||
|
||||
type ExecutionReportEvidence struct {
|
||||
ID string
|
||||
ContentURL string
|
||||
SHA256 string
|
||||
SizeBytes int64
|
||||
CreatedAt time.Time
|
||||
ReceivedAfterExecutionExpiry bool
|
||||
}
|
||||
|
||||
type ExecutionReportOutcome struct {
|
||||
ResultType string
|
||||
Outcome string
|
||||
OperatorReason string
|
||||
ErrorCode string
|
||||
ErrorMessage string
|
||||
ErrorStep string
|
||||
OrderSubmitted bool
|
||||
ReceivedAt time.Time
|
||||
ReceivedAfterExecutionExpiry bool
|
||||
}
|
||||
|
||||
type UploadReferenceInput struct {
|
||||
|
||||
@@ -2,7 +2,9 @@ package webui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"cmroubao/backend-api/internal/domain"
|
||||
"cmroubao/backend-api/internal/transport/authcommon"
|
||||
@@ -176,9 +178,82 @@ func taskFromPurchase(task domain.PurchaseTask) Task {
|
||||
func taskFromDetail(detail domain.TaskDetail) Task {
|
||||
task := taskFromPurchase(detail.Task)
|
||||
task.ReferenceAssetID = detail.Asset.ID
|
||||
task.ExecutionReport = executionReportFrom(detail.Report)
|
||||
return task
|
||||
}
|
||||
|
||||
func executionReportFrom(report *domain.ExecutionReport) *ExecutionReport {
|
||||
if report == nil {
|
||||
return nil
|
||||
}
|
||||
result := &ExecutionReport{
|
||||
Events: make([]ExecutionReportEvent, 0, len(report.Events)),
|
||||
Evidence: make([]ExecutionReportEvidence, 0, len(report.EvidenceAssets)),
|
||||
}
|
||||
for _, event := range report.Events {
|
||||
result.Events = append(result.Events, ExecutionReportEvent{
|
||||
Step: event.Step,
|
||||
Type: event.Type,
|
||||
Message: event.Message,
|
||||
OccurredAt: event.OccurredAt,
|
||||
ReceivedAfterExecutionExpiry: event.ReceivedAfterExecutionExpiry,
|
||||
})
|
||||
}
|
||||
for _, evidence := range report.EvidenceAssets {
|
||||
result.Evidence = append(result.Evidence, ExecutionReportEvidence{
|
||||
ID: evidence.ID,
|
||||
ContentURL: "/api/v1/tasks/" + evidence.TaskID + "/evidence/" + evidence.ID + "/content",
|
||||
SHA256: evidence.SHA256,
|
||||
SizeBytes: evidence.SizeBytes,
|
||||
CreatedAt: evidence.CreatedAt,
|
||||
ReceivedAfterExecutionExpiry: evidence.ReceivedAfterExecutionExpiry,
|
||||
})
|
||||
}
|
||||
if batch := report.CandidateBatch; batch != nil {
|
||||
result.Mode = batch.ExecutionMode
|
||||
result.SearchQuery = batch.SearchQuery
|
||||
result.Provenance = prettyAuditJSON(batch.ProvenanceJSON)
|
||||
result.Candidates = prettyAuditJSON(&batch.CandidatesJSON)
|
||||
result.Recommendation = prettyAuditJSON(batch.RecommendationJSON)
|
||||
}
|
||||
if outcome := report.Outcome; outcome != nil {
|
||||
result.Outcome = &ExecutionReportOutcome{
|
||||
ResultType: outcome.ResultType,
|
||||
Outcome: stringValue(outcome.Outcome),
|
||||
OperatorReason: stringValue(outcome.OperatorReason),
|
||||
ErrorCode: stringValue(outcome.ErrorCode),
|
||||
ErrorMessage: stringValue(outcome.ErrorMessage),
|
||||
ErrorStep: stringValue(outcome.ErrorStep),
|
||||
OrderSubmitted: outcome.OrderSubmitted,
|
||||
ReceivedAt: outcome.ReceivedAt,
|
||||
ReceivedAfterExecutionExpiry: outcome.ReceivedAfterExecutionExpiry,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func prettyAuditJSON(value *string) string {
|
||||
if value == nil || strings.TrimSpace(*value) == "" {
|
||||
return ""
|
||||
}
|
||||
var decoded any
|
||||
if err := json.Unmarshal([]byte(*value), &decoded); err != nil {
|
||||
return ""
|
||||
}
|
||||
formatted, err := json.MarshalIndent(decoded, "", " ")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(formatted)
|
||||
}
|
||||
|
||||
func stringValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func mapUsecaseError(err error) error {
|
||||
var typed *usecase.Error
|
||||
if !errors.As(err, &typed) {
|
||||
|
||||
Reference in New Issue
Block a user