mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-01 13:33:48 +08:00
fix: propagate publish release flag through Go agent endpoints (#17347)
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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.*
|
||||
|
||||

|
||||
@@ -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.
|
||||
- **Parser selection:** For codebases, use the **"Naive"** parser for general text extraction or a specific code-aware template if available in your current version.
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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. 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.
|
||||
|
||||
@@ -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:
|
||||
|
||||

|
||||

|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
|
||||
@@ -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).
|
||||
See [Supported Models](./supported_models.mdx).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) == "" {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -169,9 +169,7 @@ export function ModelsSection(props: ModelsSectionProps) {
|
||||
});
|
||||
|
||||
// 6a. Model selection for batch verify.
|
||||
const [selectedModels, setSelectedModels] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [selectedModels, setSelectedModels] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggleModel = useCallback((name: string) => {
|
||||
setSelectedModels((prev) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user