feat(api): add Lovart-style project and agent thread endpoints
Add canva/canda-compatible routes for lightweight project open/save and agent thread history: - POST /canva/project/queryProject, saveProject - POST /canva/agent/queryAgentLastThread, agentThreadList - GET /canda/chat-history, /canda/thread/status Canvas save now accepts a compressed SHAKKERDATA:// snapshot payload, preserves connections, and returns a SaveProject response; blank successful image nodes are backfilled from generated artifacts. Frontend designGateway adopts the new save/query flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+53
-4
@@ -177,6 +177,17 @@ Returns all project summaries, sorted by `updatedAt` descending.
|
||||
|
||||
Returns the full project, including viewport, nodes, connections, and agent messages.
|
||||
|
||||
`POST /api/canva/project/queryProject`
|
||||
|
||||
```json
|
||||
{
|
||||
"projectId": "project-id",
|
||||
"cid": "1781596271730ngdozsck"
|
||||
}
|
||||
```
|
||||
|
||||
Returns a Lovart-style project document with `assetMeta`, compressed `canvas`, `validProjectId`, `projectType`, `version`, and permission metadata. The frontend uses this for lightweight project opening, then decodes the `SHAKKERDATA://` canvas locally.
|
||||
|
||||
`PATCH /api/projects/:id/title`
|
||||
|
||||
```json
|
||||
@@ -240,19 +251,57 @@ Returns replayable assistant/user chat history for the project.
|
||||
}
|
||||
```
|
||||
|
||||
Lovart-style agent thread endpoints are also available:
|
||||
|
||||
`POST /api/canva/agent/queryAgentLastThread`
|
||||
|
||||
Returns `{ "code": 0, "msg": null, "data": "<thread-id>" }`.
|
||||
|
||||
`POST /api/canva/agent/agentThreadList`
|
||||
|
||||
Accepts `{ "projectId": "project-id", "page": 1, "pageSize": 100, "cid": "..." }` and returns paginated thread summaries.
|
||||
|
||||
`GET /api/canda/chat-history?thread_id=<thread-id>&project_id=<project-id>`
|
||||
|
||||
Returns Lovart-style thread history items grouped by `thread_meta`.
|
||||
|
||||
`GET /api/canda/thread/status?thread_id=<thread-id>&project_id=<project-id>`
|
||||
|
||||
Returns `{ "code": 0, "msg": "success", "data": { "status": "done" } }` or the current project thread status when `project_id` is present.
|
||||
|
||||
## Save Canvas
|
||||
|
||||
`POST /api/canva/project/saveProject`
|
||||
|
||||
`PATCH /api/projects/:id/canvas`
|
||||
|
||||
```json
|
||||
{
|
||||
"viewport": { "x": 420, "y": 260, "k": 0.72 },
|
||||
"nodes": [],
|
||||
"connections": []
|
||||
"canvas": "SHAKKERDATA://<gzip-base64-canvas-snapshot>",
|
||||
"projectCoverList": ["https://cdn.example.com/canvas/image.png"],
|
||||
"picCount": 1,
|
||||
"projectName": "Untitled",
|
||||
"projectId": "project-id",
|
||||
"version": "1783472149000",
|
||||
"sessionId": "browser-session-id",
|
||||
"canvasV2Gray": true
|
||||
}
|
||||
```
|
||||
|
||||
Saves canvas layout changes. The frontend uses this after dragging or zooming.
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": null,
|
||||
"data": {
|
||||
"projectId": "project-id",
|
||||
"newCreated": false,
|
||||
"validProjectId": true,
|
||||
"version": "1783498482000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Saves canvas layout changes with a Lovart-style lightweight payload. The frontend posts to `/api/canva/project/saveProject`; `/api/projects/:id/canvas` remains as a compatibility route. The `canvas` value is a `SHAKKERDATA://` gzip+base64 encoded snapshot containing viewport, nodes, and connections. Legacy uncompressed `{ "viewport": ..., "nodes": ..., "connections": ... }` payloads are still accepted.
|
||||
|
||||
When a canvas image node disappears from the saved node list, its backing OSS asset is queued for asynchronous deletion after the canvas save succeeds.
|
||||
|
||||
|
||||
@@ -134,6 +134,10 @@ curl -X PATCH http://localhost:8888/api/projects/<project-id>/title \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title":"夏季活动主视觉"}'
|
||||
|
||||
curl -X POST http://localhost:8888/api/canva/project/saveProject \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"canvas":"SHAKKERDATA://<gzip-base64-canvas-snapshot>","projectCoverList":[],"picCount":0,"projectName":"Untitled","projectId":"<project-id>","version":"1783472149000","sessionId":"browser-session-id","canvasV2Gray":true}'
|
||||
|
||||
curl -X POST http://localhost:8888/api/assets/upload-url \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"fileName":"ref.png","contentType":"image/png"}'
|
||||
|
||||
@@ -320,6 +320,11 @@ type ProjectIdRequest {
|
||||
Id string `path:"id"`
|
||||
}
|
||||
|
||||
type QueryProjectRequest {
|
||||
ProjectId string `json:"projectId"`
|
||||
Cid string `json:"cid,optional"`
|
||||
}
|
||||
|
||||
type CreateProjectRequest {
|
||||
Title string `json:"title,optional"`
|
||||
Prompt string `json:"prompt,optional"`
|
||||
@@ -338,6 +343,53 @@ type ProjectResponse {
|
||||
Project Project `json:"project"`
|
||||
}
|
||||
|
||||
type QueryProjectExtra {
|
||||
PicCount int64 `json:"picCount"`
|
||||
ProjectCoverList []string `json:"projectCoverList"`
|
||||
}
|
||||
|
||||
type QueryProjectMeta {
|
||||
UniqId string `json:"uniqId"`
|
||||
ParentUniqId string `json:"parentUniqId"`
|
||||
}
|
||||
|
||||
type QueryProjectAssetMeta {
|
||||
AssetId string `json:"assetId"`
|
||||
ProjectId string `json:"projectId"`
|
||||
Name string `json:"name"`
|
||||
ProjectType int64 `json:"projectType"`
|
||||
ContentType string `json:"contentType"`
|
||||
OwnerType string `json:"ownerType"`
|
||||
OwnerId string `json:"ownerId"`
|
||||
Extra QueryProjectExtra `json:"extra"`
|
||||
Meta QueryProjectMeta `json:"meta"`
|
||||
EffectivePermission string `json:"effectivePermission"`
|
||||
CopyTaskStatus *string `json:"copyTaskStatus"`
|
||||
Visibility string `json:"visibility"`
|
||||
CreateTime string `json:"createTime"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
}
|
||||
|
||||
type QueryProjectData {
|
||||
AssetMeta QueryProjectAssetMeta `json:"assetMeta"`
|
||||
UserId string `json:"userId"`
|
||||
ProjectName string `json:"projectName"`
|
||||
ProjectId *string `json:"projectId"`
|
||||
Canvas string `json:"canvas"`
|
||||
ValidProjectId bool `json:"validProjectId"`
|
||||
ProjectType int64 `json:"projectType"`
|
||||
Version string `json:"version"`
|
||||
SnapshotId *string `json:"snapshotId"`
|
||||
ItemId *string `json:"itemId"`
|
||||
Permission string `json:"permission"`
|
||||
}
|
||||
|
||||
type QueryProjectResponse {
|
||||
Code int64 `json:"code"`
|
||||
Msg *string `json:"msg"`
|
||||
Data QueryProjectData `json:"data"`
|
||||
}
|
||||
|
||||
type DeleteProjectResponse {
|
||||
Id string `json:"id"`
|
||||
Deleted bool `json:"deleted"`
|
||||
@@ -356,6 +408,105 @@ type ChatHistoryResponse {
|
||||
Messages []AgentMessage `json:"messages"`
|
||||
}
|
||||
|
||||
type QueryAgentLastThreadRequest {
|
||||
ProjectId string `json:"projectId"`
|
||||
Cid string `json:"cid,optional"`
|
||||
}
|
||||
|
||||
type QueryAgentLastThreadResponse {
|
||||
Code int64 `json:"code"`
|
||||
Msg *string `json:"msg"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type AgentThreadListRequest {
|
||||
ProjectId string `json:"projectId"`
|
||||
Page int64 `json:"page,optional"`
|
||||
PageSize int64 `json:"pageSize,optional"`
|
||||
Cid string `json:"cid,optional"`
|
||||
}
|
||||
|
||||
type LovartAgentThreadSummary {
|
||||
Id int64 `json:"id"`
|
||||
UserId string `json:"userId"`
|
||||
ProjectId string `json:"projectId"`
|
||||
AgentThreadId string `json:"agentThreadId"`
|
||||
ThreadIdType int64 `json:"threadIdType"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
CoverImageUrl *string `json:"coverImageUrl"`
|
||||
FileType *string `json:"fileType"`
|
||||
UpdateTimestamp int64 `json:"updateTimestamp"`
|
||||
Source int64 `json:"source"`
|
||||
}
|
||||
|
||||
type LovartAgentThreadListData {
|
||||
Page int64 `json:"page"`
|
||||
Total int64 `json:"total"`
|
||||
PageSize int64 `json:"pageSize"`
|
||||
Data []LovartAgentThreadSummary `json:"data"`
|
||||
UserIp string `json:"userIp"`
|
||||
TraceId string `json:"traceId"`
|
||||
HasMore bool `json:"hasMore"`
|
||||
RankExpId *string `json:"rankExpId"`
|
||||
AbVariant *string `json:"abVariant"`
|
||||
}
|
||||
|
||||
type LovartAgentThreadListResponse {
|
||||
Code int64 `json:"code"`
|
||||
Msg *string `json:"msg"`
|
||||
Data LovartAgentThreadListData `json:"data"`
|
||||
}
|
||||
|
||||
type CandaChatHistoryRequest {
|
||||
ThreadId string `form:"thread_id"`
|
||||
ProjectId string `form:"project_id"`
|
||||
}
|
||||
|
||||
type CandaThreadMeta {
|
||||
ActionId string `json:"action_id"`
|
||||
SessionId string `json:"session_id"`
|
||||
StepId string `json:"step_id"`
|
||||
Depth int64 `json:"depth"`
|
||||
AgentName string `json:"agent_name"`
|
||||
ProjectId string `json:"project_id"`
|
||||
}
|
||||
|
||||
type CandaChatHistoryItem {
|
||||
Id string `json:"id"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Type string `json:"type"`
|
||||
ActionId string `json:"action_id"`
|
||||
StepId string `json:"step_id"`
|
||||
Text string `json:"text,optional"`
|
||||
Content string `json:"content,optional"`
|
||||
Status string `json:"status,optional"`
|
||||
ToolCallId string `json:"tool_call_id,optional"`
|
||||
Name string `json:"name,optional"`
|
||||
ThreadMeta CandaThreadMeta `json:"thread_meta"`
|
||||
}
|
||||
|
||||
type CandaChatHistoryResponse {
|
||||
Code int64 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []CandaChatHistoryItem `json:"data"`
|
||||
}
|
||||
|
||||
type CandaThreadStatusRequest {
|
||||
ThreadId string `form:"thread_id"`
|
||||
ProjectId string `form:"project_id,optional"`
|
||||
}
|
||||
|
||||
type CandaThreadStatusData {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type CandaThreadStatusResponse {
|
||||
Code int64 `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data CandaThreadStatusData `json:"data"`
|
||||
}
|
||||
|
||||
type GenerateRequest {
|
||||
Id string `path:"id"`
|
||||
Prompt string `json:"prompt"`
|
||||
@@ -638,13 +789,47 @@ type MergeLayersRequest {
|
||||
}
|
||||
|
||||
type SaveCanvasRequest {
|
||||
Id string `path:"id"`
|
||||
Version string `json:"version,optional"`
|
||||
SnapshotId string `json:"snapshotId,optional"`
|
||||
Canvas string `json:"canvas,optional"`
|
||||
Viewport CanvasViewport `json:"viewport"`
|
||||
Nodes []CanvasNode `json:"nodes"`
|
||||
Connections []CanvasConnection `json:"connections"`
|
||||
Id string `path:"id"`
|
||||
Version string `json:"version,optional"`
|
||||
SnapshotId string `json:"snapshotId,optional"`
|
||||
Canvas string `json:"canvas,optional"`
|
||||
Viewport CanvasViewport `json:"viewport,optional"`
|
||||
Nodes []CanvasNode `json:"nodes,optional"`
|
||||
Connections []CanvasConnection `json:"connections,optional"`
|
||||
ProjectCoverList []string `json:"projectCoverList,optional"`
|
||||
PicCount int64 `json:"picCount,optional"`
|
||||
ProjectName string `json:"projectName,optional"`
|
||||
ProjectId string `json:"projectId,optional"`
|
||||
SessionId string `json:"sessionId,optional"`
|
||||
CanvasV2Gray bool `json:"canvasV2Gray,optional"`
|
||||
}
|
||||
|
||||
type SaveProjectRequest {
|
||||
Canvas string `json:"canvas,optional"`
|
||||
ProjectCoverList []string `json:"projectCoverList,optional"`
|
||||
PicCount int64 `json:"picCount,optional"`
|
||||
ProjectName string `json:"projectName,optional"`
|
||||
ProjectId string `json:"projectId"`
|
||||
Version string `json:"version,optional"`
|
||||
SessionId string `json:"sessionId,optional"`
|
||||
CanvasV2Gray bool `json:"canvasV2Gray,optional"`
|
||||
SnapshotId string `json:"snapshotId,optional"`
|
||||
Viewport CanvasViewport `json:"viewport,optional"`
|
||||
Nodes []CanvasNode `json:"nodes,optional"`
|
||||
Connections []CanvasConnection `json:"connections,optional"`
|
||||
}
|
||||
|
||||
type SaveProjectData {
|
||||
ProjectId string `json:"projectId"`
|
||||
NewCreated bool `json:"newCreated"`
|
||||
ValidProjectId bool `json:"validProjectId"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type SaveProjectResponse {
|
||||
Code int64 `json:"code"`
|
||||
Msg *string `json:"msg"`
|
||||
Data SaveProjectData `json:"data"`
|
||||
}
|
||||
|
||||
type CreateAssetUploadRequest {
|
||||
@@ -732,6 +917,24 @@ service img_infinite_canvas-api {
|
||||
@handler createProjectAsync
|
||||
post /projects/async (CreateProjectRequest) returns (ProjectResponse)
|
||||
|
||||
@handler queryProject
|
||||
post /canva/project/queryProject (QueryProjectRequest) returns (QueryProjectResponse)
|
||||
|
||||
@handler saveProject
|
||||
post /canva/project/saveProject (SaveProjectRequest) returns (SaveProjectResponse)
|
||||
|
||||
@handler queryAgentLastThread
|
||||
post /canva/agent/queryAgentLastThread (QueryAgentLastThreadRequest) returns (QueryAgentLastThreadResponse)
|
||||
|
||||
@handler agentThreadList
|
||||
post /canva/agent/agentThreadList (AgentThreadListRequest) returns (LovartAgentThreadListResponse)
|
||||
|
||||
@handler candaChatHistory
|
||||
get /canda/chat-history (CandaChatHistoryRequest) returns (CandaChatHistoryResponse)
|
||||
|
||||
@handler candaThreadStatus
|
||||
get /canda/thread/status (CandaThreadStatusRequest) returns (CandaThreadStatusResponse)
|
||||
|
||||
@handler deleteProjects
|
||||
delete /projects/batch-delete (DeleteProjectsRequest) returns (DeleteProjectsResponse)
|
||||
|
||||
@@ -802,7 +1005,7 @@ service img_infinite_canvas-api {
|
||||
post /projects/:id/layers/merge (MergeLayersRequest) returns (ProjectResponse)
|
||||
|
||||
@handler saveCanvas
|
||||
patch /projects/:id/canvas (SaveCanvasRequest) returns (ProjectResponse)
|
||||
patch /projects/:id/canvas (SaveCanvasRequest) returns (SaveProjectResponse)
|
||||
|
||||
@handler createAssetUpload
|
||||
post /assets/upload-url (CreateAssetUploadRequest) returns (CreateAssetUploadResponse)
|
||||
|
||||
@@ -122,6 +122,66 @@ func TestSaveCanvasQueuesDeletedImageAsset(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveCanvasAcceptsCompressedSnapshotPayload(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := memory.NewProjectRepository()
|
||||
service := NewDesignService(repo, nil, nil, nil, nil)
|
||||
service.now = func() time.Time {
|
||||
return time.Unix(101, 0).UTC()
|
||||
}
|
||||
project := design.Project{
|
||||
ID: "project-compressed-save",
|
||||
Title: "Compressed save",
|
||||
Brief: "brief",
|
||||
Status: design.StatusReady,
|
||||
UpdatedAt: time.Unix(100, 0).UTC(),
|
||||
Version: "100000",
|
||||
SnapshotID: "snapshot-old",
|
||||
Viewport: design.Viewport{X: 1, Y: 2, K: 1},
|
||||
Nodes: []design.Node{
|
||||
{ID: "old-image", Type: design.NodeTypeImage, Title: "Old", Content: "http://localhost:19000/canvas-assets/old.png", Status: "success"},
|
||||
},
|
||||
}
|
||||
if err := repo.Save(ctx, project); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
snapshotProject := project
|
||||
snapshotProject.Version = project.Version
|
||||
snapshotProject.SnapshotID = "snapshot-client"
|
||||
snapshotProject.Viewport = design.Viewport{X: 12, Y: 34, K: 0.8}
|
||||
snapshotProject.Nodes = []design.Node{
|
||||
{ID: "new-image", Type: design.NodeTypeImage, Title: "New", Content: "http://localhost:19000/canvas-assets/new.png", Status: "success"},
|
||||
}
|
||||
snapshotProject.Connections = []design.Connection{
|
||||
{ID: "connection-1", FromNodeID: "old-image", ToNodeID: "new-image"},
|
||||
}
|
||||
canvas, err := design.EncodeCanvasSnapshot(snapshotProject)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
saved, err := service.SaveCanvas(ctx, project.ID, design.Viewport{}, nil, nil, "", "", canvas)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if saved.Version != "101000" {
|
||||
t.Fatalf("expected refreshed version, got %s", saved.Version)
|
||||
}
|
||||
if saved.SnapshotID != "snapshot-client" {
|
||||
t.Fatalf("expected client snapshot id, got %s", saved.SnapshotID)
|
||||
}
|
||||
if saved.Viewport != snapshotProject.Viewport {
|
||||
t.Fatalf("unexpected viewport: %#v", saved.Viewport)
|
||||
}
|
||||
if len(saved.Nodes) != 1 || saved.Nodes[0].ID != "new-image" {
|
||||
t.Fatalf("unexpected saved nodes: %#v", saved.Nodes)
|
||||
}
|
||||
if len(saved.Connections) != 1 || saved.Connections[0].ID != "connection-1" {
|
||||
t.Fatalf("unexpected saved connections: %#v", saved.Connections)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveCanvasDoesNotDeleteImageAssetReferencedByAgentMessage(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := memory.NewProjectRepository()
|
||||
|
||||
@@ -1054,7 +1054,7 @@ func (s *DesignService) DeleteAsset(ctx context.Context, publicURL string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DesignService) SaveCanvas(ctx context.Context, projectID string, viewport design.Viewport, nodes []design.Node, _ []design.Connection, clientVersion string, snapshotID string, canvas string) (design.Project, error) {
|
||||
func (s *DesignService) SaveCanvas(ctx context.Context, projectID string, viewport design.Viewport, nodes []design.Node, connections []design.Connection, clientVersion string, snapshotID string, canvas string) (design.Project, error) {
|
||||
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
|
||||
if err != nil {
|
||||
return design.Project{}, err
|
||||
@@ -1064,19 +1064,24 @@ func (s *DesignService) SaveCanvas(ctx context.Context, projectID string, viewpo
|
||||
project = normalizedProject
|
||||
}
|
||||
if strings.TrimSpace(canvas) != "" {
|
||||
if snapshot, err := design.DecodeCanvasSnapshot(canvas); err == nil {
|
||||
if len(nodes) == 0 {
|
||||
nodes = snapshot.Nodes
|
||||
}
|
||||
if viewport.K == 0 {
|
||||
viewport = snapshot.Viewport
|
||||
}
|
||||
if snapshotID == "" {
|
||||
snapshotID = snapshot.SnapshotID
|
||||
}
|
||||
if clientVersion == "" {
|
||||
clientVersion = snapshot.Version
|
||||
}
|
||||
snapshot, err := design.DecodeCanvasSnapshot(canvas)
|
||||
if err != nil {
|
||||
return design.Project{}, fmt.Errorf("%w: invalid canvas snapshot", design.ErrInvalidInput)
|
||||
}
|
||||
if len(nodes) == 0 {
|
||||
nodes = snapshot.Nodes
|
||||
}
|
||||
if len(connections) == 0 {
|
||||
connections = snapshot.Connections
|
||||
}
|
||||
if viewport.K == 0 {
|
||||
viewport = snapshot.Viewport
|
||||
}
|
||||
if snapshotID == "" {
|
||||
snapshotID = snapshot.SnapshotID
|
||||
}
|
||||
if clientVersion == "" {
|
||||
clientVersion = snapshot.Version
|
||||
}
|
||||
}
|
||||
nodes, err = s.persistNodeImageAssets(ctx, project.ID, nodes)
|
||||
@@ -1090,7 +1095,7 @@ func (s *DesignService) SaveCanvas(ctx context.Context, projectID string, viewpo
|
||||
deletedAssetURLs := deletedCanvasAssetURLs(project.Nodes, nodes, project.Messages)
|
||||
project.Viewport = viewport
|
||||
project.Nodes = nodes
|
||||
project.Connections = nil
|
||||
project.Connections = append([]design.Connection(nil), connections...)
|
||||
project.UpdatedAt = now
|
||||
if strings.TrimSpace(snapshotID) != "" && strings.TrimSpace(snapshotID) != project.SnapshotID {
|
||||
project.SnapshotID = strings.TrimSpace(snapshotID)
|
||||
@@ -4042,15 +4047,25 @@ func (s *DesignService) clearStaleGeneratingPlaceholders(project design.Project,
|
||||
}
|
||||
|
||||
func (s *DesignService) restoreFinishedGeneratedImageNodes(project design.Project, now time.Time) (design.Project, bool) {
|
||||
artifactsByID := generatedImageArtifactsByID(project.Messages)
|
||||
if len(artifactsByID) == 0 {
|
||||
artifacts := generatedImageArtifacts(project.Messages)
|
||||
if len(artifacts) == 0 {
|
||||
return project, false
|
||||
}
|
||||
artifactsByID := generatedImageArtifactsByID(project.Messages)
|
||||
usedArtifacts := make(map[string]bool)
|
||||
next := make([]design.Node, 0, len(project.Nodes))
|
||||
changed := false
|
||||
for _, node := range project.Nodes {
|
||||
if artifact, ok := artifactsByID[node.ID]; ok && node.Status == "generating" {
|
||||
node = restoreNodeFromGeneratedArtifact(node, artifact)
|
||||
usedArtifacts[artifactKey(artifact)] = true
|
||||
changed = true
|
||||
}
|
||||
if blankSuccessfulGeneratedImageNode(node) {
|
||||
if artifact, ok := firstUnusedGeneratedArtifact(artifacts, usedArtifacts); ok {
|
||||
node = restoreNodeFromGeneratedArtifact(node, artifact)
|
||||
usedArtifacts[artifactKey(artifact)] = true
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
if project.Status == design.StatusReady && node.Type == design.NodeTypeImage && node.Status == "generating" && !isGeneratedImageContent(node.Content) {
|
||||
@@ -4068,23 +4083,56 @@ func (s *DesignService) restoreFinishedGeneratedImageNodes(project design.Projec
|
||||
return project, true
|
||||
}
|
||||
|
||||
func generatedImageArtifactsByID(messages []design.Message) map[string]design.GeneratorTaskArtifact {
|
||||
artifactsByID := make(map[string]design.GeneratorTaskArtifact)
|
||||
func generatedImageArtifacts(messages []design.Message) []design.GeneratorTaskArtifact {
|
||||
artifacts := make([]design.GeneratorTaskArtifact, 0)
|
||||
for _, message := range messages {
|
||||
if !isToolArtifactsMessage(message) || !strings.EqualFold(strings.TrimSpace(message.Status), "success") {
|
||||
continue
|
||||
}
|
||||
for _, artifact := range parseGeneratorArtifacts(message.Content) {
|
||||
id := strings.TrimSpace(artifact.Metadata.ArtifactID)
|
||||
if id == "" || strings.TrimSpace(artifact.Content) == "" {
|
||||
if strings.TrimSpace(artifact.Content) == "" {
|
||||
continue
|
||||
}
|
||||
artifacts = append(artifacts, artifact)
|
||||
}
|
||||
}
|
||||
return artifacts
|
||||
}
|
||||
|
||||
func generatedImageArtifactsByID(messages []design.Message) map[string]design.GeneratorTaskArtifact {
|
||||
artifactsByID := make(map[string]design.GeneratorTaskArtifact)
|
||||
for _, artifact := range generatedImageArtifacts(messages) {
|
||||
id := strings.TrimSpace(artifact.Metadata.ArtifactID)
|
||||
if id != "" {
|
||||
artifactsByID[id] = artifact
|
||||
}
|
||||
}
|
||||
return artifactsByID
|
||||
}
|
||||
|
||||
func blankSuccessfulGeneratedImageNode(node design.Node) bool {
|
||||
return node.Type == design.NodeTypeImage && strings.TrimSpace(node.Content) == "" && strings.EqualFold(strings.TrimSpace(node.Status), "success")
|
||||
}
|
||||
|
||||
func firstUnusedGeneratedArtifact(artifacts []design.GeneratorTaskArtifact, used map[string]bool) (design.GeneratorTaskArtifact, bool) {
|
||||
for index := len(artifacts) - 1; index >= 0; index-- {
|
||||
artifact := artifacts[index]
|
||||
key := artifactKey(artifact)
|
||||
if key == "" || used[key] {
|
||||
continue
|
||||
}
|
||||
return artifact, true
|
||||
}
|
||||
return design.GeneratorTaskArtifact{}, false
|
||||
}
|
||||
|
||||
func artifactKey(artifact design.GeneratorTaskArtifact) string {
|
||||
if id := strings.TrimSpace(artifact.Metadata.ArtifactID); id != "" {
|
||||
return id
|
||||
}
|
||||
return strings.TrimSpace(artifact.Content)
|
||||
}
|
||||
|
||||
func restoreNodeFromGeneratedArtifact(node design.Node, artifact design.GeneratorTaskArtifact) design.Node {
|
||||
node.Type = design.NodeTypeImage
|
||||
if title := strings.TrimSpace(artifact.Metadata.NodeName); title != "" {
|
||||
|
||||
@@ -119,6 +119,48 @@ func TestRestoreFinishedGeneratedImageNodesRepairsStaleCanvasSnapshot(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreFinishedGeneratedImageNodesRepairsBlankSuccessfulImageNode(t *testing.T) {
|
||||
now := time.Now()
|
||||
project := design.Project{
|
||||
ID: "project-blank-generated-node",
|
||||
Status: design.StatusReady,
|
||||
Nodes: []design.Node{
|
||||
{
|
||||
ID: "blank-node",
|
||||
Type: design.NodeTypeImage,
|
||||
Status: "success",
|
||||
Width: 320,
|
||||
Height: 240,
|
||||
},
|
||||
},
|
||||
Messages: []design.Message{
|
||||
toolArtifactsMessage("thread-image", "pants", []design.Node{
|
||||
{
|
||||
ID: "artifact-node",
|
||||
Type: design.NodeTypeImage,
|
||||
Title: "浅麻灰长裤抠图",
|
||||
Content: "http://localhost:19000/canvas-assets/pants.png",
|
||||
Status: "success",
|
||||
Width: 1024,
|
||||
Height: 1024,
|
||||
},
|
||||
}, now),
|
||||
},
|
||||
}
|
||||
|
||||
restored, changed := (*DesignService)(nil).restoreFinishedGeneratedImageNodes(project, now.Add(time.Second))
|
||||
if !changed {
|
||||
t.Fatal("expected blank generated node to be repaired")
|
||||
}
|
||||
if len(restored.Nodes) != 1 {
|
||||
t.Fatalf("expected one repaired node, got %#v", restored.Nodes)
|
||||
}
|
||||
node := restored.Nodes[0]
|
||||
if node.ID != "blank-node" || node.Content != "http://localhost:19000/canvas-assets/pants.png" || node.Title != "浅麻灰长裤抠图" || node.Width != 1024 || node.Height != 1024 {
|
||||
t.Fatalf("expected blank node repaired from artifact, got %#v", node)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeSavedCanvasWorkStateDoesNotReapplyStaleGeneratingNodes(t *testing.T) {
|
||||
existing := []design.Node{
|
||||
{ID: "generated", Type: design.NodeTypeImage, Title: "Generated", Content: "http://localhost:19000/canvas-assets/generated.png", Status: "success"},
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"img_infinite_canvas/internal/logic"
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
)
|
||||
|
||||
func agentThreadListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.AgentThreadListRequest
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := logic.NewAgentThreadListLogic(r.Context(), svcCtx)
|
||||
resp, err := l.AgentThreadList(&req)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"img_infinite_canvas/internal/logic"
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
)
|
||||
|
||||
func candaChatHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.CandaChatHistoryRequest
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := logic.NewCandaChatHistoryLogic(r.Context(), svcCtx)
|
||||
resp, err := l.CandaChatHistory(&req)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"img_infinite_canvas/internal/logic"
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
)
|
||||
|
||||
func candaThreadStatusHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.CandaThreadStatusRequest
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := logic.NewCandaThreadStatusLogic(r.Context(), svcCtx)
|
||||
resp, err := l.CandaThreadStatus(&req)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"img_infinite_canvas/internal/logic"
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
)
|
||||
|
||||
func queryAgentLastThreadHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.QueryAgentLastThreadRequest
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := logic.NewQueryAgentLastThreadLogic(r.Context(), svcCtx)
|
||||
resp, err := l.QueryAgentLastThread(&req)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"img_infinite_canvas/internal/logic"
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
)
|
||||
|
||||
func queryProjectHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.QueryProjectRequest
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := logic.NewQueryProjectLogic(r.Context(), svcCtx)
|
||||
resp, err := l.QueryProject(&req)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,36 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
|
||||
Path: "/auth/options",
|
||||
Handler: getAuthOptionsHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/canda/chat-history",
|
||||
Handler: candaChatHistoryHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodGet,
|
||||
Path: "/canda/thread/status",
|
||||
Handler: candaThreadStatusHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/canva/agent/agentThreadList",
|
||||
Handler: agentThreadListHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/canva/agent/queryAgentLastThread",
|
||||
Handler: queryAgentLastThreadHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/canva/project/queryProject",
|
||||
Handler: queryProjectHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/canva/project/saveProject",
|
||||
Handler: saveProjectHandler(serverCtx),
|
||||
},
|
||||
{
|
||||
Method: http.MethodPost,
|
||||
Path: "/context/vision/recognize_object",
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/zeromicro/go-zero/rest/httpx"
|
||||
"img_infinite_canvas/internal/logic"
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
)
|
||||
|
||||
func saveProjectHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req types.SaveProjectRequest
|
||||
if err := httpx.Parse(r, &req); err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
return
|
||||
}
|
||||
|
||||
l := logic.NewSaveProjectLogic(r.Context(), svcCtx)
|
||||
resp, err := l.SaveProject(&req)
|
||||
if err != nil {
|
||||
httpx.ErrorCtx(r.Context(), w, err)
|
||||
} else {
|
||||
httpx.OkJsonCtx(r.Context(), w, resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type AgentThreadListLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewAgentThreadListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AgentThreadListLogic {
|
||||
return &AgentThreadListLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *AgentThreadListLogic) AgentThreadList(req *types.AgentThreadListRequest) (resp *types.LovartAgentThreadListResponse, err error) {
|
||||
project, err := l.svcCtx.DesignService.GetProject(l.ctx, req.ProjectId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
threads, err := l.svcCtx.DesignService.AgentThreads(l.ctx, req.ProjectId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
page := req.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := req.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = 100
|
||||
}
|
||||
if pageSize > 100 {
|
||||
pageSize = 100
|
||||
}
|
||||
total := int64(len(threads))
|
||||
start := (page - 1) * pageSize
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
items := make([]types.LovartAgentThreadSummary, 0, end-start)
|
||||
for index, thread := range threads[start:end] {
|
||||
var cover *string
|
||||
if thread.CoverImageURL != "" {
|
||||
value := thread.CoverImageURL
|
||||
cover = &value
|
||||
}
|
||||
items = append(items, types.LovartAgentThreadSummary{
|
||||
Id: start + int64(index) + 1,
|
||||
UserId: project.UserID,
|
||||
ProjectId: req.ProjectId,
|
||||
AgentThreadId: thread.AgentThreadID,
|
||||
ThreadIdType: thread.ThreadIDType,
|
||||
Title: "",
|
||||
Text: thread.Text,
|
||||
CoverImageUrl: cover,
|
||||
FileType: nil,
|
||||
UpdateTimestamp: thread.UpdateTimestamp,
|
||||
Source: 1,
|
||||
})
|
||||
}
|
||||
return &types.LovartAgentThreadListResponse{
|
||||
Code: 0,
|
||||
Msg: nil,
|
||||
Data: types.LovartAgentThreadListData{
|
||||
Page: page,
|
||||
Total: total,
|
||||
PageSize: pageSize,
|
||||
Data: items,
|
||||
UserIp: "",
|
||||
TraceId: "",
|
||||
HasMore: end < total,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type CandaChatHistoryLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewCandaChatHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CandaChatHistoryLogic {
|
||||
return &CandaChatHistoryLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CandaChatHistoryLogic) CandaChatHistory(req *types.CandaChatHistoryRequest) (resp *types.CandaChatHistoryResponse, err error) {
|
||||
replay, err := l.svcCtx.DesignService.ReplayHistory(l.ctx, req.ProjectId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]types.CandaChatHistoryItem, 0, len(replay.Messages))
|
||||
for _, message := range replay.Messages {
|
||||
threadID := message.ThreadID
|
||||
if threadID == "" {
|
||||
threadID = req.ThreadId
|
||||
}
|
||||
if req.ThreadId != "" && threadID != req.ThreadId {
|
||||
continue
|
||||
}
|
||||
items = append(items, toCandaChatHistoryItem(message, req.ProjectId, threadID))
|
||||
}
|
||||
|
||||
return &types.CandaChatHistoryResponse{
|
||||
Code: 0,
|
||||
Message: "Successfully got history view for " + req.ThreadId,
|
||||
Data: items,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type CandaThreadStatusLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewCandaThreadStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CandaThreadStatusLogic {
|
||||
return &CandaThreadStatusLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CandaThreadStatusLogic) CandaThreadStatus(req *types.CandaThreadStatusRequest) (resp *types.CandaThreadStatusResponse, err error) {
|
||||
status := "done"
|
||||
if req.ProjectId != "" {
|
||||
thread, err := l.svcCtx.DesignService.AgentThreadStatus(l.ctx, req.ProjectId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status = thread.Status
|
||||
}
|
||||
|
||||
return &types.CandaThreadStatusResponse{
|
||||
Code: 0,
|
||||
Msg: "success",
|
||||
Data: types.CandaThreadStatusData{Status: status},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
"img_infinite_canvas/internal/types"
|
||||
)
|
||||
|
||||
func latestLovartThreadID(project design.Project) string {
|
||||
if threadID := strings.TrimSpace(project.LastThreadID); threadID != "" {
|
||||
return threadID
|
||||
}
|
||||
for index := len(project.Messages) - 1; index >= 0; index-- {
|
||||
message := project.Messages[index]
|
||||
if isHiddenAgentMessage(message) {
|
||||
continue
|
||||
}
|
||||
if threadID := strings.TrimSpace(message.ThreadID); threadID != "" {
|
||||
return threadID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func toCandaChatHistoryItem(message design.Message, projectID string, threadID string) types.CandaChatHistoryItem {
|
||||
messageType := strings.TrimSpace(message.Type)
|
||||
if messageType == "" {
|
||||
messageType = strings.TrimSpace(message.Role)
|
||||
}
|
||||
if messageType == "tool" {
|
||||
messageType = "tool_use"
|
||||
}
|
||||
item := types.CandaChatHistoryItem{
|
||||
Id: message.ID,
|
||||
Timestamp: formatCandaTimestamp(message.CreatedAt),
|
||||
Type: messageType,
|
||||
ActionId: nonEmpty(message.ActionID, threadID),
|
||||
StepId: nonEmpty(message.StepID, message.ID),
|
||||
Status: message.Status,
|
||||
ToolCallId: message.ToolCallID,
|
||||
Name: message.Name,
|
||||
ThreadMeta: types.CandaThreadMeta{
|
||||
ActionId: nonEmpty(message.ActionID, threadID),
|
||||
SessionId: threadID + "_main",
|
||||
StepId: nonEmpty(message.StepID, message.ID),
|
||||
Depth: 0,
|
||||
AgentName: "next_agent",
|
||||
ProjectId: projectID,
|
||||
},
|
||||
}
|
||||
if message.Role == "user" {
|
||||
item.Text = message.Content
|
||||
} else {
|
||||
item.Content = message.Content
|
||||
if item.Name == "" {
|
||||
item.Name = message.Title
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func nonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func formatCandaTimestamp(value time.Time) string {
|
||||
if value.IsZero() {
|
||||
value = time.Now()
|
||||
}
|
||||
return value.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type QueryAgentLastThreadLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewQueryAgentLastThreadLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryAgentLastThreadLogic {
|
||||
return &QueryAgentLastThreadLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *QueryAgentLastThreadLogic) QueryAgentLastThread(req *types.QueryAgentLastThreadRequest) (resp *types.QueryAgentLastThreadResponse, err error) {
|
||||
project, err := l.svcCtx.DesignService.GetProject(l.ctx, req.ProjectId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &types.QueryAgentLastThreadResponse{
|
||||
Code: 0,
|
||||
Msg: nil,
|
||||
Data: latestLovartThreadID(project),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
const lovartProjectType int64 = 6
|
||||
|
||||
type QueryProjectLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewQueryProjectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QueryProjectLogic {
|
||||
return &QueryProjectLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *QueryProjectLogic) QueryProject(req *types.QueryProjectRequest) (resp *types.QueryProjectResponse, err error) {
|
||||
project, err := l.svcCtx.DesignService.GetProject(l.ctx, req.ProjectId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toQueryProjectResponse(project), nil
|
||||
}
|
||||
|
||||
func toQueryProjectResponse(project design.Project) *types.QueryProjectResponse {
|
||||
covers := queryProjectCoverList(project)
|
||||
updatedAt := formatLovartTime(project.UpdatedAt)
|
||||
return &types.QueryProjectResponse{
|
||||
Code: 0,
|
||||
Msg: nil,
|
||||
Data: types.QueryProjectData{
|
||||
AssetMeta: types.QueryProjectAssetMeta{
|
||||
AssetId: project.ID,
|
||||
ProjectId: project.ID,
|
||||
Name: project.Title,
|
||||
ProjectType: lovartProjectType,
|
||||
ContentType: "PROJECT_LOVART_V2",
|
||||
OwnerType: "USER",
|
||||
OwnerId: project.UserID,
|
||||
Extra: types.QueryProjectExtra{
|
||||
PicCount: int64(len(covers)),
|
||||
ProjectCoverList: covers,
|
||||
},
|
||||
Meta: types.QueryProjectMeta{
|
||||
UniqId: project.SnapshotID,
|
||||
ParentUniqId: "",
|
||||
},
|
||||
EffectivePermission: "OWNER",
|
||||
CopyTaskStatus: nil,
|
||||
Visibility: "PRIVATE",
|
||||
CreateTime: updatedAt,
|
||||
UpdateTime: updatedAt,
|
||||
},
|
||||
UserId: project.UserID,
|
||||
ProjectName: project.Title,
|
||||
ProjectId: nil,
|
||||
Canvas: project.Canvas,
|
||||
ValidProjectId: true,
|
||||
ProjectType: lovartProjectType,
|
||||
Version: project.Version,
|
||||
SnapshotId: nil,
|
||||
ItemId: nil,
|
||||
Permission: "OWNER",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func queryProjectCoverList(project design.Project) []string {
|
||||
seen := make(map[string]bool)
|
||||
covers := make([]string, 0, 4)
|
||||
add := func(value string) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || strings.HasPrefix(value, "data:") || strings.HasPrefix(value, "blob:") || seen[value] {
|
||||
return
|
||||
}
|
||||
seen[value] = true
|
||||
covers = append(covers, value)
|
||||
}
|
||||
add(project.Thumbnail)
|
||||
for _, node := range project.Nodes {
|
||||
if len(covers) >= 12 {
|
||||
break
|
||||
}
|
||||
if (node.Type == design.NodeTypeImage || node.Type == design.NodeTypeFrame) && node.Status == "success" {
|
||||
add(node.Content)
|
||||
}
|
||||
}
|
||||
return covers
|
||||
}
|
||||
|
||||
func formatLovartTime(value time.Time) string {
|
||||
if value.IsZero() {
|
||||
value = time.Now()
|
||||
}
|
||||
return value.Format("2006-01-02T15:04:05")
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package logic
|
||||
import (
|
||||
"context"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
|
||||
@@ -26,7 +27,7 @@ func NewSaveCanvasLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SaveCa
|
||||
}
|
||||
}
|
||||
|
||||
func (l *SaveCanvasLogic) SaveCanvas(req *types.SaveCanvasRequest) (resp *types.ProjectResponse, err error) {
|
||||
func (l *SaveCanvasLogic) SaveCanvas(req *types.SaveCanvasRequest) (resp *types.SaveProjectResponse, err error) {
|
||||
project, err := l.svcCtx.DesignService.SaveCanvas(
|
||||
l.ctx,
|
||||
req.Id,
|
||||
@@ -41,5 +42,18 @@ func (l *SaveCanvasLogic) SaveCanvas(req *types.SaveCanvasRequest) (resp *types.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toProjectResponse(project), nil
|
||||
return saveProjectResponse(project), nil
|
||||
}
|
||||
|
||||
func saveProjectResponse(project design.Project) *types.SaveProjectResponse {
|
||||
return &types.SaveProjectResponse{
|
||||
Code: 0,
|
||||
Msg: nil,
|
||||
Data: types.SaveProjectData{
|
||||
ProjectId: project.ID,
|
||||
NewCreated: false,
|
||||
ValidProjectId: true,
|
||||
Version: project.Version,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"img_infinite_canvas/internal/application"
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
"img_infinite_canvas/internal/infrastructure/memory"
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
)
|
||||
|
||||
func TestSaveCanvasReturnsSaveProjectMetadata(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := memory.NewProjectRepository()
|
||||
service := application.NewDesignService(repo, nil, nil, nil, nil)
|
||||
project := design.Project{
|
||||
ID: "project-save-metadata",
|
||||
Title: "Save metadata",
|
||||
Status: design.StatusReady,
|
||||
UpdatedAt: time.Unix(100, 0).UTC(),
|
||||
Version: "100000",
|
||||
SnapshotID: "snapshot-old",
|
||||
Viewport: design.Viewport{K: 1},
|
||||
}
|
||||
if err := repo.Save(ctx, project); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
logic := NewSaveCanvasLogic(ctx, &svc.ServiceContext{DesignService: service})
|
||||
resp, err := logic.SaveCanvas(&types.SaveCanvasRequest{
|
||||
Id: project.ID,
|
||||
Version: project.Version,
|
||||
Viewport: types.CanvasViewport{
|
||||
K: 1,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if resp.Code != 0 || resp.Msg != nil {
|
||||
t.Fatalf("unexpected wrapper: %#v", resp)
|
||||
}
|
||||
if resp.Data.ProjectId != project.ID || resp.Data.NewCreated || !resp.Data.ValidProjectId || resp.Data.Version == "" {
|
||||
t.Fatalf("unexpected save project data: %#v", resp.Data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveProjectReturnsSaveProjectMetadata(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := memory.NewProjectRepository()
|
||||
service := application.NewDesignService(repo, nil, nil, nil, nil)
|
||||
project := design.Project{
|
||||
ID: "project-lovart-save",
|
||||
Title: "Lovart save",
|
||||
Status: design.StatusReady,
|
||||
UpdatedAt: time.Unix(100, 0).UTC(),
|
||||
Version: "100000",
|
||||
SnapshotID: "snapshot-old",
|
||||
Viewport: design.Viewport{K: 1},
|
||||
}
|
||||
if err := repo.Save(ctx, project); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
logic := NewSaveProjectLogic(ctx, &svc.ServiceContext{DesignService: service})
|
||||
resp, err := logic.SaveProject(&types.SaveProjectRequest{
|
||||
ProjectId: project.ID,
|
||||
Version: project.Version,
|
||||
Viewport: types.CanvasViewport{
|
||||
K: 1,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if resp.Code != 0 || resp.Msg != nil {
|
||||
t.Fatalf("unexpected wrapper: %#v", resp)
|
||||
}
|
||||
if resp.Data.ProjectId != project.ID || resp.Data.NewCreated || !resp.Data.ValidProjectId || resp.Data.Version == "" {
|
||||
t.Fatalf("unexpected save project data: %#v", resp.Data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Code scaffolded by goctl. Safe to edit.
|
||||
// goctl 1.10.1
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"img_infinite_canvas/internal/domain/design"
|
||||
"img_infinite_canvas/internal/svc"
|
||||
"img_infinite_canvas/internal/types"
|
||||
|
||||
"github.com/zeromicro/go-zero/core/logx"
|
||||
)
|
||||
|
||||
type SaveProjectLogic struct {
|
||||
logx.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
func NewSaveProjectLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SaveProjectLogic {
|
||||
return &SaveProjectLogic{
|
||||
Logger: logx.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *SaveProjectLogic) SaveProject(req *types.SaveProjectRequest) (resp *types.SaveProjectResponse, err error) {
|
||||
projectID := strings.TrimSpace(req.ProjectId)
|
||||
if projectID == "" {
|
||||
return nil, fmt.Errorf("%w: projectId is required", design.ErrInvalidInput)
|
||||
}
|
||||
project, err := l.svcCtx.DesignService.SaveCanvas(
|
||||
l.ctx,
|
||||
projectID,
|
||||
toDomainView(req.Viewport),
|
||||
toDomainNodes(req.Nodes),
|
||||
toDomainConnections(req.Connections),
|
||||
req.Version,
|
||||
req.SnapshotId,
|
||||
req.Canvas,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return saveProjectResponse(project), nil
|
||||
}
|
||||
@@ -75,6 +75,13 @@ type AgentPreferToolCategories struct {
|
||||
Image []string `json:"IMAGE,optional"`
|
||||
}
|
||||
|
||||
type AgentThreadListRequest struct {
|
||||
ProjectId string `json:"projectId"`
|
||||
Page int64 `json:"page,optional"`
|
||||
PageSize int64 `json:"pageSize,optional"`
|
||||
Cid string `json:"cid,optional"`
|
||||
}
|
||||
|
||||
type AgentThreadListResponse struct {
|
||||
ProjectId string `json:"projectId"`
|
||||
Threads []AgentThreadSummary `json:"threads"`
|
||||
@@ -170,6 +177,55 @@ type AuthUser struct {
|
||||
AvatarUrl string `json:"avatarUrl,optional"`
|
||||
}
|
||||
|
||||
type CandaChatHistoryItem struct {
|
||||
Id string `json:"id"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Type string `json:"type"`
|
||||
ActionId string `json:"action_id"`
|
||||
StepId string `json:"step_id"`
|
||||
Text string `json:"text,optional"`
|
||||
Content string `json:"content,optional"`
|
||||
Status string `json:"status,optional"`
|
||||
ToolCallId string `json:"tool_call_id,optional"`
|
||||
Name string `json:"name,optional"`
|
||||
ThreadMeta CandaThreadMeta `json:"thread_meta"`
|
||||
}
|
||||
|
||||
type CandaChatHistoryRequest struct {
|
||||
ThreadId string `form:"thread_id"`
|
||||
ProjectId string `form:"project_id"`
|
||||
}
|
||||
|
||||
type CandaChatHistoryResponse struct {
|
||||
Code int64 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data []CandaChatHistoryItem `json:"data"`
|
||||
}
|
||||
|
||||
type CandaThreadMeta struct {
|
||||
ActionId string `json:"action_id"`
|
||||
SessionId string `json:"session_id"`
|
||||
StepId string `json:"step_id"`
|
||||
Depth int64 `json:"depth"`
|
||||
AgentName string `json:"agent_name"`
|
||||
ProjectId string `json:"project_id"`
|
||||
}
|
||||
|
||||
type CandaThreadStatusData struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type CandaThreadStatusRequest struct {
|
||||
ThreadId string `form:"thread_id"`
|
||||
ProjectId string `form:"project_id,optional"`
|
||||
}
|
||||
|
||||
type CandaThreadStatusResponse struct {
|
||||
Code int64 `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data CandaThreadStatusData `json:"data"`
|
||||
}
|
||||
|
||||
type CanvasConnection struct {
|
||||
Id string `json:"id"`
|
||||
FromNodeId string `json:"fromNodeId"`
|
||||
@@ -471,6 +527,38 @@ type InspirationItem struct {
|
||||
Accent string `json:"accent"`
|
||||
}
|
||||
|
||||
type LovartAgentThreadListData struct {
|
||||
Page int64 `json:"page"`
|
||||
Total int64 `json:"total"`
|
||||
PageSize int64 `json:"pageSize"`
|
||||
Data []LovartAgentThreadSummary `json:"data"`
|
||||
UserIp string `json:"userIp"`
|
||||
TraceId string `json:"traceId"`
|
||||
HasMore bool `json:"hasMore"`
|
||||
RankExpId *string `json:"rankExpId"`
|
||||
AbVariant *string `json:"abVariant"`
|
||||
}
|
||||
|
||||
type LovartAgentThreadListResponse struct {
|
||||
Code int64 `json:"code"`
|
||||
Msg *string `json:"msg"`
|
||||
Data LovartAgentThreadListData `json:"data"`
|
||||
}
|
||||
|
||||
type LovartAgentThreadSummary struct {
|
||||
Id int64 `json:"id"`
|
||||
UserId string `json:"userId"`
|
||||
ProjectId string `json:"projectId"`
|
||||
AgentThreadId string `json:"agentThreadId"`
|
||||
ThreadIdType int64 `json:"threadIdType"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
CoverImageUrl *string `json:"coverImageUrl"`
|
||||
FileType *string `json:"fileType"`
|
||||
UpdateTimestamp int64 `json:"updateTimestamp"`
|
||||
Source int64 `json:"source"`
|
||||
}
|
||||
|
||||
type MergeLayersRequest struct {
|
||||
Id string `path:"id"`
|
||||
NodeIds []string `json:"nodeIds"`
|
||||
@@ -613,6 +701,69 @@ type ProjectSummary struct {
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type QueryAgentLastThreadRequest struct {
|
||||
ProjectId string `json:"projectId"`
|
||||
Cid string `json:"cid,optional"`
|
||||
}
|
||||
|
||||
type QueryAgentLastThreadResponse struct {
|
||||
Code int64 `json:"code"`
|
||||
Msg *string `json:"msg"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type QueryProjectAssetMeta struct {
|
||||
AssetId string `json:"assetId"`
|
||||
ProjectId string `json:"projectId"`
|
||||
Name string `json:"name"`
|
||||
ProjectType int64 `json:"projectType"`
|
||||
ContentType string `json:"contentType"`
|
||||
OwnerType string `json:"ownerType"`
|
||||
OwnerId string `json:"ownerId"`
|
||||
Extra QueryProjectExtra `json:"extra"`
|
||||
Meta QueryProjectMeta `json:"meta"`
|
||||
EffectivePermission string `json:"effectivePermission"`
|
||||
CopyTaskStatus *string `json:"copyTaskStatus"`
|
||||
Visibility string `json:"visibility"`
|
||||
CreateTime string `json:"createTime"`
|
||||
UpdateTime string `json:"updateTime"`
|
||||
}
|
||||
|
||||
type QueryProjectData struct {
|
||||
AssetMeta QueryProjectAssetMeta `json:"assetMeta"`
|
||||
UserId string `json:"userId"`
|
||||
ProjectName string `json:"projectName"`
|
||||
ProjectId *string `json:"projectId"`
|
||||
Canvas string `json:"canvas"`
|
||||
ValidProjectId bool `json:"validProjectId"`
|
||||
ProjectType int64 `json:"projectType"`
|
||||
Version string `json:"version"`
|
||||
SnapshotId *string `json:"snapshotId"`
|
||||
ItemId *string `json:"itemId"`
|
||||
Permission string `json:"permission"`
|
||||
}
|
||||
|
||||
type QueryProjectExtra struct {
|
||||
PicCount int64 `json:"picCount"`
|
||||
ProjectCoverList []string `json:"projectCoverList"`
|
||||
}
|
||||
|
||||
type QueryProjectMeta struct {
|
||||
UniqId string `json:"uniqId"`
|
||||
ParentUniqId string `json:"parentUniqId"`
|
||||
}
|
||||
|
||||
type QueryProjectRequest struct {
|
||||
ProjectId string `json:"projectId"`
|
||||
Cid string `json:"cid,optional"`
|
||||
}
|
||||
|
||||
type QueryProjectResponse struct {
|
||||
Code int64 `json:"code"`
|
||||
Msg *string `json:"msg"`
|
||||
Data QueryProjectData `json:"data"`
|
||||
}
|
||||
|
||||
type QuickAction struct {
|
||||
Id string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
@@ -637,13 +788,47 @@ type RecognizeObjectResponse struct {
|
||||
}
|
||||
|
||||
type SaveCanvasRequest struct {
|
||||
Id string `path:"id"`
|
||||
Version string `json:"version,optional"`
|
||||
SnapshotId string `json:"snapshotId,optional"`
|
||||
Canvas string `json:"canvas,optional"`
|
||||
Viewport CanvasViewport `json:"viewport"`
|
||||
Nodes []CanvasNode `json:"nodes"`
|
||||
Connections []CanvasConnection `json:"connections"`
|
||||
Id string `path:"id"`
|
||||
Version string `json:"version,optional"`
|
||||
SnapshotId string `json:"snapshotId,optional"`
|
||||
Canvas string `json:"canvas,optional"`
|
||||
Viewport CanvasViewport `json:"viewport,optional"`
|
||||
Nodes []CanvasNode `json:"nodes,optional"`
|
||||
Connections []CanvasConnection `json:"connections,optional"`
|
||||
ProjectCoverList []string `json:"projectCoverList,optional"`
|
||||
PicCount int64 `json:"picCount,optional"`
|
||||
ProjectName string `json:"projectName,optional"`
|
||||
ProjectId string `json:"projectId,optional"`
|
||||
SessionId string `json:"sessionId,optional"`
|
||||
CanvasV2Gray bool `json:"canvasV2Gray,optional"`
|
||||
}
|
||||
|
||||
type SaveProjectData struct {
|
||||
ProjectId string `json:"projectId"`
|
||||
NewCreated bool `json:"newCreated"`
|
||||
ValidProjectId bool `json:"validProjectId"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type SaveProjectRequest struct {
|
||||
Canvas string `json:"canvas,optional"`
|
||||
ProjectCoverList []string `json:"projectCoverList,optional"`
|
||||
PicCount int64 `json:"picCount,optional"`
|
||||
ProjectName string `json:"projectName,optional"`
|
||||
ProjectId string `json:"projectId"`
|
||||
Version string `json:"version,optional"`
|
||||
SessionId string `json:"sessionId,optional"`
|
||||
CanvasV2Gray bool `json:"canvasV2Gray,optional"`
|
||||
SnapshotId string `json:"snapshotId,optional"`
|
||||
Viewport CanvasViewport `json:"viewport,optional"`
|
||||
Nodes []CanvasNode `json:"nodes,optional"`
|
||||
Connections []CanvasConnection `json:"connections,optional"`
|
||||
}
|
||||
|
||||
type SaveProjectResponse struct {
|
||||
Code int64 `json:"code"`
|
||||
Msg *string `json:"msg"`
|
||||
Data SaveProjectData `json:"data"`
|
||||
}
|
||||
|
||||
type StopAgentTaskRequest struct {
|
||||
|
||||
Reference in New Issue
Block a user