Skip to main content

yggdrasil/mcp/tools/
comments.rs

1//! MCP 写作用域工具:评论审核管理。
2//!
3//! 委托 `src/api/comments` 的 `_impl`,仅鉴权方式不同(bearer token,不走 cookie)。
4//! 状态变更后的评论缓存失效在 `_impl` 内完成,与 web 后台一致。
5//!
6//! 本模块仅 `feature = "server"` 编译。
7
8#![cfg(feature = "server")]
9
10use rmcp::handler::server::tool::Extension;
11use rmcp::handler::server::wrapper::Parameters;
12use rmcp::model::CallToolResult;
13use rmcp::{schemars, tool, tool_router, ErrorData as McpError};
14use serde::Deserialize;
15
16use super::common::{internal, ok_json, require_scope};
17use crate::models::mcp_token::TokenScope;
18
19#[tool_router(router = comments_router, vis = "pub")]
20impl crate::mcp::server::YggMcpServer {
21    /// 列出评论(分页,可按状态筛选)。要求 write 作用域。
22    #[tool(
23        description = "列出全部评论(分页,每页 20 条)。可按状态筛选:pending/approved/spam/trash。"
24    )]
25    async fn list_comments(
26        &self,
27        Parameters(p): Parameters<ListCommentsParams>,
28        Extension(parts): Extension<http::request::Parts>,
29    ) -> Result<CallToolResult, McpError> {
30        let _principal = require_scope(&parts, "list_comments", TokenScope::Write)?;
31
32        let page = p.page.unwrap_or(1).max(1);
33        let resp = crate::api::comments::list_admin_comments_impl(page, p.status.clone())
34            .await
35            .map_err(|e| internal(format!("{e:?}"), "list comments"))?;
36
37        // 不暴露邮箱与头像。
38        let comments: Vec<CommentItem> = resp
39            .comments
40            .into_iter()
41            .map(|c| CommentItem {
42                id: c.id,
43                post_id: c.post_id,
44                post_title: c.post_title,
45                post_slug: c.post_slug,
46                parent_id: c.parent_id,
47                depth: c.depth,
48                author_name: c.author_name,
49                author_url: c.author_url,
50                content_md: c.content_md,
51                status: c.status,
52                created_at: c.created_at.to_rfc3339(),
53            })
54            .collect();
55
56        ok_json(CommentsList {
57            comments,
58            total: resp.total,
59            page,
60            per_page: 20,
61        })
62    }
63
64    /// 通过指定评论(同时递归通过所有 pending 祖先评论)。要求 write 作用域。
65    #[tool(description = "通过指定评论。同时递归通过所有 pending 的祖先评论,确保嵌套链可见。")]
66    async fn approve_comment(
67        &self,
68        Parameters(p): Parameters<CommentIdParams>,
69        Extension(parts): Extension<http::request::Parts>,
70    ) -> Result<CallToolResult, McpError> {
71        let _principal = require_scope(&parts, "approve_comment", TokenScope::Write)?;
72
73        let found = crate::api::comments::approve_comment_impl(p.comment_id)
74            .await
75            .map_err(|e| internal(format!("{e:?}"), "approve comment"))?;
76        if !found {
77            return Err(McpError::invalid_request("评论不存在", None));
78        }
79
80        ok_json(CommentResult {
81            success: true,
82            message: "已通过".into(),
83        })
84    }
85
86    /// 删除指定评论(软删除:设置 deleted_at 与 status=trash)。要求 write 作用域。
87    #[tool(description = "删除指定评论(移入回收站,软删除)。")]
88    async fn delete_comment(
89        &self,
90        Parameters(p): Parameters<CommentIdParams>,
91        Extension(parts): Extension<http::request::Parts>,
92    ) -> Result<CallToolResult, McpError> {
93        let _principal = require_scope(&parts, "delete_comment", TokenScope::Write)?;
94
95        crate::api::comments::trash_comment_impl(p.comment_id)
96            .await
97            .map_err(|e| internal(format!("{e:?}"), "trash comment"))?;
98
99        ok_json(CommentResult {
100            success: true,
101            message: "已删除".into(),
102        })
103    }
104
105    /// 设置评论状态(approved/spam/trash)。要求 write 作用域。
106    #[tool(description = "设置评论审核状态。status 可选 approved/spam/trash。trash 会软删除。")]
107    async fn set_comment_status(
108        &self,
109        Parameters(p): Parameters<SetCommentStatusParams>,
110        Extension(parts): Extension<http::request::Parts>,
111    ) -> Result<CallToolResult, McpError> {
112        let _principal = require_scope(&parts, "set_comment_status", TokenScope::Write)?;
113
114        let normalized = p.status.trim().to_lowercase();
115        if !matches!(normalized.as_str(), "approved" | "spam" | "trash") {
116            return Err(McpError::invalid_request(
117                "status must be one of: approved, spam, trash",
118                None,
119            ));
120        }
121
122        let found = match normalized.as_str() {
123            // 统一为递归通过 pending 祖先,与 approve_comment 工具一致。
124            "approved" => crate::api::comments::approve_comment_impl(p.comment_id).await,
125            "spam" => crate::api::comments::spam_comment_impl(p.comment_id).await,
126            "trash" => crate::api::comments::trash_comment_impl(p.comment_id).await,
127            _ => unreachable!("validated above"),
128        }
129        .map_err(|e| internal(format!("{e:?}"), "set comment status"))?;
130        if !found {
131            return Err(McpError::invalid_request("评论不存在", None));
132        }
133
134        ok_json(CommentResult {
135            success: true,
136            message: format!("状态已设为 {normalized}"),
137        })
138    }
139}
140
141// ---------------------------------------------------------------------------
142// 参数与输出结构
143// ---------------------------------------------------------------------------
144
145#[derive(Debug, Deserialize, schemars::JsonSchema)]
146pub struct ListCommentsParams {
147    /// 页码(从 1 开始,默认 1)。
148    #[serde(default)]
149    pub page: Option<i32>,
150    /// 按状态筛选:pending / approved / spam / trash。不传则返回全部。
151    #[serde(default)]
152    pub status: Option<String>,
153}
154
155#[derive(Debug, Deserialize, schemars::JsonSchema)]
156pub struct CommentIdParams {
157    /// 评论 id。
158    pub comment_id: i64,
159}
160
161#[derive(Debug, Deserialize, schemars::JsonSchema)]
162pub struct SetCommentStatusParams {
163    /// 评论 id。
164    pub comment_id: i64,
165    /// 目标状态:approved / spam / trash。
166    pub status: String,
167}
168
169#[derive(Debug, serde::Serialize)]
170struct CommentItem {
171    id: i64,
172    post_id: i32,
173    post_title: String,
174    post_slug: String,
175    parent_id: Option<i64>,
176    depth: i32,
177    author_name: String,
178    author_url: Option<String>,
179    content_md: String,
180    status: crate::models::comment::CommentStatus,
181    created_at: String,
182}
183
184#[derive(Debug, serde::Serialize)]
185struct CommentsList {
186    comments: Vec<CommentItem>,
187    total: i64,
188    page: i32,
189    per_page: i64,
190}
191
192#[derive(Debug, serde::Serialize)]
193struct CommentResult {
194    success: bool,
195    message: String,
196}