feat(sagent): add labels, members, read state, repo links and artifacts
ZhenYi committed
This change is very large, so some diffs are not shown. Clone the repository to see the full change.
Modified config/default.toml config/default.toml +1 −0 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 lib/config/src/sagent.rs +3 −0 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 lib/sagent/src/handlers/artifacts/get_artifact.rs +40 −0 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 lib/sagent/src/handlers/artifacts/list_artifacts.rs +41 −0 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 lib/sagent/src/handlers/artifacts/mod.rs +2 −0 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 lib/sagent/src/handlers/labels/bind_label.rs +40 −0 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 lib/sagent/src/handlers/labels/create_label.rs +37 −0 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 lib/sagent/src/handlers/labels/delete_label.rs +33 −0 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 lib/sagent/src/handlers/labels/get_label.rs +33 −0 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 lib/sagent/src/handlers/labels/list_labels.rs +35 −0 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 lib/sagent/src/handlers/labels/list_task_labels.rs +34 −0 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 lib/sagent/src/handlers/labels/mod.rs +8 −0 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 lib/sagent/src/handlers/labels/unbind_label.rs +39 −0 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 lib/sagent/src/handlers/labels/update_label.rs +42 −0 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 lib/sagent/src/handlers/members/create_member.rs +44 −0 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 lib/sagent/src/handlers/members/delete_member.rs +39 −0 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 lib/sagent/src/handlers/members/list_members.rs +41 −0 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 lib/sagent/src/handlers/members/mod.rs +3 −0 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 lib/sagent/src/handlers/reads/get_read_state.rs +34 −0 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 lib/sagent/src/handlers/reads/mod.rs +2 −0 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 lib/sagent/src/handlers/reads/update_read_state.rs +43 −0 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 lib/sagent/src/handlers/repo_relations/create_repo_relation.rs +44 −0 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 lib/sagent/src/handlers/repo_relations/delete_repo_relation.rs +39 −0 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 lib/sagent/src/handlers/repo_relations/list_repo_relations.rs +34 −0 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 lib/sagent/src/handlers/repo_relations/mod.rs +3 −0 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 lib/sagent/src/handlers/mod.rs +5 −0 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 lib/sagent/src/services/artifacts.rs +153 −0 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 lib/sagent/src/services/labels.rs +417 −0 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 lib/sagent/src/services/members.rs +265 −0 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 lib/sagent/src/services/mod.rs +5 −0 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 lib/sagent/src/services/reads.rs +102 −0 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 lib/sagent/src/services/repo_relations.rs +190 −0 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 lib/sagent/src/services/service.rs +54 −1 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 lib/sagent/src/services/subscribe.rs +85 −1 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 lib/sagent/src/services/tasks.rs +25 −1 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 lib/sagent/src/openapi.rs +50 −0 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 lib/sagent/src/routes.rs +63 −1 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 web/src/shared/api/apiDefinitions.ts +30 −0 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 web/src/shared/api/globals.d.ts View web/src/shared/api/globals.d.ts
Diff too large to display.
Modified web/openapi.json web/openapi.json View web/openapi.json
Diff too large to display.
Modified openapi.json openapi.json View openapi.json
Diff too large to display.