From 9519ad7e083259194882f19644ff8d84a5677c7f Mon Sep 17 00:00:00 2001 From: euvre <93761161+euvre@users.noreply.github.com> Date: Thu, 30 Jul 2026 19:27:44 +0800 Subject: [PATCH] fix: propagate publish release flag through Go agent endpoints (#17347) --- .../agent_quickstarts/sandbox_quickstart.md | 2 +- .../add_data_source/add_github_repo.md | 4 +- .../advanced/construct_knowledge_graph.md | 10 +- docs/guides/dataset/advanced/enable_raptor.md | 6 +- .../advanced/extract_table_of_contents.md | 4 +- .../configure_child_chunking_strategy.md | 2 +- docs/guides/dataset/set_metadata.md | 2 +- docs/guides/models/llm_api_key_setup.md | 4 +- internal/dao/user_canvas.go | 8 + internal/dao/user_canvas_version.go | 44 +++++ internal/handler/agent.go | 17 +- internal/service/agent.go | 97 +++++++++-- internal/service/agent_test.go | 160 ++++++++++++++++++ web/src/assets/querit.png | Bin 49633 -> 49632 bytes web/src/components/message-input/next.tsx | 4 +- web/src/hooks/use-knowledge-request.ts | 5 +- web/src/pages/dataset/dataset/hooks.ts | 11 +- .../instance-card/models-section/index.tsx | 4 +- web/src/utils/next-request.ts | 3 +- 19 files changed, 340 insertions(+), 47 deletions(-) diff --git a/docs/guides/agent/agent_quickstarts/sandbox_quickstart.md b/docs/guides/agent/agent_quickstarts/sandbox_quickstart.md index fac48df4cb..c174fcfec7 100644 --- a/docs/guides/agent/agent_quickstarts/sandbox_quickstart.md +++ b/docs/guides/agent/agent_quickstarts/sandbox_quickstart.md @@ -122,7 +122,7 @@ docker compose -f docker-compose.yml down docker compose -f docker-compose.yml up -d ``` -## Running with RAGFlow +## Running with RAGFlow 1. Verify that gVisor is properly installed and operational. diff --git a/docs/guides/dataset/add_data_source/add_github_repo.md b/docs/guides/dataset/add_data_source/add_github_repo.md index 1a73107ab1..578406fee1 100644 --- a/docs/guides/dataset/add_data_source/add_github_repo.md +++ b/docs/guides/dataset/add_data_source/add_github_repo.md @@ -48,7 +48,7 @@ Once your GitHub token is ready, register the external data source within your R - **Access token:** Paste the PAT generated in section 1. - **Include Pull Request** Whether to include pull requests in the selected repo. - **Include Issues** Whether to include issues in the selected repo. -4. Click **Save** to confirm your changes. +4. Click **Save** to confirm your changes. *RAGFlow validates the connection immediately.* ![GitHub](https://raw.githubusercontent.com/infiniflow/ragflow-docs/main/images/github.jpg) @@ -66,4 +66,4 @@ Finally, link the connector to a specific knowledge base to begin the RAG proces 3. **Select GitHub source:** Pick the connector you just created. 4. **Trigger initial sync:** - The files from the repo will appear in your file list. - Select the files and click **Run/parsing**. - - **Parser selection:** For codebases, use the **"Naive"** parser for general text extraction or a specific code-aware template if available in your current version. \ No newline at end of file + - **Parser selection:** For codebases, use the **"Naive"** parser for general text extraction or a specific code-aware template if available in your current version. diff --git a/docs/guides/dataset/advanced/construct_knowledge_graph.md b/docs/guides/dataset/advanced/construct_knowledge_graph.md index 825e397196..8c37417255 100644 --- a/docs/guides/dataset/advanced/construct_knowledge_graph.md +++ b/docs/guides/dataset/advanced/construct_knowledge_graph.md @@ -65,7 +65,7 @@ In a knowledge graph, a community is a cluster of entities linked by relationshi ## Quickstart 1. Navigate to the **Configuration** page of your dataset and update: - + - Entity types: *Required* - Specifies the entity types in the knowledge graph to generate. You don't have to stick with the default, but you need to customize them for your documents. - Method: *Optional* - Entity resolution: *Optional* @@ -76,12 +76,12 @@ In a knowledge graph, a community is a cluster of entities linked by relationshi *You can click the pause button in the dropdown to halt the build process when necessary.* -3. Go back to the **Configuration** page: - +3. Go back to the **Configuration** page: + *Once a knowledge graph is generated, the **Knowledge graph** field changes from `Not generated` to `Generated at a specific timestamp`. You can delete it by clicking the recycle bin button to the right of the field.* 4. To use the created knowledge graph, do either of the following: - + - In the **Chat setting** panel of your chat app, switch on the **Use knowledge graph** toggle. - If you are using an agent, click the **Retrieval** agent component to specify the dataset(s) and switch on the **Use knowledge graph** toggle. @@ -101,4 +101,4 @@ All chunks of the created knowledge graph are stored in RAGFlow's document engin ### How to export a created knowledge graph? -Nope. Exporting a created knowledge graph is not supported. If you still consider this feature essential, please [raise an issue](https://github.com/infiniflow/ragflow/issues) explaining your use case and its importance. \ No newline at end of file +Nope. Exporting a created knowledge graph is not supported. If you still consider this feature essential, please [raise an issue](https://github.com/infiniflow/ragflow/issues) explaining your use case and its importance. diff --git a/docs/guides/dataset/advanced/enable_raptor.md b/docs/guides/dataset/advanced/enable_raptor.md index 48311e31fd..ce21213d90 100644 --- a/docs/guides/dataset/advanced/enable_raptor.md +++ b/docs/guides/dataset/advanced/enable_raptor.md @@ -78,7 +78,7 @@ A random seed. Click **+** to change the seed value. ## Quickstart 1. Navigate to the **Configuration** page of your dataset and update: - + - Prompt: *Optional* - We recommend that you keep it as-is until you understand the mechanism behind. - Max token: *Optional* - Threshold: *Optional* @@ -88,8 +88,8 @@ A random seed. Click **+** to change the seed value. *You can click the pause button in the dropdown to halt the build process when necessary.* -3. Go back to the **Configuration** page: - +3. Go back to the **Configuration** page: + *The **RAPTOR** field changes from `Not generated` to `Generated at a specific timestamp` when a RAPTOR hierarchical tree structure is generated. You can delete it by clicking the recycle bin button to the right of the field.* 4. Once a RAPTOR hierarchical tree structure is generated, your chat assistant and **Retrieval** agent component will use it for retrieval as a default. diff --git a/docs/guides/dataset/advanced/extract_table_of_contents.md b/docs/guides/dataset/advanced/extract_table_of_contents.md index 0b36738d2a..5fe00557f5 100644 --- a/docs/guides/dataset/advanced/extract_table_of_contents.md +++ b/docs/guides/dataset/advanced/extract_table_of_contents.md @@ -30,7 +30,7 @@ The system's default chat model is used to summarize clustered content. Before p 2. Enable **PageIndex**. 3. To use this technique during retrieval, do either of the following: - + - In the **Chat setting** panel of your chat app, switch on the **PageIndex** toggle. - If you are using an Agent, click the **Retrieval** agent component to specify the dataset(s) and switch on the **PageIndex** toggle. @@ -38,4 +38,4 @@ The system's default chat model is used to summarize clustered content. Before p ### Will previously parsed files be searched using the directory enhancement feature once I enable `PageIndex`? -No. Only files parsed after you enable **PageIndex** will be searched using the directory enhancement feature. To apply this feature to files parsed before enabling **PageIndex**, you must reparse them. \ No newline at end of file +No. Only files parsed after you enable **PageIndex** will be searched using the directory enhancement feature. To apply this feature to files parsed before enabling **PageIndex**, you must reparse them. diff --git a/docs/guides/dataset/configure_child_chunking_strategy.md b/docs/guides/dataset/configure_child_chunking_strategy.md index d153a4c82e..983cb4c11f 100644 --- a/docs/guides/dataset/configure_child_chunking_strategy.md +++ b/docs/guides/dataset/configure_child_chunking_strategy.md @@ -33,4 +33,4 @@ Through this dual-layer structure of "precise localization + contextual suppleme 3. This configuration applies to the **Chunker** component when it comes to ingestion pipeline settings: -![](https://raw.githubusercontent.com/infiniflow/ragflow-docs/main/images/child_chunking_chunker.png) \ No newline at end of file +![](https://raw.githubusercontent.com/infiniflow/ragflow-docs/main/images/child_chunking_chunker.png) diff --git a/docs/guides/dataset/set_metadata.md b/docs/guides/dataset/set_metadata.md index 49fde4b91c..6ff853e93a 100644 --- a/docs/guides/dataset/set_metadata.md +++ b/docs/guides/dataset/set_metadata.md @@ -31,4 +31,4 @@ Ensure that your metadata is in JSON format; otherwise, your updates will not be ### Can I set metadata for multiple documents at once? -From v0.23.0 onwards, you can set metadata for each document individually or have the LLM auto-generate metadata for multiple files. See [Extract metadata](./advanced/auto_metadata.md) for details. \ No newline at end of file +From v0.23.0 onwards, you can set metadata for each document individually or have the LLM auto-generate metadata for multiple files. See [Extract metadata](./advanced/auto_metadata.md) for details. diff --git a/docs/guides/models/llm_api_key_setup.md b/docs/guides/models/llm_api_key_setup.md index 7b5cbce068..b39d3bb994 100644 --- a/docs/guides/models/llm_api_key_setup.md +++ b/docs/guides/models/llm_api_key_setup.md @@ -11,7 +11,7 @@ RAGFlow model provider management allows you to connect online models, local mod ## Get model API key -RAGFlow supports most mainstream LLMs. Please refer to [Supported Models](../../guides/models/supported_models.mdx) for a complete list of supported models. You will need to apply for your model API key online. +RAGFlow supports most mainstream LLMs. Please refer to [Supported Models](../../guides/models/supported_models.mdx) for a complete list of supported models. You will need to apply for your model API key online. :::note If you find your online LLM is not on the list, don't feel disheartened. The list is expanding, and you can [file a feature request](https://github.com/infiniflow/ragflow/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.yml&title=%5BFeature+Request%5D%3A+) with us! Alternatively, if you have customized or locally-deployed models, you can [bind them to RAGFlow using Ollama, Xinference, or LocalAI](./deploy_local_llm.mdx). @@ -119,4 +119,4 @@ The following model types usually work together for retrieval and generation: ## Supported model list -See [Supported Models](./supported_models.mdx). \ No newline at end of file +See [Supported Models](./supported_models.mdx). diff --git a/internal/dao/user_canvas.go b/internal/dao/user_canvas.go index 0a1873b02c..ecd9a896ee 100644 --- a/internal/dao/user_canvas.go +++ b/internal/dao/user_canvas.go @@ -490,6 +490,14 @@ func (dao *UserCanvasDAO) UpdateFields(ctx context.Context, db *gorm.DB, canvasI return result.RowsAffected, result.Error } +// UpdateFieldsTx is the transactional variant of UpdateFields. Used by +// service.AgentService.UpdateAgent so the canvas row update and the +// version-row save commit atomically in one transaction. +func (dao *UserCanvasDAO) UpdateFieldsTx(tx *gorm.DB, canvasID string, fields map[string]interface{}) (int64, error) { + result := tx.Model(&entity.UserCanvas{}).Where("id = ?", canvasID).Updates(fields) + return result.RowsAffected, result.Error +} + // UpdateTags updates a canvas's comma-separated tags by canvas ID. func (dao *UserCanvasDAO) UpdateTags(ctx context.Context, db *gorm.DB, canvasID, tags string) (int64, error) { result := db.WithContext(ctx).Model(&entity.UserCanvas{}).Where("id = ?", canvasID).Update("tags", tags) diff --git a/internal/dao/user_canvas_version.go b/internal/dao/user_canvas_version.go index aaaa1898a6..491b330b88 100644 --- a/internal/dao/user_canvas_version.go +++ b/internal/dao/user_canvas_version.go @@ -100,6 +100,50 @@ func (dao *UserCanvasVersionDAO) GetLatest(ctx context.Context, db *gorm.DB, can return &v, nil } +// GetLatestReleased returns the most recently updated released version of +// canvasID, or ErrUserCanvasVersionNotFound when nothing was ever published. +// Mirrors the released-version lookup in Python's get_agent handler. +func (dao *UserCanvasVersionDAO) GetLatestReleased(canvasID string) (*entity.UserCanvasVersion, error) { + var v entity.UserCanvasVersion + err := DB.Where("user_canvas_id = ? AND `release` = ?", canvasID, true). + Order("update_time DESC"). + First(&v).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUserCanvasVersionNotFound + } + return nil, err + } + return &v, nil +} + +// GetLatestReleaseTimes returns MAX(create_time) of released versions per +// canvas for the given canvas IDs. Mirrors the release_time aggregation in +// Python's UserCanvasService.get_list. +func (dao *UserCanvasVersionDAO) GetLatestReleaseTimes(canvasIDs []string) (map[string]int64, error) { + result := make(map[string]int64, len(canvasIDs)) + if len(canvasIDs) == 0 { + return result, nil + } + type row struct { + UserCanvasID string + ReleaseTime int64 + } + var rows []row + err := DB.Model(&entity.UserCanvasVersion{}). + Select("user_canvas_id, MAX(create_time) AS release_time"). + Where("user_canvas_id IN ? AND `release` = ?", canvasIDs, true). + Group("user_canvas_id"). + Scan(&rows).Error + if err != nil { + return nil, err + } + for _, r := range rows { + result[r.UserCanvasID] = r.ReleaseTime + } + return result, nil +} + // Delete removes a single version by id. No-op when the row is absent. func (dao *UserCanvasVersionDAO) Delete(ctx context.Context, db *gorm.DB, id string) error { return db.WithContext(ctx).Where("id = ?", id).Delete(&entity.UserCanvasVersion{}).Error diff --git a/internal/handler/agent.go b/internal/handler/agent.go index 9583f3fd3f..f2e3552937 100644 --- a/internal/handler/agent.go +++ b/internal/handler/agent.go @@ -291,7 +291,22 @@ func (h *AgentHandler) GetAgent(c *gin.Context) { if row != nil { row.DSL = dslpkg.NormalizeForCanvas(row.DSL) } - common.SuccessWithData(c, row, "success") + // Attach last_publish_time so the front-end can render when the agent + // was last published (Python get_agent parity). + lastPublishTime, err := h.agentService.GetLastPublishTime(c.Request.Context(), canvasID) + if err != nil { + ec, em := mapAgentError(err) + common.ResponseWithCodeData(c, ec, nil, em) + return + } + common.SuccessWithData(c, &agentDetailResponse{UserCanvas: row, LastPublishTime: lastPublishTime}, "success") +} + +// agentDetailResponse wraps the canvas row with derived fields that Python's +// get_agent handler adds to the response. +type agentDetailResponse struct { + *entity.UserCanvas + LastPublishTime *int64 `json:"last_publish_time,omitempty"` } // updateAgentRequest is the wire shape for PUT /api/v1/agents/:canvas_id. diff --git a/internal/service/agent.go b/internal/service/agent.go index 7d9c83dc7c..c8a4cd74ad 100644 --- a/internal/service/agent.go +++ b/internal/service/agent.go @@ -412,6 +412,7 @@ type AgentItem struct { CanvasCategory string `json:"canvas_category"` CreateTime *int64 `json:"create_time,omitempty"` UpdateTime *int64 `json:"update_time,omitempty"` + ReleaseTime *int64 `json:"release_time,omitempty"` } // ListAgentsResponse is the response body for GET /api/v1/agents. @@ -503,6 +504,25 @@ func (s *AgentService) ListAgents(ctx context.Context, userID string, keywords s for i, c := range canvases { items[i] = toAgentItem(c) } + + // Attach the latest release time per canvas so agent cards can render + // the "published at" line (Python UserCanvasService.get_list parity). + if len(items) > 0 { + canvasIDs := make([]string, 0, len(items)) + for _, item := range items { + canvasIDs = append(canvasIDs, item.ID) + } + releaseTimes, err := s.versionDAO.GetLatestReleaseTimes(canvasIDs) + if err != nil { + return nil, common.CodeServerError, fmt.Errorf("failed to get release times: %w", err) + } + for _, item := range items { + if t, ok := releaseTimes[item.ID]; ok { + item.ReleaseTime = &t + } + } + } + return &ListAgentsResponse{Canvas: items, Total: total}, common.CodeSuccess, nil } @@ -651,6 +671,20 @@ func (s *AgentService) GetAgent(ctx context.Context, userID, canvasID string) (* return s.loadCanvasForUser(ctx, userID, canvasID) } +// GetLastPublishTime returns the update_time of the most recently updated +// released version, or nil when the canvas has never been published. +// Mirrors the last_publish_time computation in Python's get_agent handler. +func (s *AgentService) GetLastPublishTime(ctx context.Context, canvasID string) (*int64, error) { + version, err := s.versionDAO.GetLatestReleased(canvasID) + if err != nil { + if errors.Is(err, dao.ErrUserCanvasVersionNotFound) { + return nil, nil + } + return nil, err + } + return version.UpdateTime, nil +} + // UpdateAgent applies a draft patch to user_canvas. Settings updates may omit // dsl; in that case the existing draft DSL must be preserved. // @@ -681,6 +715,23 @@ func (s *AgentService) UpdateAgent(ctx context.Context, userID, canvasID string, updates[key] = value } } + + // Publish flow: the front-end sends release ("true"/true) together with + // dsl via PUT, and Python's update_agent coerces it with + // `bool(req.get("release", ""))` — any non-empty string is truthy. + // Mirror that here so the canvas row and the new version row both carry + // the release flag; an absent release keeps parity with Python, which + // always writes the coerced value (False when missing). + release := false + if value, ok := patch["release"]; ok && value != nil { + switch v := value.(type) { + case bool: + release = v + case string: + release = v != "" + } + } + updates["release"] = release if title, ok := updatedAgentTitle(canvasInstance, updates); ok { canvasCategory := updatedAgentCanvasCategory(canvasInstance, updates) if existing, err := s.canvasDAO.GetByUserAndTitle(ctx, dao.DB, userID, title, canvasCategory); err != nil { @@ -701,16 +752,10 @@ func (s *AgentService) UpdateAgent(ctx context.Context, userID, canvasID string, updates["dsl"] = entity.JSONMap(dslpkg.NormalizeForCanvas(dslMap)) } - _, err = s.canvasDAO.UpdateFields(ctx, dao.DB, canvasID, updates) - if err != nil { - if dao.IsDuplicateKeyErr(err) { - if title, ok := updatedAgentTitle(canvasInstance, updates); ok { - return agentTitleAlreadyExistsError(title) - } - return errors.New("agent title already exists") - } - return fmt.Errorf("update agent %s: %w", canvasID, err) - } + // Build the version options up front (it only reads the user nickname) + // so the canvas row update and the version save can share one + // transaction below. + var versionOpts *dao.SaveOrReplaceLatestVersionOptions if dslValue, ok := updates["dsl"]; ok { dsl, ok := dslValue.(entity.JSONMap) if !ok { @@ -722,9 +767,33 @@ func (s *AgentService) UpdateAgent(ctx context.Context, userID, canvasID string, } else if canvasInstance.Title != nil { title = *canvasInstance.Title } - if _, err := s.saveOrReplaceVersion(ctx, userID, canvasID, dsl, title, nil, false); err != nil { - return fmt.Errorf("update agent %s: save version: %w", canvasID, err) + opts := s.saveOrReplaceVersionOptions(ctx, userID, canvasID, dsl, title, nil, release) + versionOpts = &opts + } + + // The canvas release/DSL update and the version save must commit + // atomically: if the version write fails after the canvas row is + // committed, the canvas is left published (or unpublished) without a + // matching version state. + err = dao.DB.Transaction(func(tx *gorm.DB) error { + if _, err := s.canvasDAO.UpdateFieldsTx(tx, canvasID, updates); err != nil { + return err } + if versionOpts != nil { + if _, err := s.versionDAO.SaveOrReplaceLatestTx(ctx, tx, *versionOpts); err != nil { + return fmt.Errorf("save version: %w", err) + } + } + return nil + }) + if err != nil { + if dao.IsDuplicateKeyErr(err) { + if title, ok := updatedAgentTitle(canvasInstance, updates); ok { + return agentTitleAlreadyExistsError(title) + } + return errors.New("agent title already exists") + } + return fmt.Errorf("update agent %s: %w", canvasID, err) } return nil } @@ -851,10 +920,6 @@ func (s *AgentService) PublishAgent(ctx context.Context, userID, canvasID string return row, nil } -func (s *AgentService) saveOrReplaceVersion(ctx context.Context, userID, canvasID string, dsl entity.JSONMap, title string, description *string, release bool) (*entity.UserCanvasVersion, error) { - return s.versionDAO.SaveOrReplaceLatest(ctx, dao.DB, s.saveOrReplaceVersionOptions(ctx, userID, canvasID, dsl, title, description, release)) -} - func (s *AgentService) saveOrReplaceVersionOptions(ctx context.Context, userID, canvasID string, dsl entity.JSONMap, title string, description *string, release bool) dao.SaveOrReplaceLatestVersionOptions { nickname, err := s.userDAO.GetNicknameByID(ctx, dao.DB, userID) if err != nil || strings.TrimSpace(nickname) == "" { diff --git a/internal/service/agent_test.go b/internal/service/agent_test.go index 46821d8dcb..a64b6a3501 100644 --- a/internal/service/agent_test.go +++ b/internal/service/agent_test.go @@ -1682,6 +1682,166 @@ func TestUpdateAgentDSLCreatesAndReplacesDraftVersion(t *testing.T) { } } +func TestUpdateAgentReleaseTrueMarksCanvasAndVersionReleased(t *testing.T) { + setupAgentSessionServiceTest(t) + + if err := dao.DB.Create(&entity.User{ID: "user-1", Nickname: "owner", Email: "owner@test.com"}).Error; err != nil { + t.Fatalf("failed to seed user: %v", err) + } + if err := dao.DB.Create(&entity.UserCanvas{ + ID: "canvas-update-release", + UserID: "user-1", + Title: sptr("Release Agent"), + CanvasCategory: "agent_canvas", + DSL: entity.JSONMap{}, + }).Error; err != nil { + t.Fatalf("failed to seed canvas: %v", err) + } + + ctx := t.Context() + // The front-end publish flow sends release as the string "true". + if err := NewAgentService().UpdateAgent(ctx, "user-1", "canvas-update-release", map[string]interface{}{ + "title": "Release Agent", + "release": "true", + "dsl": map[string]interface{}{ + "graph": map[string]interface{}{ + "nodes": []interface{}{map[string]interface{}{"id": "begin"}}, + "edges": []interface{}{}, + }, + "components": map[string]interface{}{ + "begin": map[string]interface{}{ + "obj": map[string]interface{}{"component_name": "Begin"}, + }, + }, + }, + }); err != nil { + t.Fatalf("UpdateAgent failed: %v", err) + } + persisted, err := dao.NewUserCanvasDAO().GetByID(ctx, dao.DB, "canvas-update-release") + if err != nil { + t.Fatalf("failed to reload canvas: %v", err) + } + if !persisted.Release { + t.Fatal("canvas release flag was not set after publish update") + } + versions, err := dao.NewUserCanvasVersionDAO().ListByCanvasID(ctx, dao.DB, "canvas-update-release") + if err != nil { + t.Fatalf("failed to list versions: %v", err) + } + if len(versions) != 1 { + t.Fatalf("expected 1 version, got %d", len(versions)) + } + if !versions[0].Release { + t.Fatal("version created by publish update is not marked released") + } + lastPublishTime, err := NewAgentService().GetLastPublishTime(ctx, "canvas-update-release") + if err != nil { + t.Fatalf("GetLastPublishTime failed: %v", err) + } + if lastPublishTime == nil { + t.Fatal("GetLastPublishTime returned nil after publish") + } + if versions[0].UpdateTime == nil || *lastPublishTime != *versions[0].UpdateTime { + t.Fatalf("last publish time = %v, want version update_time %v", lastPublishTime, versions[0].UpdateTime) + } +} + +func TestUpdateAgentWithoutReleaseKeepsCanvasUnreleased(t *testing.T) { + setupAgentSessionServiceTest(t) + + if err := dao.DB.Create(&entity.User{ID: "user-1", Nickname: "owner", Email: "owner@test.com"}).Error; err != nil { + t.Fatalf("failed to seed user: %v", err) + } + if err := dao.DB.Create(&entity.UserCanvas{ + ID: "canvas-update-draft", + UserID: "user-1", + Title: sptr("Draft Agent"), + CanvasCategory: "agent_canvas", + DSL: entity.JSONMap{}, + Release: true, + }).Error; err != nil { + t.Fatalf("failed to seed canvas: %v", err) + } + + // Python parity: update_agent coerces a missing release to False and + // always writes it back to the canvas row. + ctx := t.Context() + if err := NewAgentService().UpdateAgent(ctx, "user-1", "canvas-update-draft", map[string]interface{}{ + "title": "Draft Agent", + "dsl": map[string]interface{}{ + "graph": map[string]interface{}{ + "nodes": []interface{}{map[string]interface{}{"id": "begin"}}, + "edges": []interface{}{}, + }, + "components": map[string]interface{}{ + "begin": map[string]interface{}{ + "obj": map[string]interface{}{"component_name": "Begin"}, + }, + }, + }, + }); err != nil { + t.Fatalf("UpdateAgent failed: %v", err) + } + persisted, err := dao.NewUserCanvasDAO().GetByID(ctx, dao.DB, "canvas-update-draft") + if err != nil { + t.Fatalf("failed to reload canvas: %v", err) + } + if persisted.Release { + t.Fatal("canvas release flag should be reset to false when release is absent") + } + if lastPublishTime, err := NewAgentService().GetLastPublishTime(ctx, "canvas-update-draft"); err != nil { + t.Fatalf("GetLastPublishTime failed: %v", err) + } else if lastPublishTime != nil { + t.Fatalf("GetLastPublishTime = %v, want nil without released version", lastPublishTime) + } +} + +func TestListAgentsIncludesReleaseTime(t *testing.T) { + setupAgentSessionServiceTest(t) + + if err := dao.DB.Create(&entity.User{ID: "user-1", Nickname: "owner", Email: "owner@test.com"}).Error; err != nil { + t.Fatalf("failed to seed user: %v", err) + } + createAgentSessionTestCanvas(t, "canvas-listed-released", "user-1") + createAgentSessionTestCanvas(t, "canvas-listed-draft", "user-1") + + releaseTime := int64(1753335149000) + if err := dao.DB.Create(&entity.UserCanvasVersion{ + ID: "version-listed-released", + UserCanvasID: "canvas-listed-released", + Title: sptr("owner_Test Agent_2026-07-24 00:00:00"), + Release: true, + DSL: entity.JSONMap{}, + BaseModel: entity.BaseModel{CreateTime: &releaseTime}, + }).Error; err != nil { + t.Fatalf("failed to seed released version: %v", err) + } + ctx := t.Context() + resp, code, err := NewAgentService().ListAgents(ctx, "user-1", "", 1, 30, "create_time", true, nil, "", "", nil) + if err != nil || code != common.CodeSuccess { + t.Fatalf("ListAgents failed: code=%v err=%v", code, err) + } + + var released, draft *AgentItem + for _, item := range resp.Canvas { + switch item.ID { + case "canvas-listed-released": + released = item + case "canvas-listed-draft": + draft = item + } + } + if released == nil || draft == nil { + t.Fatalf("expected both canvases in list, got %#v", resp.Canvas) + } + if released.ReleaseTime == nil || *released.ReleaseTime != releaseTime { + t.Fatalf("ReleaseTime = %v, want %d", released.ReleaseTime, releaseTime) + } + if draft.ReleaseTime != nil { + t.Fatalf("draft canvas ReleaseTime = %v, want nil", draft.ReleaseTime) + } +} + func TestPublishAgentUpdatesCanvasAndReleasedVersion(t *testing.T) { setupAgentSessionServiceTest(t) diff --git a/web/src/assets/querit.png b/web/src/assets/querit.png index 223524516a2bd87e0522b81a7dc23e9678892d1d..7a3045f62552adafbc85be035dfe638470399197 100644 GIT binary patch delta 22 ecmaFZ%>1C4nWZzp&wV579ly1yKnYA;(&z*N8%N;*P-pvpE{ucoNZRiP& diff --git a/web/src/components/message-input/next.tsx b/web/src/components/message-input/next.tsx index 2ae5e873ad..26e570618b 100644 --- a/web/src/components/message-input/next.tsx +++ b/web/src/components/message-input/next.tsx @@ -75,8 +75,8 @@ export function NextMessageInput({ null, ); - const [enableThinking, setEnableThinking] = useState( - () => storage.getThinkingLevel(), + const [enableThinking, setEnableThinking] = useState(() => + storage.getThinkingLevel(), ); const [enableInternet, setEnableInternet] = useState(false); diff --git a/web/src/hooks/use-knowledge-request.ts b/web/src/hooks/use-knowledge-request.ts index 140565c55d..4c336d2206 100644 --- a/web/src/hooks/use-knowledge-request.ts +++ b/web/src/hooks/use-knowledge-request.ts @@ -1039,7 +1039,10 @@ export const useFetchKnowledgeList = ( }; }, getNextPageParam: (lastPage, allPages) => { - const loaded = allPages.reduce((total, page) => total + page.items.length, 0); + const loaded = allPages.reduce( + (total, page) => total + page.items.length, + 0, + ); return loaded < lastPage.total ? allPages.length + 1 : undefined; }, }); diff --git a/web/src/pages/dataset/dataset/hooks.ts b/web/src/pages/dataset/dataset/hooks.ts index 1ca57666ee..2a9bb292b4 100644 --- a/web/src/pages/dataset/dataset/hooks.ts +++ b/web/src/pages/dataset/dataset/hooks.ts @@ -24,9 +24,10 @@ export const useShowLog = (documents: IDocumentInfo[]) => { const liveDoc = liveDocs?.[0]; const logInfo = useMemo(() => { - const source = liveDoc ?? documents.find( - (item: IDocumentInfo) => item.id === record?.id, - ) ?? record; + const source = + liveDoc ?? + documents.find((item: IDocumentInfo) => item.id === record?.id) ?? + record; let log: ILogInfo = { taskId: source?.id, fileName: source?.name || '-', @@ -41,9 +42,7 @@ export const useShowLog = (documents: IDocumentInfo[]) => { fileSize: formatBytes(source.size || 0), processBeginAt: formatDate(source.process_begin_at), chunkNumber: source.chunk_count, - duration: formatSecondsToHumanReadable( - source.process_duration || 0, - ), + duration: formatSecondsToHumanReadable(source.process_duration || 0), status: source.run as RunningStatus, details: source.progress_msg, }; diff --git a/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx b/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx index b34d95f556..a216ac4f76 100644 --- a/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx +++ b/web/src/pages/user-setting/setting-model/instance-card/models-section/index.tsx @@ -169,9 +169,7 @@ export function ModelsSection(props: ModelsSectionProps) { }); // 6a. Model selection for batch verify. - const [selectedModels, setSelectedModels] = useState>( - new Set(), - ); + const [selectedModels, setSelectedModels] = useState>(new Set()); const toggleModel = useCallback((name: string) => { setSelectedModels((prev) => { diff --git a/web/src/utils/next-request.ts b/web/src/utils/next-request.ts index 97971531a1..cc52a54280 100644 --- a/web/src/utils/next-request.ts +++ b/web/src/utils/next-request.ts @@ -125,7 +125,8 @@ request.interceptors.response.use( } } - const skipErrorNotification = (response.config as any)?.skipGlobalErrorNotification; + const skipErrorNotification = (response.config as any) + ?.skipGlobalErrorNotification; if (data?.code === 100 && !skipErrorNotification) { message.error(data?.message); } else if (data?.code === 401) {