Skip to content
Clone

feat(sagent): add labels, members, read state, repo links and artifacts

ZhenYi committed
parent 491f98e commit 21a5db1 Browse files

41 files changed

+2158 −4

This change is very large, so some diffs are not shown. Clone the repository to see the full change.

File tree 41
Modified config/default.toml View config/default.toml
@@ -434,6 +434,7 @@
434434max_pagination = 100
435435default_pagination = 20
436436max_collection_name_len = 100
437max_label_name_len = 50
437438max_title_len = 255
438439max_body_len = 65535
439440max_message_len = 65535
Modified lib/config/src/sagent.rs View lib/config/src/sagent.rs
@@ -11,6 +11,7 @@
1111 pub max_pagination: u64,
1212 pub default_pagination: u64,
1313 pub max_collection_name_len: usize,
14 pub max_label_name_len: usize,
1415 pub max_title_len: usize,
1516 pub max_body_len: usize,
1617 pub max_message_len: usize,
@@ -31,6 +32,7 @@
3132 max_pagination: 100,
3233 default_pagination: 20,
3334 max_collection_name_len: 100,
35 max_label_name_len: 50,
3436 max_title_len: 255,
3537 max_body_len: 65_535,
3638 max_message_len: 65_535,
@@ -73,6 +75,7 @@
7375 || self.default_pagination == 0
7476 || self.reconcile_interval_secs == 0
7577 || self.max_collection_name_len == 0
78 || self.max_label_name_len == 0
7679 || self.max_title_len == 0
7780 || self.max_body_len == 0
7881 || self.max_message_len == 0
Added lib/sagent/src/handlers/artifacts/get_artifact.rs View lib/sagent/src/handlers/artifacts/get_artifact.rs
@@ -0,0 +1,40 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::artifacts::AgentArtifactDto;
8use crate::services::service::SagentService;
9use crate::services::tasks::AgentPath;
10
11#[utoipa::path(
12 get,
13 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/artifacts/{artifact_id}",
14 tag = "sagent",
15 description = "读取单个产物元数据与临时下载链接。",
16 params(AgentPath, crate::services::artifacts::TaskArtifactPath),
17 responses(
18 (status = 200, description = "get artifact", body = AppResponseData<AgentArtifactDto>),
19 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
20 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
21 (status = 404, description = "not found", body = AppResponseData<NoData>)
22 )
23)]
24pub async fn get_artifact(
25 State(service): State<SagentService>,
26 session: Session,
27 Path(path): Path<crate::services::artifacts::TaskArtifactPath>,
28) -> Result<AppResponseData<AgentArtifactDto>, SagentApiError> {
29 let uid = require_uid(&session)?;
30 let dto = service
31 .get_artifact(
32 uid,
33 &path.space_name,
34 path.collection_id,
35 &path.task_id,
36 path.artifact_id,
37 )
38 .await?;
39 Ok(AppResponseData::ok(dto))
40}
Added lib/sagent/src/handlers/artifacts/list_artifacts.rs View lib/sagent/src/handlers/artifacts/list_artifacts.rs
@@ -0,0 +1,41 @@
1use axum::extract::{Path, Query, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::artifacts::{AgentArtifactPage, ListAgentArtifactQuery};
8use crate::services::service::SagentService;
9use crate::services::tasks::AgentPath;
10
11#[utoipa::path(
12 get,
13 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/artifacts",
14 tag = "sagent",
15 description = "分页列出 run 产出的产物,返回临时下载链接。",
16 params(AgentPath, ListAgentArtifactQuery),
17 responses(
18 (status = 200, description = "list artifacts", body = AppResponseData<AgentArtifactPage>),
19 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
20 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
21 (status = 404, description = "not found", body = AppResponseData<NoData>)
22 )
23)]
24pub async fn list_artifacts(
25 State(service): State<SagentService>,
26 session: Session,
27 Path(path): Path<AgentPath>,
28 Query(query): Query<ListAgentArtifactQuery>,
29) -> Result<AppResponseData<AgentArtifactPage>, SagentApiError> {
30 let uid = require_uid(&session)?;
31 let page = service
32 .list_artifacts(
33 uid,
34 &path.space_name,
35 path.collection_id,
36 &path.task_id,
37 query,
38 )
39 .await?;
40 Ok(AppResponseData::ok(page))
41}
Added lib/sagent/src/handlers/artifacts/mod.rs View lib/sagent/src/handlers/artifacts/mod.rs
@@ -0,0 +1,2 @@
1pub mod get_artifact;
2pub mod list_artifacts;
Added lib/sagent/src/handlers/labels/bind_label.rs View lib/sagent/src/handlers/labels/bind_label.rs
@@ -0,0 +1,40 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::labels::TaskLabelPath;
8use crate::services::service::SagentService;
9
10#[utoipa::path(
11 post,
12 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/labels/{label_id}",
13 tag = "sagent",
14 description = "给任务绑定标签,并在活动流记录 label_added。",
15 params(TaskLabelPath),
16 responses(
17 (status = 200, description = "bind label", body = AppResponseData<NoData>),
18 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
19 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
20 (status = 404, description = "not found", body = AppResponseData<NoData>),
21 (status = 409, description = "label already bound", body = AppResponseData<NoData>)
22 )
23)]
24pub async fn bind_label(
25 State(service): State<SagentService>,
26 session: Session,
27 Path(path): Path<TaskLabelPath>,
28) -> Result<AppResponseData<NoData>, SagentApiError> {
29 let uid = require_uid(&session)?;
30 service
31 .bind_label(
32 uid,
33 &path.space_name,
34 path.collection_id,
35 &path.task_id,
36 path.label_id,
37 )
38 .await?;
39 Ok(AppResponseData::ok(NoData))
40}
Added lib/sagent/src/handlers/labels/create_label.rs View lib/sagent/src/handlers/labels/create_label.rs
@@ -0,0 +1,37 @@
1use axum::extract::{Json, Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::labels::{AgentLabelDto, CreateAgentLabelCmd};
8use crate::services::service::SagentService;
9use crate::services::tasks::CollectionRef;
10
11#[utoipa::path(
12 post,
13 path = "/agents/{space_name}/collections/{collection_id}/labels",
14 tag = "sagent",
15 description = "在智能体集所属目录下创建标签,标签挂在目录而非单个任务上。",
16 params(CollectionRef),
17 request_body = CreateAgentLabelCmd,
18 responses(
19 (status = 200, description = "create label", body = AppResponseData<AgentLabelDto>),
20 (status = 400, description = "bad request", body = AppResponseData<NoData>),
21 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
22 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
23 (status = 404, description = "not found", body = AppResponseData<NoData>)
24 )
25)]
26pub async fn create_label(
27 State(service): State<SagentService>,
28 session: Session,
29 Path(path): Path<CollectionRef>,
30 Json(cmd): Json<CreateAgentLabelCmd>,
31) -> Result<AppResponseData<AgentLabelDto>, SagentApiError> {
32 let uid = require_uid(&session)?;
33 let dto = service
34 .create_label(uid, &path.space_name, path.collection_id, cmd)
35 .await?;
36 Ok(AppResponseData::ok(dto))
37}
Added lib/sagent/src/handlers/labels/delete_label.rs View lib/sagent/src/handlers/labels/delete_label.rs
@@ -0,0 +1,33 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::labels::AgentLabelPath;
8use crate::services::service::SagentService;
9
10#[utoipa::path(
11 delete,
12 path = "/agents/{space_name}/collections/{collection_id}/labels/{label_id}",
13 tag = "sagent",
14 description = "删除智能体标签并解除其与所有任务的绑定。",
15 params(AgentLabelPath),
16 responses(
17 (status = 200, description = "delete label", body = AppResponseData<NoData>),
18 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
19 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
20 (status = 404, description = "not found", body = AppResponseData<NoData>)
21 )
22)]
23pub async fn delete_label(
24 State(service): State<SagentService>,
25 session: Session,
26 Path(path): Path<AgentLabelPath>,
27) -> Result<AppResponseData<NoData>, SagentApiError> {
28 let uid = require_uid(&session)?;
29 service
30 .delete_label(uid, &path.space_name, path.collection_id, path.label_id)
31 .await?;
32 Ok(AppResponseData::ok(NoData))
33}
Added lib/sagent/src/handlers/labels/get_label.rs View lib/sagent/src/handlers/labels/get_label.rs
@@ -0,0 +1,33 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::labels::{AgentLabelDto, AgentLabelPath};
8use crate::services::service::SagentService;
9
10#[utoipa::path(
11 get,
12 path = "/agents/{space_name}/collections/{collection_id}/labels/{label_id}",
13 tag = "sagent",
14 description = "读取单个智能体标签。",
15 params(AgentLabelPath),
16 responses(
17 (status = 200, description = "get label", body = AppResponseData<AgentLabelDto>),
18 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
19 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
20 (status = 404, description = "not found", body = AppResponseData<NoData>)
21 )
22)]
23pub async fn get_label(
24 State(service): State<SagentService>,
25 session: Session,
26 Path(path): Path<AgentLabelPath>,
27) -> Result<AppResponseData<AgentLabelDto>, SagentApiError> {
28 let uid = require_uid(&session)?;
29 let dto = service
30 .get_label(uid, &path.space_name, path.collection_id, path.label_id)
31 .await?;
32 Ok(AppResponseData::ok(dto))
33}
Added lib/sagent/src/handlers/labels/list_labels.rs View lib/sagent/src/handlers/labels/list_labels.rs
@@ -0,0 +1,35 @@
1use axum::extract::{Path, Query, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::labels::{AgentLabelPage, ListAgentLabelQuery};
8use crate::services::service::SagentService;
9use crate::services::tasks::CollectionRef;
10
11#[utoipa::path(
12 get,
13 path = "/agents/{space_name}/collections/{collection_id}/labels",
14 tag = "sagent",
15 description = "分页列出智能体集标签,可按关键字与归档状态过滤。",
16 params(CollectionRef, ListAgentLabelQuery),
17 responses(
18 (status = 200, description = "list labels", body = AppResponseData<AgentLabelPage>),
19 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
20 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
21 (status = 404, description = "not found", body = AppResponseData<NoData>)
22 )
23)]
24pub async fn list_labels(
25 State(service): State<SagentService>,
26 session: Session,
27 Path(path): Path<CollectionRef>,
28 Query(query): Query<ListAgentLabelQuery>,
29) -> Result<AppResponseData<AgentLabelPage>, SagentApiError> {
30 let uid = require_uid(&session)?;
31 let page = service
32 .list_labels(uid, &path.space_name, path.collection_id, query)
33 .await?;
34 Ok(AppResponseData::ok(page))
35}
Added lib/sagent/src/handlers/labels/list_task_labels.rs View lib/sagent/src/handlers/labels/list_task_labels.rs
@@ -0,0 +1,34 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::labels::AgentLabelDto;
8use crate::services::service::SagentService;
9use crate::services::tasks::AgentPath;
10
11#[utoipa::path(
12 get,
13 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/labels",
14 tag = "sagent",
15 description = "列出任务上已绑定的标签。",
16 params(AgentPath),
17 responses(
18 (status = 200, description = "list task labels", body = AppResponseData<Vec<AgentLabelDto>>),
19 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
20 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
21 (status = 404, description = "not found", body = AppResponseData<NoData>)
22 )
23)]
24pub async fn list_task_labels(
25 State(service): State<SagentService>,
26 session: Session,
27 Path(path): Path<AgentPath>,
28) -> Result<AppResponseData<Vec<AgentLabelDto>>, SagentApiError> {
29 let uid = require_uid(&session)?;
30 let labels = service
31 .list_task_labels(uid, &path.space_name, path.collection_id, &path.task_id)
32 .await?;
33 Ok(AppResponseData::ok(labels))
34}
Added lib/sagent/src/handlers/labels/mod.rs View lib/sagent/src/handlers/labels/mod.rs
@@ -0,0 +1,8 @@
1pub mod bind_label;
2pub mod create_label;
3pub mod delete_label;
4pub mod get_label;
5pub mod list_labels;
6pub mod list_task_labels;
7pub mod unbind_label;
8pub mod update_label;
Added lib/sagent/src/handlers/labels/unbind_label.rs View lib/sagent/src/handlers/labels/unbind_label.rs
@@ -0,0 +1,39 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::labels::TaskLabelPath;
8use crate::services::service::SagentService;
9
10#[utoipa::path(
11 delete,
12 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/labels/{label_id}",
13 tag = "sagent",
14 description = "解除任务上的标签绑定,并在活动流记录 label_removed。",
15 params(TaskLabelPath),
16 responses(
17 (status = 200, description = "unbind label", body = AppResponseData<NoData>),
18 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
19 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
20 (status = 404, description = "not found", body = AppResponseData<NoData>)
21 )
22)]
23pub async fn unbind_label(
24 State(service): State<SagentService>,
25 session: Session,
26 Path(path): Path<TaskLabelPath>,
27) -> Result<AppResponseData<NoData>, SagentApiError> {
28 let uid = require_uid(&session)?;
29 service
30 .unbind_label(
31 uid,
32 &path.space_name,
33 path.collection_id,
34 &path.task_id,
35 path.label_id,
36 )
37 .await?;
38 Ok(AppResponseData::ok(NoData))
39}
Added lib/sagent/src/handlers/labels/update_label.rs View lib/sagent/src/handlers/labels/update_label.rs
@@ -0,0 +1,42 @@
1use axum::extract::{Json, Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::labels::{AgentLabelDto, AgentLabelPath, UpdateAgentLabelCmd};
8use crate::services::service::SagentService;
9
10#[utoipa::path(
11 patch,
12 path = "/agents/{space_name}/collections/{collection_id}/labels/{label_id}",
13 tag = "sagent",
14 description = "更新智能体标签名称、颜色、独占标记或归档状态。",
15 params(AgentLabelPath),
16 request_body = UpdateAgentLabelCmd,
17 responses(
18 (status = 200, description = "update label", body = AppResponseData<AgentLabelDto>),
19 (status = 400, description = "bad request", body = AppResponseData<NoData>),
20 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
21 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
22 (status = 404, description = "not found", body = AppResponseData<NoData>)
23 )
24)]
25pub async fn update_label(
26 State(service): State<SagentService>,
27 session: Session,
28 Path(path): Path<AgentLabelPath>,
29 Json(cmd): Json<UpdateAgentLabelCmd>,
30) -> Result<AppResponseData<AgentLabelDto>, SagentApiError> {
31 let uid = require_uid(&session)?;
32 let dto = service
33 .update_label(
34 uid,
35 &path.space_name,
36 path.collection_id,
37 path.label_id,
38 cmd,
39 )
40 .await?;
41 Ok(AppResponseData::ok(dto))
42}
Added lib/sagent/src/handlers/members/create_member.rs View lib/sagent/src/handlers/members/create_member.rs
@@ -0,0 +1,44 @@
1use axum::extract::{Json, Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::members::{AgentMemberDto, CreateAgentMemberCmd};
8use crate::services::service::SagentService;
9use crate::services::tasks::AgentPath;
10
11#[utoipa::path(
12 post,
13 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/members",
14 tag = "sagent",
15 description = "给任务添加协作人;指派(assignee)会通知被指派者。",
16 params(AgentPath),
17 request_body = CreateAgentMemberCmd,
18 responses(
19 (status = 200, description = "create member", body = AppResponseData<AgentMemberDto>),
20 (status = 400, description = "bad request", body = AppResponseData<NoData>),
21 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
22 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
23 (status = 404, description = "not found", body = AppResponseData<NoData>),
24 (status = 409, description = "member already exists", body = AppResponseData<NoData>)
25 )
26)]
27pub async fn create_member(
28 State(service): State<SagentService>,
29 session: Session,
30 Path(path): Path<AgentPath>,
31 Json(cmd): Json<CreateAgentMemberCmd>,
32) -> Result<AppResponseData<AgentMemberDto>, SagentApiError> {
33 let uid = require_uid(&session)?;
34 let dto = service
35 .create_member(
36 uid,
37 &path.space_name,
38 path.collection_id,
39 &path.task_id,
40 cmd,
41 )
42 .await?;
43 Ok(AppResponseData::ok(dto))
44}
Added lib/sagent/src/handlers/members/delete_member.rs View lib/sagent/src/handlers/members/delete_member.rs
@@ -0,0 +1,39 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::members::TaskMemberPath;
8use crate::services::service::SagentService;
9
10#[utoipa::path(
11 delete,
12 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/members/{user_id}",
13 tag = "sagent",
14 description = "移除任务协作人,并在活动流记录 unassigned。",
15 params(TaskMemberPath),
16 responses(
17 (status = 200, description = "delete member", body = AppResponseData<NoData>),
18 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
19 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
20 (status = 404, description = "not found", body = AppResponseData<NoData>)
21 )
22)]
23pub async fn delete_member(
24 State(service): State<SagentService>,
25 session: Session,
26 Path(path): Path<TaskMemberPath>,
27) -> Result<AppResponseData<NoData>, SagentApiError> {
28 let uid = require_uid(&session)?;
29 service
30 .delete_member(
31 uid,
32 &path.space_name,
33 path.collection_id,
34 &path.task_id,
35 path.user_id,
36 )
37 .await?;
38 Ok(AppResponseData::ok(NoData))
39}
Added lib/sagent/src/handlers/members/list_members.rs View lib/sagent/src/handlers/members/list_members.rs
@@ -0,0 +1,41 @@
1use axum::extract::{Path, Query, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::members::{AgentMemberPage, ListAgentMemberQuery};
8use crate::services::service::SagentService;
9use crate::services::tasks::AgentPath;
10
11#[utoipa::path(
12 get,
13 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/members",
14 tag = "sagent",
15 description = "分页列出任务协作人,可按角色过滤。",
16 params(AgentPath, ListAgentMemberQuery),
17 responses(
18 (status = 200, description = "list members", body = AppResponseData<AgentMemberPage>),
19 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
20 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
21 (status = 404, description = "not found", body = AppResponseData<NoData>)
22 )
23)]
24pub async fn list_members(
25 State(service): State<SagentService>,
26 session: Session,
27 Path(path): Path<AgentPath>,
28 Query(query): Query<ListAgentMemberQuery>,
29) -> Result<AppResponseData<AgentMemberPage>, SagentApiError> {
30 let uid = require_uid(&session)?;
31 let page = service
32 .list_members(
33 uid,
34 &path.space_name,
35 path.collection_id,
36 &path.task_id,
37 query,
38 )
39 .await?;
40 Ok(AppResponseData::ok(page))
41}
Added lib/sagent/src/handlers/members/mod.rs View lib/sagent/src/handlers/members/mod.rs
@@ -0,0 +1,3 @@
1pub mod create_member;
2pub mod delete_member;
3pub mod list_members;
Added lib/sagent/src/handlers/reads/get_read_state.rs View lib/sagent/src/handlers/reads/get_read_state.rs
@@ -0,0 +1,34 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::reads::AgentReadStateDto;
8use crate::services::service::SagentService;
9use crate::services::tasks::AgentPath;
10
11#[utoipa::path(
12 get,
13 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/read-state",
14 tag = "sagent",
15 description = "读取当前用户在该任务的已读水位与未读数。",
16 params(AgentPath),
17 responses(
18 (status = 200, description = "get read state", body = AppResponseData<AgentReadStateDto>),
19 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
20 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
21 (status = 404, description = "not found", body = AppResponseData<NoData>)
22 )
23)]
24pub async fn get_read_state(
25 State(service): State<SagentService>,
26 session: Session,
27 Path(path): Path<AgentPath>,
28) -> Result<AppResponseData<AgentReadStateDto>, SagentApiError> {
29 let uid = require_uid(&session)?;
30 let dto = service
31 .get_read_state(uid, &path.space_name, path.collection_id, &path.task_id)
32 .await?;
33 Ok(AppResponseData::ok(dto))
34}
Added lib/sagent/src/handlers/reads/mod.rs View lib/sagent/src/handlers/reads/mod.rs
@@ -0,0 +1,2 @@
1pub mod get_read_state;
2pub mod update_read_state;
Added lib/sagent/src/handlers/reads/update_read_state.rs View lib/sagent/src/handlers/reads/update_read_state.rs
@@ -0,0 +1,43 @@
1use axum::extract::{Json, Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::reads::{AgentReadStateDto, UpdateReadStateCmd};
8use crate::services::service::SagentService;
9use crate::services::tasks::AgentPath;
10
11#[utoipa::path(
12 put,
13 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/read-state",
14 tag = "sagent",
15 description = "推进已读水位;省略 last_read_seq 时以任务当前事件序号为水位。",
16 params(AgentPath),
17 request_body = UpdateReadStateCmd,
18 responses(
19 (status = 200, description = "update read state", body = AppResponseData<AgentReadStateDto>),
20 (status = 400, description = "bad request", body = AppResponseData<NoData>),
21 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
22 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
23 (status = 404, description = "not found", body = AppResponseData<NoData>)
24 )
25)]
26pub async fn update_read_state(
27 State(service): State<SagentService>,
28 session: Session,
29 Path(path): Path<AgentPath>,
30 Json(cmd): Json<UpdateReadStateCmd>,
31) -> Result<AppResponseData<AgentReadStateDto>, SagentApiError> {
32 let uid = require_uid(&session)?;
33 let dto = service
34 .mark_read(
35 uid,
36 &path.space_name,
37 path.collection_id,
38 &path.task_id,
39 cmd,
40 )
41 .await?;
42 Ok(AppResponseData::ok(dto))
43}
Added lib/sagent/src/handlers/repo_relations/create_repo_relation.rs View lib/sagent/src/handlers/repo_relations/create_repo_relation.rs
@@ -0,0 +1,44 @@
1use axum::extract::{Json, Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::repo_relations::{AgentRepoRelDto, CreateAgentRepoRelCmd};
8use crate::services::service::SagentService;
9use crate::services::tasks::AgentPath;
10
11#[utoipa::path(
12 post,
13 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/repos",
14 tag = "sagent",
15 description = "把仓库关联到任务,作为沙箱签出目标;可指定分支与子路径。",
16 params(AgentPath),
17 request_body = CreateAgentRepoRelCmd,
18 responses(
19 (status = 200, description = "create repo relation", body = AppResponseData<AgentRepoRelDto>),
20 (status = 400, description = "bad request", body = AppResponseData<NoData>),
21 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
22 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
23 (status = 404, description = "not found", body = AppResponseData<NoData>),
24 (status = 409, description = "repo relation already exists", body = AppResponseData<NoData>)
25 )
26)]
27pub async fn create_repo_relation(
28 State(service): State<SagentService>,
29 session: Session,
30 Path(path): Path<AgentPath>,
31 Json(cmd): Json<CreateAgentRepoRelCmd>,
32) -> Result<AppResponseData<AgentRepoRelDto>, SagentApiError> {
33 let uid = require_uid(&session)?;
34 let dto = service
35 .create_repo_relation(
36 uid,
37 &path.space_name,
38 path.collection_id,
39 &path.task_id,
40 cmd,
41 )
42 .await?;
43 Ok(AppResponseData::ok(dto))
44}
Added lib/sagent/src/handlers/repo_relations/delete_repo_relation.rs View lib/sagent/src/handlers/repo_relations/delete_repo_relation.rs
@@ -0,0 +1,39 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::repo_relations::TaskRepoRelPath;
8use crate::services::service::SagentService;
9
10#[utoipa::path(
11 delete,
12 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/repos/{rel_id}",
13 tag = "sagent",
14 description = "解除任务与仓库的关联。",
15 params(TaskRepoRelPath),
16 responses(
17 (status = 200, description = "delete repo relation", body = AppResponseData<NoData>),
18 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
19 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
20 (status = 404, description = "not found", body = AppResponseData<NoData>)
21 )
22)]
23pub async fn delete_repo_relation(
24 State(service): State<SagentService>,
25 session: Session,
26 Path(path): Path<TaskRepoRelPath>,
27) -> Result<AppResponseData<NoData>, SagentApiError> {
28 let uid = require_uid(&session)?;
29 service
30 .delete_repo_relation(
31 uid,
32 &path.space_name,
33 path.collection_id,
34 &path.task_id,
35 path.rel_id,
36 )
37 .await?;
38 Ok(AppResponseData::ok(NoData))
39}
Added lib/sagent/src/handlers/repo_relations/list_repo_relations.rs View lib/sagent/src/handlers/repo_relations/list_repo_relations.rs
@@ -0,0 +1,34 @@
1use axum::extract::{Path, State};
2use export::rest::{AppResponseData, NoData};
3use session::Session;
4use session::share::session::require_uid;
5
6use crate::errors::SagentApiError;
7use crate::services::repo_relations::AgentRepoRelDto;
8use crate::services::service::SagentService;
9use crate::services::tasks::AgentPath;
10
11#[utoipa::path(
12 get,
13 path = "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/repos",
14 tag = "sagent",
15 description = "列出任务已关联的仓库。",
16 params(AgentPath),
17 responses(
18 (status = 200, description = "list repo relations", body = AppResponseData<Vec<AgentRepoRelDto>>),
19 (status = 401, description = "authentication required", body = AppResponseData<NoData>),
20 (status = 403, description = "forbidden", body = AppResponseData<NoData>),
21 (status = 404, description = "not found", body = AppResponseData<NoData>)
22 )
23)]
24pub async fn list_repo_relations(
25 State(service): State<SagentService>,
26 session: Session,
27 Path(path): Path<AgentPath>,
28) -> Result<AppResponseData<Vec<AgentRepoRelDto>>, SagentApiError> {
29 let uid = require_uid(&session)?;
30 let items = service
31 .list_repo_relations(uid, &path.space_name, path.collection_id, &path.task_id)
32 .await?;
33 Ok(AppResponseData::ok(items))
34}
Added lib/sagent/src/handlers/repo_relations/mod.rs View lib/sagent/src/handlers/repo_relations/mod.rs
@@ -0,0 +1,3 @@
1pub mod create_repo_relation;
2pub mod delete_repo_relation;
3pub mod list_repo_relations;
Modified lib/sagent/src/handlers/mod.rs View lib/sagent/src/handlers/mod.rs
@@ -1,6 +1,11 @@
1pub mod artifacts;
12pub mod collections;
23pub mod events;
4pub mod labels;
5pub mod members;
36pub mod messages;
7pub mod reads;
8pub mod repo_relations;
49pub mod tasks;
510pub mod tokens;
611pub mod watches;
Added lib/sagent/src/services/artifacts.rs View lib/sagent/src/services/artifacts.rs
@@ -0,0 +1,153 @@
1use models::sagent::agent_artifact;
2use sea_orm::{
3 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
4 QuerySelect, Set,
5};
6use serde::{Deserialize, Serialize};
7use time::OffsetDateTime;
8use utoipa::{IntoParams, ToSchema};
9use uuid::Uuid;
10
11use crate::errors::SagentError;
12use crate::services::service::SagentService;
13use crate::share::lookup::task_context;
14
15const DOWNLOAD_TTL_SECS: u32 = 3600;
16
17#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
18pub struct AgentArtifactDto {
19 pub id: Uuid,
20 pub agent_id: Uuid,
21 pub run_id: Option<Uuid>,
22 pub kind: String,
23 pub name: String,
24 pub mime: Option<String>,
25 pub size: i64,
26 pub storage_key: String,
27 pub created_by: Option<Uuid>,
28 pub created_at: OffsetDateTime,
29 pub download_url: Option<String>,
30}
31
32pub type AgentArtifactPage = export::pagination::Page<AgentArtifactDto>;
33
34#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
35pub struct ListAgentArtifactQuery {
36 pub kind: Option<String>,
37 pub run_id: Option<Uuid>,
38 pub limit: Option<u64>,
39 pub offset: Option<u64>,
40}
41
42#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
43pub struct TaskArtifactPath {
44 pub space_name: String,
45 pub collection_id: Uuid,
46 pub task_id: String,
47 pub artifact_id: Uuid,
48}
49
50impl SagentService {
51 fn artifact_dto(&self, row: agent_artifact::Model) -> AgentArtifactDto {
52 let download_url = self
53 .state
54 .storage
55 .client()
56 .presign_get(&row.storage_key, DOWNLOAD_TTL_SECS)
57 .ok();
58 AgentArtifactDto {
59 id: row.id,
60 agent_id: row.agent_id,
61 run_id: row.run_id,
62 kind: row.kind,
63 name: row.name,
64 mime: row.mime,
65 size: row.size,
66 storage_key: row.storage_key,
67 created_by: row.created_by,
68 created_at: row.created_at,
69 download_url,
70 }
71 }
72
73 pub async fn record_artifact(
74 &self,
75 txn: &impl sea_orm::ConnectionTrait,
76 agent_id: Uuid,
77 run_id: Option<Uuid>,
78 kind: &str,
79 name: &str,
80 mime: Option<String>,
81 size: i64,
82 storage_key: &str,
83 ) -> Result<(), SagentError> {
84 agent_artifact::ActiveModel {
85 id: Set(Uuid::now_v7()),
86 agent_id: Set(agent_id),
87 run_id: Set(run_id),
88 kind: Set(kind.to_string()),
89 name: Set(name.to_string()),
90 mime: Set(mime),
91 size: Set(size),
92 storage_key: Set(storage_key.to_string()),
93 created_by: Set(None),
94 created_at: Set(OffsetDateTime::now_utc()),
95 }
96 .insert(txn)
97 .await?;
98 Ok(())
99 }
100
101 pub async fn list_artifacts(
102 &self,
103 caller: Uuid,
104 space_name: &str,
105 collection_id: Uuid,
106 task_ref: &str,
107 query: ListAgentArtifactQuery,
108 ) -> Result<AgentArtifactPage, SagentError> {
109 let (_, _, task) = task_context(self, caller, space_name, collection_id, task_ref).await?;
110 let (limit, offset) = self.paginate(query.limit, query.offset);
111
112 let mut select =
113 agent_artifact::Entity::find().filter(agent_artifact::Column::AgentId.eq(task.id));
114 if let Some(kind) = query.kind {
115 select = select.filter(agent_artifact::Column::Kind.eq(kind));
116 }
117 if let Some(run_id) = query.run_id {
118 select = select.filter(agent_artifact::Column::RunId.eq(run_id));
119 }
120 select = select.order_by_desc(agent_artifact::Column::CreatedAt);
121
122 let total = select.clone().count(self.db().read()).await? as i64;
123 let rows = select
124 .limit(limit)
125 .offset(offset)
126 .all(self.db().read())
127 .await?;
128
129 Ok(AgentArtifactPage {
130 items: rows.into_iter().map(|row| self.artifact_dto(row)).collect(),
131 total,
132 limit,
133 offset,
134 })
135 }
136
137 pub async fn get_artifact(
138 &self,
139 caller: Uuid,
140 space_name: &str,
141 collection_id: Uuid,
142 task_ref: &str,
143 artifact_id: Uuid,
144 ) -> Result<AgentArtifactDto, SagentError> {
145 let (_, _, task) = task_context(self, caller, space_name, collection_id, task_ref).await?;
146 let row = agent_artifact::Entity::find_by_id(artifact_id)
147 .filter(agent_artifact::Column::AgentId.eq(task.id))
148 .one(self.db().read())
149 .await?
150 .ok_or(SagentError::NotFound)?;
151 Ok(self.artifact_dto(row))
152 }
153}
Added lib/sagent/src/services/labels.rs View lib/sagent/src/services/labels.rs
@@ -0,0 +1,417 @@
1use models::sagent::agent_event_line::AgentEventType;
2use models::sagent::{agent_label, agent_relation_label};
3use sea_orm::{
4 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
5 QuerySelect, Set, TransactionTrait,
6};
7use serde::{Deserialize, Serialize};
8use time::OffsetDateTime;
9use utoipa::{IntoParams, ToSchema};
10use uuid::Uuid;
11
12use crate::errors::SagentError;
13use crate::services::service::SagentService;
14use crate::share::authz::require_collection_manage;
15use crate::share::lookup::{collection_context, task_context};
16
17#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
18pub struct AgentLabelDto {
19 pub id: Uuid,
20 pub dir_id: Uuid,
21 pub name: String,
22 pub exclusive: bool,
23 pub exclusive_order: i32,
24 pub description: Option<String>,
25 pub color: String,
26 pub is_archived: bool,
27 pub created_at: OffsetDateTime,
28 pub updated_at: Option<OffsetDateTime>,
29}
30
31impl From<agent_label::Model> for AgentLabelDto {
32 fn from(row: agent_label::Model) -> Self {
33 Self {
34 id: row.id,
35 dir_id: row.dir_id,
36 name: row.name,
37 exclusive: row.exclusive,
38 exclusive_order: row.exclusive_order,
39 description: row.description,
40 color: row.color,
41 is_archived: row.is_archived,
42 created_at: row.created_at,
43 updated_at: row.updated_at,
44 }
45 }
46}
47
48pub type AgentLabelPage = export::pagination::Page<AgentLabelDto>;
49
50#[derive(Debug, Clone, Deserialize, ToSchema)]
51pub struct CreateAgentLabelCmd {
52 pub name: String,
53 pub color: String,
54 #[serde(default)]
55 pub exclusive: Option<bool>,
56 #[serde(default)]
57 pub exclusive_order: Option<i32>,
58 #[serde(default)]
59 pub description: Option<String>,
60}
61
62#[derive(Debug, Clone, Deserialize, ToSchema)]
63pub struct UpdateAgentLabelCmd {
64 #[serde(default)]
65 pub name: Option<String>,
66 #[serde(default)]
67 pub color: Option<String>,
68 #[serde(default)]
69 pub exclusive: Option<bool>,
70 #[serde(default)]
71 pub exclusive_order: Option<i32>,
72 #[serde(default, deserialize_with = "export::serde_patch::deserialize_opt_opt")]
73 pub description: Option<Option<String>>,
74 #[serde(default)]
75 pub is_archived: Option<bool>,
76}
77
78#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
79pub struct ListAgentLabelQuery {
80 pub q: Option<String>,
81 pub is_archived: Option<bool>,
82 pub limit: Option<u64>,
83 pub offset: Option<u64>,
84}
85
86#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
87pub struct AgentLabelPath {
88 pub space_name: String,
89 pub collection_id: Uuid,
90 pub label_id: Uuid,
91}
92
93#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
94pub struct TaskLabelPath {
95 pub space_name: String,
96 pub collection_id: Uuid,
97 pub task_id: String,
98 pub label_id: Uuid,
99}
100
101fn validate_hex_color(color: &str) -> bool {
102 let clean = color.trim();
103 clean.len() == 7 && clean.starts_with('#') && clean[1..].chars().all(|c| c.is_ascii_hexdigit())
104}
105
106impl SagentService {
107 fn check_label_name(&self, name: &str) -> Result<String, SagentError> {
108 let trimmed = name.trim();
109 if trimmed.is_empty() || trimmed.chars().count() > self.config().sagent.max_label_name_len {
110 return Err(SagentError::BadRequest(
111 "label name is empty or too long".to_string(),
112 ));
113 }
114 Ok(trimmed.to_string())
115 }
116
117 fn check_label_color(&self, color: &str) -> Result<String, SagentError> {
118 let trimmed = color.trim();
119 if !validate_hex_color(trimmed) {
120 return Err(SagentError::BadRequest(
121 "invalid color format: must be #rrggbb hex code".to_string(),
122 ));
123 }
124 Ok(trimmed.to_string())
125 }
126
127 async fn lookup_label(
128 &self,
129 dir_id: Uuid,
130 label_id: Uuid,
131 ) -> Result<agent_label::Model, SagentError> {
132 agent_label::Entity::find_by_id(label_id)
133 .filter(agent_label::Column::DirId.eq(dir_id))
134 .one(self.db().read())
135 .await?
136 .ok_or(SagentError::NotFound)
137 }
138
139 pub async fn create_label(
140 &self,
141 caller: Uuid,
142 space_name: &str,
143 collection_id: Uuid,
144 cmd: CreateAgentLabelCmd,
145 ) -> Result<AgentLabelDto, SagentError> {
146 let (space, collection) =
147 collection_context(self, caller, space_name, collection_id).await?;
148 require_collection_manage(self, &space, caller).await?;
149 let name = self.check_label_name(&cmd.name)?;
150 let color = self.check_label_color(&cmd.color)?;
151 if cmd.exclusive_order.is_some_and(|order| order < 0) {
152 return Err(SagentError::BadRequest(
153 "exclusive_order must be non-negative".to_string(),
154 ));
155 }
156
157 let row = agent_label::ActiveModel {
158 id: Set(Uuid::now_v7()),
159 dir_id: Set(collection.dir_id),
160 name: Set(name),
161 exclusive: Set(cmd.exclusive.unwrap_or(false)),
162 exclusive_order: Set(cmd.exclusive_order.unwrap_or(0)),
163 description: Set(cmd.description),
164 color: Set(color),
165 is_archived: Set(false),
166 created_at: Set(OffsetDateTime::now_utc()),
167 updated_at: Set(None),
168 }
169 .insert(self.db().write())
170 .await?;
171
172 Ok(AgentLabelDto::from(row))
173 }
174
175 pub async fn list_labels(
176 &self,
177 caller: Uuid,
178 space_name: &str,
179 collection_id: Uuid,
180 query: ListAgentLabelQuery,
181 ) -> Result<AgentLabelPage, SagentError> {
182 let (_, collection) = collection_context(self, caller, space_name, collection_id).await?;
183 let (limit, offset) = self.paginate(query.limit, query.offset);
184
185 let mut select =
186 agent_label::Entity::find().filter(agent_label::Column::DirId.eq(collection.dir_id));
187 if let Some(is_archived) = query.is_archived {
188 select = select.filter(agent_label::Column::IsArchived.eq(is_archived));
189 }
190 if let Some(search) = query.q {
191 let keyword = search.trim();
192 if !keyword.is_empty() {
193 select = select.filter(agent_label::Column::Name.contains(keyword));
194 }
195 }
196 select = select.order_by_desc(agent_label::Column::CreatedAt);
197
198 let total = select.clone().count(self.db().read()).await? as i64;
199 let rows = select
200 .limit(limit)
201 .offset(offset)
202 .all(self.db().read())
203 .await?;
204
205 Ok(AgentLabelPage {
206 items: rows.into_iter().map(AgentLabelDto::from).collect(),
207 total,
208 limit,
209 offset,
210 })
211 }
212
213 pub async fn get_label(
214 &self,
215 caller: Uuid,
216 space_name: &str,
217 collection_id: Uuid,
218 label_id: Uuid,
219 ) -> Result<AgentLabelDto, SagentError> {
220 let (_, collection) = collection_context(self, caller, space_name, collection_id).await?;
221 let row = self.lookup_label(collection.dir_id, label_id).await?;
222 Ok(AgentLabelDto::from(row))
223 }
224
225 pub async fn update_label(
226 &self,
227 caller: Uuid,
228 space_name: &str,
229 collection_id: Uuid,
230 label_id: Uuid,
231 cmd: UpdateAgentLabelCmd,
232 ) -> Result<AgentLabelDto, SagentError> {
233 let (space, collection) =
234 collection_context(self, caller, space_name, collection_id).await?;
235 require_collection_manage(self, &space, caller).await?;
236 let row = self.lookup_label(collection.dir_id, label_id).await?;
237
238 let mut update: agent_label::ActiveModel = row.into();
239 if let Some(name) = cmd.name {
240 update.name = Set(self.check_label_name(&name)?);
241 }
242 if let Some(color) = cmd.color {
243 update.color = Set(self.check_label_color(&color)?);
244 }
245 if let Some(exclusive) = cmd.exclusive {
246 update.exclusive = Set(exclusive);
247 }
248 if let Some(order) = cmd.exclusive_order {
249 if order < 0 {
250 return Err(SagentError::BadRequest(
251 "exclusive_order must be non-negative".to_string(),
252 ));
253 }
254 update.exclusive_order = Set(order);
255 }
256 if let Some(description) = cmd.description {
257 update.description = Set(description);
258 }
259 if let Some(is_archived) = cmd.is_archived {
260 update.is_archived = Set(is_archived);
261 }
262 update.updated_at = Set(Some(OffsetDateTime::now_utc()));
263
264 let updated = update.update(self.db().write()).await?;
265 Ok(AgentLabelDto::from(updated))
266 }
267
268 pub async fn delete_label(
269 &self,
270 caller: Uuid,
271 space_name: &str,
272 collection_id: Uuid,
273 label_id: Uuid,
274 ) -> Result<(), SagentError> {
275 let (space, collection) =
276 collection_context(self, caller, space_name, collection_id).await?;
277 require_collection_manage(self, &space, caller).await?;
278 let _ = self.lookup_label(collection.dir_id, label_id).await?;
279
280 self.db()
281 .primary()
282 .transaction::<_, (), SagentError>(|txn| {
283 Box::pin(async move {
284 agent_relation_label::Entity::delete_many()
285 .filter(agent_relation_label::Column::LabelId.eq(label_id))
286 .exec(txn)
287 .await?;
288 agent_label::Entity::delete_by_id(label_id)
289 .exec(txn)
290 .await?;
291 Ok(())
292 })
293 })
294 .await?;
295
296 Ok(())
297 }
298
299 pub async fn list_task_labels(
300 &self,
301 caller: Uuid,
302 space_name: &str,
303 collection_id: Uuid,
304 task_ref: &str,
305 ) -> Result<Vec<AgentLabelDto>, SagentError> {
306 let (_, _, task) = task_context(self, caller, space_name, collection_id, task_ref).await?;
307 self.task_labels(task.id).await
308 }
309
310 pub(crate) async fn task_labels(
311 &self,
312 task_id: Uuid,
313 ) -> Result<Vec<AgentLabelDto>, SagentError> {
314 let label_ids: Vec<Uuid> = agent_relation_label::Entity::find()
315 .filter(agent_relation_label::Column::AgentId.eq(task_id))
316 .all(self.db().read())
317 .await?
318 .into_iter()
319 .map(|row| row.label_id)
320 .collect();
321 if label_ids.is_empty() {
322 return Ok(Vec::new());
323 }
324 let rows = agent_label::Entity::find()
325 .filter(agent_label::Column::Id.is_in(label_ids))
326 .all(self.db().read())
327 .await?;
328 Ok(rows.into_iter().map(AgentLabelDto::from).collect())
329 }
330
331 pub async fn bind_label(
332 &self,
333 caller: Uuid,
334 space_name: &str,
335 collection_id: Uuid,
336 task_ref: &str,
337 label_id: Uuid,
338 ) -> Result<(), SagentError> {
339 let (space, collection, task) =
340 task_context(self, caller, space_name, collection_id, task_ref).await?;
341 crate::share::authz::require_task_write(self, &space, caller, &task).await?;
342 let label = self.lookup_label(collection.dir_id, label_id).await?;
343
344 let existing = agent_relation_label::Entity::find()
345 .filter(agent_relation_label::Column::AgentId.eq(task.id))
346 .filter(agent_relation_label::Column::LabelId.eq(label_id))
347 .one(self.db().read())
348 .await?;
349 if existing.is_some() {
350 return Err(SagentError::Conflict(
351 "label already bound to task".to_string(),
352 ));
353 }
354
355 let txn = self.db().write().begin().await?;
356 agent_relation_label::ActiveModel {
357 id: Set(Uuid::now_v7()),
358 agent_id: Set(task.id),
359 label_id: Set(label_id),
360 created_at: Set(OffsetDateTime::now_utc()),
361 }
362 .insert(&txn)
363 .await?;
364 self.write_event(
365 &txn,
366 task.id,
367 collection.id,
368 Some(caller),
369 AgentEventType::LabelAdded,
370 None,
371 Some(serde_json::json!({ "label": label.name })),
372 )
373 .await?;
374 txn.commit().await?;
375
376 Ok(())
377 }
378
379 pub async fn unbind_label(
380 &self,
381 caller: Uuid,
382 space_name: &str,
383 collection_id: Uuid,
384 task_ref: &str,
385 label_id: Uuid,
386 ) -> Result<(), SagentError> {
387 let (space, collection, task) =
388 task_context(self, caller, space_name, collection_id, task_ref).await?;
389 crate::share::authz::require_task_write(self, &space, caller, &task).await?;
390 let label = self.lookup_label(collection.dir_id, label_id).await?;
391
392 let relation = agent_relation_label::Entity::find()
393 .filter(agent_relation_label::Column::AgentId.eq(task.id))
394 .filter(agent_relation_label::Column::LabelId.eq(label_id))
395 .one(self.db().read())
396 .await?
397 .ok_or(SagentError::NotFound)?;
398
399 let txn = self.db().write().begin().await?;
400 agent_relation_label::Entity::delete_by_id(relation.id)
401 .exec(&txn)
402 .await?;
403 self.write_event(
404 &txn,
405 task.id,
406 collection.id,
407 Some(caller),
408 AgentEventType::LabelRemoved,
409 None,
410 Some(serde_json::json!({ "label": label.name })),
411 )
412 .await?;
413 txn.commit().await?;
414
415 Ok(())
416 }
417}
Added lib/sagent/src/services/members.rs View lib/sagent/src/services/members.rs
@@ -0,0 +1,265 @@
1use models::sagent::agent_event_line::AgentEventType;
2use models::sagent::agent_member::{self, AgentMemberKind};
3use models::space::space_members::{self, MemberStatus};
4use models::users::user_notifications::NotificationType;
5use sea_orm::{
6 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
7 QuerySelect, Set, TransactionTrait,
8};
9use serde::{Deserialize, Serialize};
10use space::services::spaces::UserDto;
11use time::OffsetDateTime;
12use utoipa::{IntoParams, ToSchema};
13use uuid::Uuid;
14
15use crate::errors::SagentError;
16use crate::services::service::SagentService;
17use crate::share::authz::require_task_write;
18use crate::share::lookup::task_context;
19
20#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
21pub struct AgentMemberDto {
22 pub id: Uuid,
23 pub agent_id: Uuid,
24 pub user: Option<UserDto>,
25 pub kind: AgentMemberKind,
26 pub created_at: OffsetDateTime,
27 pub updated_at: Option<OffsetDateTime>,
28}
29
30pub type AgentMemberPage = export::pagination::Page<AgentMemberDto>;
31
32#[derive(Debug, Clone, Deserialize, ToSchema)]
33pub struct CreateAgentMemberCmd {
34 pub user_id: Uuid,
35 #[serde(default)]
36 pub kind: Option<AgentMemberKind>,
37}
38
39#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
40pub struct ListAgentMemberQuery {
41 #[param(value_type = String, required = false)]
42 pub kind: Option<AgentMemberKind>,
43 pub limit: Option<u64>,
44 pub offset: Option<u64>,
45}
46
47#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
48pub struct TaskMemberPath {
49 pub space_name: String,
50 pub collection_id: Uuid,
51 pub task_id: String,
52 pub user_id: Uuid,
53}
54
55impl SagentService {
56 async fn require_space_member(
57 &self,
58 space_id: Uuid,
59 owner_id: Uuid,
60 user_id: Uuid,
61 ) -> Result<(), SagentError> {
62 if user_id == owner_id {
63 return Ok(());
64 }
65 let member = space_members::Entity::find_by_id((space_id, user_id))
66 .one(self.db().read())
67 .await?;
68 match member {
69 Some(row) if row.status == MemberStatus::Active => Ok(()),
70 _ => Err(SagentError::Forbidden),
71 }
72 }
73
74 pub async fn create_member(
75 &self,
76 caller: Uuid,
77 space_name: &str,
78 collection_id: Uuid,
79 task_ref: &str,
80 cmd: CreateAgentMemberCmd,
81 ) -> Result<AgentMemberDto, SagentError> {
82 let (space, collection, task) =
83 task_context(self, caller, space_name, collection_id, task_ref).await?;
84 require_task_write(self, &space, caller, &task).await?;
85 self.require_space_member(space.id, space.owner_id, cmd.user_id)
86 .await?;
87 let kind = cmd.kind.unwrap_or(AgentMemberKind::Assignee);
88
89 let existing = agent_member::Entity::find()
90 .filter(agent_member::Column::AgentId.eq(task.id))
91 .filter(agent_member::Column::UserId.eq(cmd.user_id))
92 .filter(agent_member::Column::Kind.eq(kind))
93 .one(self.db().read())
94 .await?;
95 if existing.is_some() {
96 return Err(SagentError::Conflict(
97 "member already exists with this role".to_string(),
98 ));
99 }
100
101 let now = OffsetDateTime::now_utc();
102 let member_id = Uuid::now_v7();
103 let target_uid = cmd.user_id;
104 let task_title = task.title.clone();
105 let task_number = task.autoincrement_id;
106 let collection_id_for_txn = collection.id;
107 let this = self.clone();
108 let space_lower = space.lower_name.clone();
109 let pending = export::notify::PendingNotifications::default();
110 let pending_txn = pending.clone();
111
112 let inserted = self
113 .db()
114 .primary()
115 .transaction::<_, agent_member::Model, SagentError>(move |txn| {
116 let title = task_title.clone();
117 let space_lower = space_lower.clone();
118 Box::pin(async move {
119 let row = agent_member::ActiveModel {
120 id: Set(member_id),
121 agent_id: Set(task.id),
122 user_id: Set(target_uid),
123 kind: Set(kind),
124 created_at: Set(now),
125 updated_at: Set(None),
126 };
127 let inserted = row.insert(txn).await?;
128
129 this.write_event(
130 txn,
131 task.id,
132 collection_id_for_txn,
133 Some(caller),
134 AgentEventType::Assigned,
135 None,
136 Some(serde_json::json!({ "user_id": target_uid, "kind": kind })),
137 )
138 .await?;
139
140 if kind == AgentMemberKind::Assignee && target_uid != caller {
141 let data = serde_json::json!({
142 "space_name": space_lower,
143 "collection_id": collection_id_for_txn,
144 "agent_id": task.id,
145 "number": task_number,
146 });
147 this.notify_user(
148 txn,
149 &pending_txn,
150 target_uid,
151 NotificationType::Assignment,
152 format!("You were assigned to agent task #{task_number}: {title}"),
153 Some(title),
154 Some(data),
155 Some(caller),
156 )
157 .await?;
158 }
159
160 Ok(inserted)
161 })
162 })
163 .await?;
164 pending.flush(&self.state.redis).await;
165
166 let briefs = self.load_user_briefs(&[cmd.user_id]).await?;
167 Ok(AgentMemberDto {
168 id: inserted.id,
169 agent_id: inserted.agent_id,
170 user: briefs.get(&cmd.user_id).cloned(),
171 kind: inserted.kind,
172 created_at: inserted.created_at,
173 updated_at: inserted.updated_at,
174 })
175 }
176
177 pub async fn list_members(
178 &self,
179 caller: Uuid,
180 space_name: &str,
181 collection_id: Uuid,
182 task_ref: &str,
183 query: ListAgentMemberQuery,
184 ) -> Result<AgentMemberPage, SagentError> {
185 let (_, _, task) = task_context(self, caller, space_name, collection_id, task_ref).await?;
186 let (limit, offset) = self.paginate(query.limit, query.offset);
187
188 let mut select =
189 agent_member::Entity::find().filter(agent_member::Column::AgentId.eq(task.id));
190 if let Some(kind) = query.kind {
191 select = select.filter(agent_member::Column::Kind.eq(kind));
192 }
193 select = select.order_by_asc(agent_member::Column::CreatedAt);
194
195 let total = select.clone().count(self.db().read()).await? as i64;
196 let rows = select
197 .limit(limit)
198 .offset(offset)
199 .all(self.db().read())
200 .await?;
201
202 let uids: Vec<Uuid> = rows.iter().map(|row| row.user_id).collect();
203 let briefs = self.load_user_briefs(&uids).await?;
204 let items = rows
205 .into_iter()
206 .map(|row| AgentMemberDto {
207 id: row.id,
208 agent_id: row.agent_id,
209 user: briefs.get(&row.user_id).cloned(),
210 kind: row.kind,
211 created_at: row.created_at,
212 updated_at: row.updated_at,
213 })
214 .collect();
215
216 Ok(AgentMemberPage {
217 items,
218 total,
219 limit,
220 offset,
221 })
222 }
223
224 pub async fn delete_member(
225 &self,
226 caller: Uuid,
227 space_name: &str,
228 collection_id: Uuid,
229 task_ref: &str,
230 user_id: Uuid,
231 ) -> Result<(), SagentError> {
232 let (space, collection, task) =
233 task_context(self, caller, space_name, collection_id, task_ref).await?;
234 require_task_write(self, &space, caller, &task).await?;
235
236 let rows = agent_member::Entity::find()
237 .filter(agent_member::Column::AgentId.eq(task.id))
238 .filter(agent_member::Column::UserId.eq(user_id))
239 .all(self.db().read())
240 .await?;
241 if rows.is_empty() {
242 return Err(SagentError::NotFound);
243 }
244
245 let txn = self.db().write().begin().await?;
246 for row in rows {
247 agent_member::Entity::delete_by_id(row.id)
248 .exec(&txn)
249 .await?;
250 self.write_event(
251 &txn,
252 task.id,
253 collection.id,
254 Some(caller),
255 AgentEventType::Unassigned,
256 None,
257 Some(serde_json::json!({ "user_id": user_id, "kind": row.kind })),
258 )
259 .await?;
260 }
261 txn.commit().await?;
262
263 Ok(())
264 }
265}
Modified lib/sagent/src/services/mod.rs View lib/sagent/src/services/mod.rs
@@ -1,6 +1,11 @@
1pub mod artifacts;
12pub mod collections;
23pub mod events;
4pub mod labels;
5pub mod members;
36pub mod messages;
7pub mod reads;
8pub mod repo_relations;
49pub mod service;
510pub mod subscribe;
611pub mod tasks;
Added lib/sagent/src/services/reads.rs View lib/sagent/src/services/reads.rs
@@ -0,0 +1,102 @@
1use models::sagent::agent_user;
2use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set};
3use serde::{Deserialize, Serialize};
4use time::OffsetDateTime;
5use utoipa::ToSchema;
6use uuid::Uuid;
7
8use crate::errors::SagentError;
9use crate::services::service::SagentService;
10use crate::share::lookup::task_context;
11
12#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
13pub struct AgentReadStateDto {
14 pub agent_id: Uuid,
15 pub user_id: Uuid,
16 pub last_read_seq: i64,
17 pub last_event_seq: i64,
18 pub unread: i64,
19 pub updated_at: Option<OffsetDateTime>,
20}
21
22#[derive(Debug, Clone, Deserialize, ToSchema)]
23pub struct UpdateReadStateCmd {
24 #[serde(default)]
25 pub last_read_seq: Option<i64>,
26}
27
28impl SagentService {
29 pub async fn get_read_state(
30 &self,
31 caller: Uuid,
32 space_name: &str,
33 collection_id: Uuid,
34 task_ref: &str,
35 ) -> Result<AgentReadStateDto, SagentError> {
36 let (_, _, task) = task_context(self, caller, space_name, collection_id, task_ref).await?;
37 let row = agent_user::Entity::find()
38 .filter(agent_user::Column::Uid.eq(caller))
39 .filter(agent_user::Column::AgentId.eq(task.id))
40 .one(self.db().read())
41 .await?;
42 let last_read_seq = row.as_ref().map(|row| row.last_read_seq).unwrap_or(0);
43 Ok(AgentReadStateDto {
44 agent_id: task.id,
45 user_id: caller,
46 last_read_seq,
47 last_event_seq: task.last_event_seq,
48 unread: (task.last_event_seq - last_read_seq).max(0),
49 updated_at: row.and_then(|row| row.updated_at),
50 })
51 }
52
53 pub async fn mark_read(
54 &self,
55 caller: Uuid,
56 space_name: &str,
57 collection_id: Uuid,
58 task_ref: &str,
59 cmd: UpdateReadStateCmd,
60 ) -> Result<AgentReadStateDto, SagentError> {
61 let (_, _, task) = task_context(self, caller, space_name, collection_id, task_ref).await?;
62 let target = cmd.last_read_seq.unwrap_or(task.last_event_seq).max(0);
63
64 let existing = agent_user::Entity::find()
65 .filter(agent_user::Column::Uid.eq(caller))
66 .filter(agent_user::Column::AgentId.eq(task.id))
67 .one(self.db().read())
68 .await?;
69
70 let now = OffsetDateTime::now_utc();
71 let row = match existing {
72 Some(row) => {
73 let last_read_seq = row.last_read_seq.max(target);
74 let mut update: agent_user::ActiveModel = row.into();
75 update.last_read_seq = Set(last_read_seq);
76 update.updated_at = Set(Some(now));
77 update.update(self.db().write()).await?
78 }
79 None => {
80 agent_user::ActiveModel {
81 id: Set(Uuid::now_v7()),
82 uid: Set(caller),
83 agent_id: Set(task.id),
84 last_read_seq: Set(target),
85 created_at: Set(now),
86 updated_at: Set(None),
87 }
88 .insert(self.db().write())
89 .await?
90 }
91 };
92
93 Ok(AgentReadStateDto {
94 agent_id: task.id,
95 user_id: caller,
96 last_read_seq: row.last_read_seq,
97 last_event_seq: task.last_event_seq,
98 unread: (task.last_event_seq - row.last_read_seq).max(0),
99 updated_at: row.updated_at,
100 })
101 }
102}
Added lib/sagent/src/services/repo_relations.rs View lib/sagent/src/services/repo_relations.rs
@@ -0,0 +1,190 @@
1use models::dir::dirs;
2use models::repo::repos;
3use models::sagent::agent_event_line::AgentEventType;
4use models::sagent::agent_relation_repo;
5use sea_orm::{
6 ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set, TransactionTrait,
7};
8use serde::{Deserialize, Serialize};
9use time::OffsetDateTime;
10use utoipa::{IntoParams, ToSchema};
11use uuid::Uuid;
12
13use crate::errors::SagentError;
14use crate::services::service::SagentService;
15use crate::share::authz::require_task_write;
16use crate::share::lookup::task_context;
17
18#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
19pub struct AgentRepoRelDto {
20 pub id: Uuid,
21 pub agent_id: Uuid,
22 pub repo_id: Uuid,
23 pub repo_name: Option<String>,
24 pub ref_name: Option<String>,
25 pub sub_path: Option<String>,
26 pub created_at: OffsetDateTime,
27}
28
29#[derive(Debug, Clone, Deserialize, ToSchema)]
30pub struct CreateAgentRepoRelCmd {
31 pub repo_id: Uuid,
32 #[serde(default)]
33 pub ref_name: Option<String>,
34 #[serde(default)]
35 pub sub_path: Option<String>,
36}
37
38#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
39pub struct TaskRepoRelPath {
40 pub space_name: String,
41 pub collection_id: Uuid,
42 pub task_id: String,
43 pub rel_id: Uuid,
44}
45
46impl SagentService {
47 pub async fn create_repo_relation(
48 &self,
49 caller: Uuid,
50 space_name: &str,
51 collection_id: Uuid,
52 task_ref: &str,
53 cmd: CreateAgentRepoRelCmd,
54 ) -> Result<AgentRepoRelDto, SagentError> {
55 let (space, collection, task) =
56 task_context(self, caller, space_name, collection_id, task_ref).await?;
57 require_task_write(self, &space, caller, &task).await?;
58
59 let repo = repos::Entity::find_by_id(cmd.repo_id)
60 .one(self.db().read())
61 .await?
62 .ok_or(SagentError::NotFound)?;
63 let dir = dirs::Entity::find_by_id(repo.dir_id)
64 .one(self.db().read())
65 .await?
66 .ok_or(SagentError::NotFound)?;
67 if dir.space_id != Some(space.id) {
68 return Err(SagentError::NotFound);
69 }
70
71 let existing = agent_relation_repo::Entity::find()
72 .filter(agent_relation_repo::Column::AgentId.eq(task.id))
73 .filter(agent_relation_repo::Column::RepoId.eq(cmd.repo_id))
74 .one(self.db().read())
75 .await?;
76 if existing.is_some() {
77 return Err(SagentError::Conflict(
78 "repo relation already exists".to_string(),
79 ));
80 }
81
82 let txn = self.db().write().begin().await?;
83 let inserted = agent_relation_repo::ActiveModel {
84 id: Set(Uuid::now_v7()),
85 agent_id: Set(task.id),
86 repo_id: Set(cmd.repo_id),
87 ref_name: Set(cmd.ref_name),
88 sub_path: Set(cmd.sub_path),
89 created_at: Set(OffsetDateTime::now_utc()),
90 }
91 .insert(&txn)
92 .await?;
93 self.write_event(
94 &txn,
95 task.id,
96 collection.id,
97 Some(caller),
98 AgentEventType::RepoRelationAdded,
99 None,
100 Some(serde_json::json!({ "repo_id": cmd.repo_id, "repo_name": repo.name })),
101 )
102 .await?;
103 txn.commit().await?;
104
105 Ok(AgentRepoRelDto {
106 id: inserted.id,
107 agent_id: inserted.agent_id,
108 repo_id: inserted.repo_id,
109 repo_name: Some(repo.name),
110 ref_name: inserted.ref_name,
111 sub_path: inserted.sub_path,
112 created_at: inserted.created_at,
113 })
114 }
115
116 pub async fn list_repo_relations(
117 &self,
118 caller: Uuid,
119 space_name: &str,
120 collection_id: Uuid,
121 task_ref: &str,
122 ) -> Result<Vec<AgentRepoRelDto>, SagentError> {
123 let (_, _, task) = task_context(self, caller, space_name, collection_id, task_ref).await?;
124 let rows = agent_relation_repo::Entity::find()
125 .filter(agent_relation_repo::Column::AgentId.eq(task.id))
126 .order_by_asc(agent_relation_repo::Column::CreatedAt)
127 .all(self.db().read())
128 .await?;
129 if rows.is_empty() {
130 return Ok(Vec::new());
131 }
132 let repo_ids: Vec<Uuid> = rows.iter().map(|row| row.repo_id).collect();
133 let repos = repos::Entity::find()
134 .filter(repos::Column::Id.is_in(repo_ids))
135 .all(self.db().read())
136 .await?;
137 let names: std::collections::HashMap<Uuid, String> =
138 repos.into_iter().map(|repo| (repo.id, repo.name)).collect();
139
140 Ok(rows
141 .into_iter()
142 .map(|row| AgentRepoRelDto {
143 id: row.id,
144 agent_id: row.agent_id,
145 repo_id: row.repo_id,
146 repo_name: names.get(&row.repo_id).cloned(),
147 ref_name: row.ref_name,
148 sub_path: row.sub_path,
149 created_at: row.created_at,
150 })
151 .collect())
152 }
153
154 pub async fn delete_repo_relation(
155 &self,
156 caller: Uuid,
157 space_name: &str,
158 collection_id: Uuid,
159 task_ref: &str,
160 rel_id: Uuid,
161 ) -> Result<(), SagentError> {
162 let (space, collection, task) =
163 task_context(self, caller, space_name, collection_id, task_ref).await?;
164 require_task_write(self, &space, caller, &task).await?;
165
166 let row = agent_relation_repo::Entity::find_by_id(rel_id)
167 .filter(agent_relation_repo::Column::AgentId.eq(task.id))
168 .one(self.db().read())
169 .await?
170 .ok_or(SagentError::NotFound)?;
171
172 let txn = self.db().write().begin().await?;
173 agent_relation_repo::Entity::delete_by_id(row.id)
174 .exec(&txn)
175 .await?;
176 self.write_event(
177 &txn,
178 task.id,
179 collection.id,
180 Some(caller),
181 AgentEventType::RepoRelationRemoved,
182 None,
183 Some(serde_json::json!({ "repo_id": row.repo_id })),
184 )
185 .await?;
186 txn.commit().await?;
187
188 Ok(())
189 }
190}
Modified lib/sagent/src/services/service.rs View lib/sagent/src/services/service.rs
@@ -1,3 +1,4 @@
1use std::collections::HashMap;
12use std::sync::{Arc, OnceLock};
23
34use async_trait::async_trait;
@@ -5,9 +6,12 @@
56use export::AppState;
67use export::database::AppDatabase;
78use models::sagent::agent_event_line::{self, AgentEventType};
8use sea_orm::{ActiveModelTrait, ConnectionTrait, Set};
9use models::users::user;
10use models::users::user_notifications::{self, NotificationType};
11use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectionTrait, EntityTrait, QueryFilter, Set};
912use session::SessionError;
1013use session::share::guard::{SessionGuard, SessionWithUser};
14use space::services::spaces::UserDto;
1115use time::OffsetDateTime;
1216use uuid::Uuid;
1317
@@ -79,6 +83,55 @@
7983 .await?;
8084 Ok(())
8185 }
86
87 pub(crate) async fn load_user_briefs(
88 &self,
89 uids: &[Uuid],
90 ) -> Result<HashMap<Uuid, UserDto>, SagentError> {
91 if uids.is_empty() {
92 return Ok(HashMap::new());
93 }
94 let mut set: Vec<Uuid> = uids.to_vec();
95 set.sort();
96 set.dedup();
97 let users = user::Entity::find()
98 .filter(user::Column::Uid.is_in(set))
99 .all(self.db().read())
100 .await?;
101 Ok(users
102 .into_iter()
103 .map(|row| (row.uid, UserDto::from(row)))
104 .collect())
105 }
106
107 #[allow(clippy::too_many_arguments)]
108 pub(crate) async fn notify_user(
109 &self,
110 txn: &impl ConnectionTrait,
111 pending: &export::notify::PendingNotifications,
112 recipient: Uuid,
113 ntype: NotificationType,
114 title: String,
115 body: Option<String>,
116 data: Option<serde_json::Value>,
117 actor_uid: Option<Uuid>,
118 ) -> Result<(), SagentError> {
119 let row = user_notifications::ActiveModel {
120 id: Set(Uuid::now_v7()),
121 user_id: Set(recipient),
122 notification_type: Set(ntype),
123 title: Set(title),
124 body: Set(body),
125 data_json: Set(data),
126 actor_uid: Set(actor_uid),
127 read_at: Set(None),
128 created_at: Set(OffsetDateTime::now_utc()),
129 }
130 .insert(txn)
131 .await?;
132 pending.push(recipient, row.to_event_value());
133 Ok(())
134 }
82135}
83136
84137#[async_trait]
Modified lib/sagent/src/services/subscribe.rs View lib/sagent/src/services/subscribe.rs
@@ -1,6 +1,7 @@
11use std::time::Duration;
22
33use agent_sdk::SseEvent;
4use agent_types::event::AgentEvent;
45use agent_types::session::{MessageDto, MessageRole};
56use models::sagent::agent;
67use models::sagent::agent_event_line::AgentEventType;
@@ -28,6 +29,20 @@
2829 .unwrap_or_else(|_| OffsetDateTime::now_utc())
2930}
3031
32fn state_name(state: models::sagent::agent::AgentState) -> &'static str {
33 match state {
34 models::sagent::agent::AgentState::Draft => "draft",
35 models::sagent::agent::AgentState::Pending => "pending",
36 models::sagent::agent::AgentState::Planning => "planning",
37 models::sagent::agent::AgentState::Executing => "executing",
38 models::sagent::agent::AgentState::Waiting => "waiting",
39 models::sagent::agent::AgentState::Succeeded => "succeeded",
40 models::sagent::agent::AgentState::Failed => "failed",
41 models::sagent::agent::AgentState::Canceled => "canceled",
42 models::sagent::agent::AgentState::Closed => "closed",
43 }
44}
45
3146impl SagentService {
3247 pub(crate) async fn load_task(
3348 &self,
@@ -124,6 +139,10 @@
124139 Ok(false)
125140 }
126141 SseEvent::RunEvent { run_id, seq, event } => {
142 let sealed = match &event {
143 AgentEvent::ArtifactSealed { artifact, .. } => Some(artifact.clone()),
144 _ => None,
145 };
127146 let txn = self.db().write().begin().await?;
128147 self.write_event(
129148 &txn,
@@ -135,6 +154,21 @@
135154 Some(serde_json::json!({ "seq": seq, "event": event })),
136155 )
137156 .await?;
157 if let Some(artifact) = sealed {
158 let key = artifact.object.key.to_string();
159 let name = key.rsplit('/').next().unwrap_or(&key).to_string();
160 self.record_artifact(
161 &txn,
162 task.id,
163 Some(run_id.as_uuid()),
164 artifact.kind.as_str(),
165 &name,
166 None,
167 artifact.size_bytes.min(i64::MAX as u64) as i64,
168 &key,
169 )
170 .await?;
171 }
138172 txn.commit().await?;
139173 Ok(false)
140174 }
@@ -214,7 +248,7 @@
214248 if terminal {
215249 update = update.col_expr(agent::Column::FinishedAt, Expr::value(Some(now)));
216250 }
217 update.exec(&txn).await?;
251 let changed = update.exec(&txn).await?.rows_affected > 0;
218252
219253 self.write_event(
220254 &txn,
@@ -226,11 +260,61 @@
226260 Some(serde_json::json!({ "state": mapped, "terminal": terminal })),
227261 )
228262 .await?;
263
264 let pending = export::notify::PendingNotifications::default();
265 if changed && terminal {
266 self.notify_watchers(&txn, &pending, task, mapped).await?;
267 }
229268 txn.commit().await?;
269 pending.flush(&self.state.redis).await;
230270
231271 Ok(terminal)
232272 }
233273
274 async fn notify_watchers(
275 &self,
276 txn: &impl sea_orm::ConnectionTrait,
277 pending: &export::notify::PendingNotifications,
278 task: &agent::Model,
279 state: models::sagent::agent::AgentState,
280 ) -> Result<(), SagentError> {
281 use models::common::watch_mode::WatchMode;
282 use models::sagent::agent_watch;
283 use models::users::user_notifications::NotificationType;
284
285 let watchers = agent_watch::Entity::find()
286 .filter(agent_watch::Column::AgentId.eq(task.id))
287 .filter(agent_watch::Column::Mode.eq(WatchMode::Watching))
288 .all(self.db().read())
289 .await?;
290
291 let data = serde_json::json!({
292 "collection_id": task.collection_id,
293 "agent_id": task.id,
294 "number": task.autoincrement_id,
295 "state": state,
296 });
297 for watcher in watchers {
298 self.notify_user(
299 txn,
300 pending,
301 watcher.user_id,
302 NotificationType::System,
303 format!(
304 "Agent task #{} {}: {}",
305 task.autoincrement_id,
306 state_name(state),
307 task.title
308 ),
309 Some(task.title.clone()),
310 Some(data.clone()),
311 None,
312 )
313 .await?;
314 }
315 Ok(())
316 }
317
234318 async fn mark_subscriber_lost(&self, task_id: Uuid, reason: &str) -> Result<(), SagentError> {
235319 let Some(task) = self.load_task(task_id).await? else {
236320 return Ok(());
Modified lib/sagent/src/services/tasks.rs View lib/sagent/src/services/tasks.rs
@@ -1,5 +1,6 @@
11use models::sagent::agent::{AgentPriority, AgentState};
2use models::sagent::{agent, agent_event_line, agent_message, agent_watch};
2use models::sagent::agent_member::{self, AgentMemberKind};
3use models::sagent::{agent, agent_event_line, agent_message, agent_relation_label, agent_watch};
34use sea_orm::sea_query::LockType;
45use sea_orm::{
56 ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder,
@@ -74,6 +75,8 @@
7475 pub state: Option<AgentState>,
7576 pub priority: Option<AgentPriority>,
7677 pub author_id: Option<Uuid>,
78 pub assignee_id: Option<Uuid>,
79 pub label_id: Option<Uuid>,
7780 pub sort: Option<String>,
7881 pub limit: Option<u64>,
7982 pub offset: Option<u64>,
@@ -230,6 +233,27 @@
230233 if let Some(author_id) = query.author_id {
231234 select = select.filter(agent::Column::AuthorId.eq(author_id));
232235 }
236 if let Some(assignee_id) = query.assignee_id {
237 let task_ids: Vec<Uuid> = agent_member::Entity::find()
238 .filter(agent_member::Column::UserId.eq(assignee_id))
239 .filter(agent_member::Column::Kind.eq(AgentMemberKind::Assignee))
240 .all(self.db().read())
241 .await?
242 .into_iter()
243 .map(|row| row.agent_id)
244 .collect();
245 select = select.filter(agent::Column::Id.is_in(task_ids));
246 }
247 if let Some(label_id) = query.label_id {
248 let task_ids: Vec<Uuid> = agent_relation_label::Entity::find()
249 .filter(agent_relation_label::Column::LabelId.eq(label_id))
250 .all(self.db().read())
251 .await?
252 .into_iter()
253 .map(|row| row.agent_id)
254 .collect();
255 select = select.filter(agent::Column::Id.is_in(task_ids));
256 }
233257
234258 select = match query.sort.as_deref() {
235259 Some("updated") => select.order_by_desc(agent::Column::UpdatedAt),
Modified lib/sagent/src/openapi.rs View lib/sagent/src/openapi.rs
@@ -19,6 +19,24 @@
1919 crate::handlers::tasks::close_task::close_task,
2020 crate::handlers::tasks::reopen_task::reopen_task,
2121 crate::handlers::messages::list_messages::list_messages,
22 crate::handlers::labels::create_label::create_label,
23 crate::handlers::labels::list_labels::list_labels,
24 crate::handlers::labels::get_label::get_label,
25 crate::handlers::labels::update_label::update_label,
26 crate::handlers::labels::delete_label::delete_label,
27 crate::handlers::labels::list_task_labels::list_task_labels,
28 crate::handlers::labels::bind_label::bind_label,
29 crate::handlers::labels::unbind_label::unbind_label,
30 crate::handlers::members::create_member::create_member,
31 crate::handlers::members::list_members::list_members,
32 crate::handlers::members::delete_member::delete_member,
33 crate::handlers::reads::get_read_state::get_read_state,
34 crate::handlers::reads::update_read_state::update_read_state,
35 crate::handlers::repo_relations::create_repo_relation::create_repo_relation,
36 crate::handlers::repo_relations::list_repo_relations::list_repo_relations,
37 crate::handlers::repo_relations::delete_repo_relation::delete_repo_relation,
38 crate::handlers::artifacts::list_artifacts::list_artifacts,
39 crate::handlers::artifacts::get_artifact::get_artifact,
2240 crate::handlers::events::list_events::list_events,
2341 crate::handlers::tokens::get_task_token::get_task_token,
2442 crate::handlers::watches::upsert_watch::upsert_watch,
@@ -60,6 +78,38 @@
6078 crate::services::watches::UpsertWatchCmd,
6179 crate::services::tokens::TaskTokenDto,
6280 models::sagent::agent_message::AgentRole,
81 AppResponseData<crate::services::labels::AgentLabelDto>,
82 AppResponseData<crate::services::labels::AgentLabelPage>,
83 AppResponseData<Vec<crate::services::labels::AgentLabelDto>>,
84 crate::services::labels::AgentLabelDto,
85 crate::services::labels::AgentLabelPage,
86 crate::services::labels::CreateAgentLabelCmd,
87 crate::services::labels::UpdateAgentLabelCmd,
88 crate::services::labels::ListAgentLabelQuery,
89 crate::services::labels::AgentLabelPath,
90 crate::services::labels::TaskLabelPath,
91 AppResponseData<crate::services::members::AgentMemberDto>,
92 AppResponseData<crate::services::members::AgentMemberPage>,
93 AppResponseData<crate::services::reads::AgentReadStateDto>,
94 AppResponseData<crate::services::repo_relations::AgentRepoRelDto>,
95 AppResponseData<Vec<crate::services::repo_relations::AgentRepoRelDto>>,
96 AppResponseData<crate::services::artifacts::AgentArtifactDto>,
97 AppResponseData<crate::services::artifacts::AgentArtifactPage>,
98 crate::services::members::AgentMemberDto,
99 crate::services::members::AgentMemberPage,
100 crate::services::members::CreateAgentMemberCmd,
101 crate::services::members::ListAgentMemberQuery,
102 crate::services::members::TaskMemberPath,
103 crate::services::reads::AgentReadStateDto,
104 crate::services::reads::UpdateReadStateCmd,
105 crate::services::repo_relations::AgentRepoRelDto,
106 crate::services::repo_relations::CreateAgentRepoRelCmd,
107 crate::services::repo_relations::TaskRepoRelPath,
108 crate::services::artifacts::AgentArtifactDto,
109 crate::services::artifacts::AgentArtifactPage,
110 crate::services::artifacts::ListAgentArtifactQuery,
111 crate::services::artifacts::TaskArtifactPath,
112 models::sagent::agent_member::AgentMemberKind,
63113 models::sagent::agent_event_line::AgentEventType,
64114 models::common::watch_mode::WatchMode,
65115 )),
Modified lib/sagent/src/routes.rs View lib/sagent/src/routes.rs
@@ -1,13 +1,31 @@
11use axum::Router;
2use axum::routing::{get, post, put};
2use axum::routing::{delete, get, post, put};
33
4use crate::handlers::artifacts::get_artifact::get_artifact;
5use crate::handlers::artifacts::list_artifacts::list_artifacts;
46use crate::handlers::collections::create_collection::create_collection;
57use crate::handlers::collections::delete_collection::delete_collection;
68use crate::handlers::collections::get_collection::get_collection;
79use crate::handlers::collections::list_collections::list_collections;
810use crate::handlers::collections::update_collection::update_collection;
911use crate::handlers::events::list_events::list_events;
12use crate::handlers::labels::bind_label::bind_label;
13use crate::handlers::labels::create_label::create_label;
14use crate::handlers::labels::delete_label::delete_label;
15use crate::handlers::labels::get_label::get_label;
16use crate::handlers::labels::list_labels::list_labels;
17use crate::handlers::labels::list_task_labels::list_task_labels;
18use crate::handlers::labels::unbind_label::unbind_label;
19use crate::handlers::labels::update_label::update_label;
20use crate::handlers::members::create_member::create_member;
21use crate::handlers::members::delete_member::delete_member;
22use crate::handlers::members::list_members::list_members;
1023use crate::handlers::messages::list_messages::list_messages;
24use crate::handlers::reads::get_read_state::get_read_state;
25use crate::handlers::reads::update_read_state::update_read_state;
26use crate::handlers::repo_relations::create_repo_relation::create_repo_relation;
27use crate::handlers::repo_relations::delete_repo_relation::delete_repo_relation;
28use crate::handlers::repo_relations::list_repo_relations::list_repo_relations;
1129use crate::handlers::tasks::cancel_task::cancel_task;
1230use crate::handlers::tasks::close_task::close_task;
1331use crate::handlers::tasks::create_task::create_task;
@@ -59,10 +77,54 @@
5977 post(reopen_task),
6078 )
6179 .route(
80 "/agents/{space_name}/collections/{collection_id}/labels",
81 post(create_label).get(list_labels),
82 )
83 .route(
84 "/agents/{space_name}/collections/{collection_id}/labels/{label_id}",
85 get(get_label).patch(update_label).delete(delete_label),
86 )
87 .route(
88 "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/labels",
89 get(list_task_labels),
90 )
91 .route(
92 "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/labels/{label_id}",
93 post(bind_label).delete(unbind_label),
94 )
95 .route(
6296 "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/messages",
6397 get(list_messages),
6498 )
6599 .route(
100 "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/members",
101 post(create_member).get(list_members),
102 )
103 .route(
104 "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/members/{user_id}",
105 delete(delete_member),
106 )
107 .route(
108 "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/read-state",
109 get(get_read_state).put(update_read_state),
110 )
111 .route(
112 "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/repos",
113 post(create_repo_relation).get(list_repo_relations),
114 )
115 .route(
116 "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/repos/{rel_id}",
117 delete(delete_repo_relation),
118 )
119 .route(
120 "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/artifacts",
121 get(list_artifacts),
122 )
123 .route(
124 "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/artifacts/{artifact_id}",
125 get(get_artifact),
126 )
127 .route(
66128 "/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/events",
67129 get(list_events),
68130 )
Modified web/src/shared/api/apiDefinitions.ts View web/src/shared/api/apiDefinitions.ts
@@ -105,16 +105,46 @@
105105 'sagent.get_collection': ['GET', '/agents/{space_name}/collections/{collection_id}'],
106106 'sagent.delete_collection': ['DELETE', '/agents/{space_name}/collections/{collection_id}'],
107107 'sagent.update_collection': ['PATCH', '/agents/{space_name}/collections/{collection_id}'],
108 'sagent.list_labels': ['GET', '/agents/{space_name}/collections/{collection_id}/labels'],
109 'sagent.create_label': ['POST', '/agents/{space_name}/collections/{collection_id}/labels'],
110 'sagent.get_label': ['GET', '/agents/{space_name}/collections/{collection_id}/labels/{label_id}'],
111 'sagent.delete_label': ['DELETE', '/agents/{space_name}/collections/{collection_id}/labels/{label_id}'],
112 'sagent.update_label': ['PATCH', '/agents/{space_name}/collections/{collection_id}/labels/{label_id}'],
108113 'sagent.list_tasks': ['GET', '/agents/{space_name}/collections/{collection_id}/tasks'],
109114 'sagent.create_task': ['POST', '/agents/{space_name}/collections/{collection_id}/tasks'],
110115 'sagent.get_task': ['GET', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}'],
111116 'sagent.delete_task': ['DELETE', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}'],
112117 'sagent.update_task': ['PATCH', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}'],
118 'sagent.list_artifacts': ['GET', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/artifacts'],
119 'sagent.get_artifact': [
120 'GET',
121 '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/artifacts/{artifact_id}'
122 ],
113123 'sagent.cancel_task': ['POST', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/cancel'],
114124 'sagent.close_task': ['POST', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/close'],
115125 'sagent.list_events': ['GET', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/events'],
126 'sagent.list_task_labels': ['GET', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/labels'],
127 'sagent.bind_label': ['POST', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/labels/{label_id}'],
128 'sagent.unbind_label': [
129 'DELETE',
130 '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/labels/{label_id}'
131 ],
132 'sagent.list_members': ['GET', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/members'],
133 'sagent.create_member': ['POST', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/members'],
134 'sagent.delete_member': [
135 'DELETE',
136 '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/members/{user_id}'
137 ],
116138 'sagent.list_messages': ['GET', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/messages'],
139 'sagent.get_read_state': ['GET', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/read-state'],
140 'sagent.update_read_state': ['PUT', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/read-state'],
117141 'sagent.reopen_task': ['POST', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/reopen'],
142 'sagent.list_repo_relations': ['GET', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/repos'],
143 'sagent.create_repo_relation': ['POST', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/repos'],
144 'sagent.delete_repo_relation': [
145 'DELETE',
146 '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/repos/{rel_id}'
147 ],
118148 'sagent.start_task': ['POST', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/start'],
119149 'sagent.get_task_token': ['GET', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/token'],
120150 'sagent.upsert_watch': ['PUT', '/agents/{space_name}/collections/{collection_id}/tasks/{task_id}/watch'],
Modified web/src/shared/api/globals.d.ts View web/src/shared/api/globals.d.ts

Diff too large to display.

Modified web/openapi.json View web/openapi.json

Diff too large to display.

Modified openapi.json View openapi.json

Diff too large to display.