Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a5ef6b75c | ||
|
|
fcfe8149bd | ||
|
|
00f182611c |
+46
-13
@@ -12,19 +12,52 @@ import "time"
|
||||
type ActivityCard struct {
|
||||
ID ID `json:"id"`
|
||||
// ProjectID/ConversationID 定位归属;ConversationID 为空表示项目级卡片(远期跨会话收件箱)。
|
||||
ProjectID ID `json:"projectId"`
|
||||
ConversationID ID `json:"conversationId,omitempty"`
|
||||
SourceRunID ID `json:"sourceRunId,omitempty"` // 溯源:产生卡片的 run
|
||||
AnchorMessageID ID `json:"anchorMessageId,omitempty"` // 互定位:出生消息(↙ 定位到对话)
|
||||
Kind CardKind `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Status CardStatus `json:"status"`
|
||||
Tone string `json:"tone,omitempty"` // ok | warn | danger | info
|
||||
Body []UINode `json:"body,omitempty"`
|
||||
Actions []UIAction `json:"actions,omitempty"` // 走命令总线;未绑定 command 降级为发消息
|
||||
Pinned bool `json:"pinned,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ProjectID ID `json:"projectId"`
|
||||
ConversationID ID `json:"conversationId,omitempty"`
|
||||
SourceRunID ID `json:"sourceRunId,omitempty"` // 溯源:产生卡片的 run
|
||||
AnchorMessageID ID `json:"anchorMessageId,omitempty"` // 互定位:出生消息(↙ 定位到对话)
|
||||
Kind CardKind `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
Status CardStatus `json:"status"`
|
||||
Tone string `json:"tone,omitempty"` // ok | warn | danger | info
|
||||
Body []UINode `json:"body,omitempty"`
|
||||
Actions []UIAction `json:"actions,omitempty"` // 走命令总线;未绑定 command 降级为发消息
|
||||
Pinned bool `json:"pinned,omitempty"`
|
||||
Work *WorkCard `json:"work,omitempty"` // optional background-work snapshot; never a foreground approval
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// WorkCard keeps the complete saved result and an optional, non-blocking next step
|
||||
// on the original task. Reading and answering do not change the result version.
|
||||
type WorkCard struct {
|
||||
Result string `json:"result,omitempty"`
|
||||
ResultVersion int `json:"resultVersion"`
|
||||
ReadVersion int `json:"readVersion"`
|
||||
CompletedAt *time.Time `json:"completedAt,omitempty"`
|
||||
Decision *WorkDecision `json:"decision,omitempty"`
|
||||
}
|
||||
|
||||
type WorkDecision struct {
|
||||
RunID ID `json:"runId,omitempty"` // reserved child identity; reconciles acceptance after restart without rerunning
|
||||
Mode string `json:"mode"` // single | multiple
|
||||
State string `json:"state"` // open | deferred | dispatching | submitted
|
||||
Options []WorkOption `json:"options,omitempty"`
|
||||
SelectedIDs []string `json:"selectedIds,omitempty"`
|
||||
FreeText string `json:"freeText,omitempty"`
|
||||
RespondedAt *time.Time `json:"respondedAt,omitempty"`
|
||||
}
|
||||
|
||||
type WorkOption struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
|
||||
// WorkCardAction is version-fenced. Presentation-only opens never mark a result read.
|
||||
type WorkCardAction struct {
|
||||
ResultVersion int `json:"resultVersion"`
|
||||
SelectedIDs []string `json:"selectedIds,omitempty"`
|
||||
FreeText string `json:"freeText,omitempty"`
|
||||
}
|
||||
|
||||
type CardKind string
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWorkCardSurvivesPersistedSnapshot(t *testing.T) {
|
||||
completed := time.Date(2026, 10, 15, 6, 0, 0, 0, time.UTC)
|
||||
card := ActivityCard{ID: "card_work", Status: CardDone, Work: &WorkCard{
|
||||
Result: strings.Repeat("完整结果\n", 200) + "最后一项依据",
|
||||
ResultVersion: 2, ReadVersion: 1, CompletedAt: &completed,
|
||||
Decision: &WorkDecision{Mode: "multiple", State: "submitted", Options: []WorkOption{{ID: "summary", Label: "生成摘要"}},
|
||||
SelectedIDs: []string{"summary"}, FreeText: "请突出关键限制", RespondedAt: &completed},
|
||||
}}
|
||||
data, err := json.Marshal(card)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var restored ActivityCard
|
||||
if err := json.Unmarshal(data, &restored); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if restored.Work == nil || restored.Work.Result != card.Work.Result || restored.Work.ReadVersion != 1 || restored.Work.ResultVersion != 2 {
|
||||
t.Fatalf("complete result or unread version lost: %+v", restored.Work)
|
||||
}
|
||||
if d := restored.Work.Decision; d.State != "submitted" || d.SelectedIDs[0] != "summary" || d.FreeText != "请突出关键限制" || !d.RespondedAt.Equal(completed) {
|
||||
t.Fatalf("decision history lost: %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyCardOmitsOptionalWork(t *testing.T) {
|
||||
var legacy ActivityCard
|
||||
if err := json.Unmarshal([]byte(`{"id":"old","status":"resolved","body":[{"type":"markdown","markdown":"保留历史"}]}`), &legacy); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := json.Marshal(legacy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if legacy.Work != nil || strings.Contains(string(data), `"work"`) || legacy.Body[0].Markdown != "保留历史" {
|
||||
t.Fatalf("legacy payload changed: %s", data)
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -150,7 +150,21 @@ export interface ActivityCard {
|
||||
sourceRunId?: ID; anchorMessageId?: ID;
|
||||
kind: CardKind; title: string; status: CardStatus; tone?: string;
|
||||
body?: UINode[]; actions?: { id: string; label: string; style?: 'primary' | 'default' | 'danger'; command?: string }[];
|
||||
pinned?: boolean; createdAt: string; updatedAt: string;
|
||||
pinned?: boolean; createdAt: string; updatedAt: string; work?: WorkCard;
|
||||
}
|
||||
|
||||
export interface WorkCard {
|
||||
result?: string; resultVersion: number; readVersion: number; completedAt?: string;
|
||||
decision?: WorkDecision;
|
||||
}
|
||||
export interface WorkDecision {
|
||||
runId?: ID;
|
||||
mode: 'single' | 'multiple'; state: 'open' | 'deferred' | 'dispatching' | 'submitted';
|
||||
options?: { id: string; label: string }[];
|
||||
selectedIds?: string[]; freeText?: string; respondedAt?: string;
|
||||
}
|
||||
export interface WorkCardAction {
|
||||
resultVersion: number; selectedIds?: string[]; freeText?: string;
|
||||
}
|
||||
|
||||
export interface CardPayload { card: ActivityCard }
|
||||
|
||||
Reference in New Issue
Block a user