Files
MyAiDesk/nomifun-tauri/crates/backend/nomifun-requirement/src/sink.rs
T
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

92 lines
3.2 KiB
Rust

use std::sync::Arc;
use async_trait::async_trait;
use nomifun_ai_agent::RequirementSink;
use nomifun_api_types::{CreateRequirementRequest, RequirementStatus};
use nomifun_common::RequirementCreator;
use crate::service::RequirementService;
/// Backend implementation of the agent-side `RequirementSink` trait, delegating
/// to `RequirementService`. Injected into the nomi engine via the agent factory.
pub struct RequirementServiceSink {
service: Arc<RequirementService>,
}
impl RequirementServiceSink {
/// Build the sink as a trait object ready to inject into the agent factory.
pub fn into_arc(service: Arc<RequirementService>) -> Arc<dyn RequirementSink> {
Arc::new(Self { service })
}
/// Build the same sink as a [`RequirementCreator`] trait object for the
/// opt-in IM → requirement pipeline (channel inbound → tracked requirement).
pub fn creator_arc(service: Arc<RequirementService>) -> Arc<dyn RequirementCreator> {
Arc::new(Self { service })
}
}
#[async_trait]
impl RequirementCreator for RequirementServiceSink {
async fn create_from_message(
&self,
title: &str,
content: &str,
tag: &str,
created_by: &str,
) -> Result<String, String> {
let req = CreateRequirementRequest {
title: title.to_string(),
content: content.to_string(),
tag: tag.to_string(),
order_key: None,
status: None, // None → Pending → wakes AutoWork
created_by: Some(created_by.to_string()),
attachments: Vec::new(),
};
self.service
.create(req)
.await
.map(|r| r.id.to_string())
.map_err(|e| e.to_string())
}
}
#[async_trait]
impl RequirementSink for RequirementServiceSink {
async fn complete(&self, requirement_id: &str, note: &str) -> Result<(), String> {
let id = parse_req_id(requirement_id)?;
self.service
.complete(id, Some(note.to_string()))
.await
.map(|_| ())
.map_err(|e| e.to_string())
}
async fn update_status(&self, requirement_id: &str, status: &str, note: Option<&str>) -> Result<(), String> {
let id = parse_req_id(requirement_id)?;
let parsed = match status {
"in_progress" => RequirementStatus::InProgress,
"done" => RequirementStatus::Done,
"failed" => RequirementStatus::Failed,
other => return Err(format!("invalid status '{other}'")),
};
self.service
.set_status(id, parsed, note.map(|s| s.to_string()))
.await
.map(|_| ())
.map_err(|e| e.to_string())
}
}
/// Parse the requirement id the nomi engine passes from the prompt (an integer,
/// single-track per spec §2.3) — carried as a string across the agent-engine
/// seam. A non-numeric id (e.g. a stale id replayed from a persisted transcript,
/// spec §2.5/§7.4) is rejected explicitly rather than silently coerced.
fn parse_req_id(requirement_id: &str) -> Result<i64, String> {
requirement_id
.trim()
.parse::<i64>()
.map_err(|_| format!("invalid requirement id '{requirement_id}' (expected an integer)"))
}