feat(mcp): 单一全局 lian-jing 服务器——组级挂载收敛(mcp-single-server T1-T3/T5/T6) #3
@ -420,9 +420,9 @@ modules:
|
|||||||
name: MCP 单一服务器化
|
name: MCP 单一服务器化
|
||||||
area: backend
|
area: backend
|
||||||
status: in_progress
|
status: in_progress
|
||||||
progress: 0
|
progress: 85
|
||||||
updated: 2026-07-06
|
updated: 2026-07-06
|
||||||
note: "组级挂载 lian-jing-{group_id} 收敛为单一全局 lian-jing:授权/注册一次,项目数增长零客户端配置;liangjing.json 只需 project_id。已拆卡 T1-T6(双路由过渡方案),T6 删旧路由后置一个版本"
|
note: "T1/T2/T3/T5/T6 代码完成(cargo test 93 绿 + typecheck 过 + grep 零残留),T6 经评估提前并入本次发版。剩 T4:构建新二进制 + 重启触发自动迁移 + 端到端验证"
|
||||||
position: [1750, 500]
|
position: [1750, 500]
|
||||||
|
|
||||||
edges:
|
edges:
|
||||||
|
|||||||
@ -61,43 +61,48 @@
|
|||||||
|
|
||||||
## 任务卡
|
## 任务卡
|
||||||
|
|
||||||
### 🔵 T1. 服务器层双路由:全局 /mcp + 组上下文 Option 化
|
### ✅ T1. 服务器层双路由:全局 /mcp + 组上下文 Option 化
|
||||||
- status: in_progress
|
- status: done
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- files: src-tauri/src/mcp_server.rs
|
- files: src-tauri/src/mcp_server.rs
|
||||||
- acceptance: "[HTTP] POST http://127.0.0.1:{port}/mcp (initialize) → 200 且 serverInfo.name=炼境,GET /mcp 返回 SSE 事件流(endpoint 指向 /mcp/messages);旧路由 /mcp/group/{gid} 同请求仍 200(双路由并存)"
|
- acceptance: "[HTTP] POST http://127.0.0.1:{port}/mcp (initialize) → 200 且 serverInfo.name=炼境,GET /mcp 返回 SSE 事件流(endpoint 指向 /mcp/messages)"
|
||||||
|
- done_note: 2026-07-06 完成。process_rpc 驻军测试覆盖握手路径(cargo test 全绿);HTTP 层实测归入 T4(需新二进制运行)。注:T6 随后并入同一发版,双路由过渡态只存在于中间 commit
|
||||||
|
|
||||||
### 🔵 T2. 工具层去组化:resolve_project_root 按 project_id 直查
|
### ✅ T2. 工具层去组化:resolve_project_root 按 project_id 直查
|
||||||
- status: in_progress
|
- status: done
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- files: src-tauri/src/mcp_tools.rs
|
- files: src-tauri/src/mcp_tools.rs
|
||||||
- depends: T1
|
- depends: T1
|
||||||
- acceptance: "[HTTP] 经 /mcp 调 get_project_blueprint(project_id) → 返回 manifest 内容,无'不属于产品组'错误;list_group_projects(group_id=xxx) 显式传参 → 返回成员列表,无参数且无挂载上下文 → 报错提示传参;cargo test 全绿"
|
- acceptance: "[HTTP] 经 /mcp 调 get_project_blueprint(project_id) → 返回 manifest 内容,无'不属于产品组'错误;list_group_projects(group_id=xxx) 显式传参 → 返回成员列表;cargo test 全绿"
|
||||||
|
- done_note: 2026-07-06 完成。resolve_project_root 直查 project_workspaces;3 处组归属校验替换为 ensure_project_exists;list_group_projects schema 改 required group_id。HTTP 实测归入 T4
|
||||||
|
|
||||||
### 🔵 T3. 注入层收敛:lian-jing 单 key + 旧配置清理 + liangjing.json 瘦身
|
### ✅ T3. 注入层收敛:lian-jing 单 key + 旧配置清理 + liangjing.json 瘦身
|
||||||
- status: in_progress
|
- status: done
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- files: src-tauri/src/mcp_inject.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/commands/groups.rs
|
- files: src-tauri/src/mcp_inject.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/commands/groups.rs, src-tauri/src/commands/projects.rs, src-tauri/src/lib.rs
|
||||||
- depends: T1
|
- depends: T1
|
||||||
- acceptance: "[Tauri command] inject_project_mcp(pid) → 项目 settings.json 含 key \"lian-jing\"(URL 以 /mcp 结尾)且无 lian-jing:group-* 残留;liangjing.json 含 project_id+mcp_port 且 group_id 键被删除;try_register_globally 注册单个 lian-jing 并对旧名(完整 UUID + 短 8 位两种格式)执行 claude mcp remove;remove_group_member 仅在项目不属于任何组时移除 key;cargo test 全绿"
|
- acceptance: "[Tauri command] inject_project_mcp(pid) → 项目 settings.json 含 key \"lian-jing\"(URL 以 /mcp 结尾)且无 lian-jing:group-* 残留;liangjing.json 含 project_id+mcp_port 且 group_id 键被删除;remove_group_member 仅在项目不属于任何组时移除 key;cargo test 全绿"
|
||||||
|
- done_note: 2026-07-06 完成。额外实现 migrate_to_single_server 启动时一次性迁移(标记位 mcp_single_migrated,claude CLI 缺失时不落标记等重试);delete_group/remove_group_member 加"仍属其他组则保留"判断;文件实测归入 T4
|
||||||
|
|
||||||
### 📋 T4. 存量批量迁移执行 + 端到端验证
|
### 📋 T4. 存量批量迁移执行 + 端到端验证
|
||||||
- status: todo
|
- status: todo
|
||||||
- complexity: S
|
- complexity: S
|
||||||
- files: (执行操作,无新代码——复用 reinject_all_mcp)
|
- files: (执行操作,无新代码——迁移在新版启动时自动执行)
|
||||||
- depends: T1, T2, T3(需先发版/重启炼境运行新二进制)
|
- depends: T1, T2, T3, T6(需先构建 + 重启炼境运行新二进制)
|
||||||
- acceptance: "[CLI] claude mcp list → lian-jing-* 旧条目清零、仅剩 lian-jing;[MCP] 抽查 3 个项目(含本项目 + enterprise-system)settings.json/liangjing.json 符合新格式;新 Claude 会话经 lian-jing 实调 get_diagnostics → 返回 JSON 无 error"
|
- acceptance: "[CLI] claude mcp list → lian-jing-* 旧条目清零、仅剩 lian-jing;[MCP] 抽查 3 个项目(含本项目 + enterprise-system)settings.json/liangjing.json 符合新格式;新 Claude 会话经 lian-jing 实调 get_diagnostics → 返回 JSON 无 error;[HTTP] POST /mcp/group/{gid} → 404"
|
||||||
|
|
||||||
### 🔵 T5. 文档/模板/技能/UI 寻址说明同步
|
### ✅ T5. 文档/模板/技能/UI 寻址说明同步
|
||||||
- status: in_progress
|
- status: done
|
||||||
- complexity: S
|
- complexity: S
|
||||||
- files: docs/ai-context/git-workflow.md, src-tauri/resources/onboarding/git-workflow.md.tmpl, .claude/skills/retro-catchup/SKILL.md, src/components/settings/SettingsPage.tsx, .blueprint/modules/mcp-config-inject.md
|
- files: docs/ai-context/git-workflow.md, src-tauri/resources/onboarding/git-workflow.md.tmpl, .claude/skills/retro-catchup/SKILL.md, src/components/settings/SettingsPage.tsx, .blueprint/modules/mcp-config-inject.md
|
||||||
- depends: T3
|
- depends: T3
|
||||||
- acceptance: "[Bash] grep -rE 'lian-jing-\\{group_id\\}|lian-jing:group-' docs/ src/ src-tauri/resources/ → 零命中(mcp-config-inject.md 保留历史注记除外);pnpm typecheck 通过;另提醒用户同步仓库外 ~/.claude/CLAUDE.md 炼境规则段"
|
- acceptance: "[Bash] grep -rE 'lian-jing-\\{group_id\\}|lian-jing:group-' docs/ src/ src-tauri/resources/ → 零命中(mcp-config-inject.md 保留历史注记除外);pnpm typecheck 通过"
|
||||||
|
- done_note: 2026-07-06 完成。grep 零命中、typecheck 通过;用户全局 ~/.claude/CLAUDE.md 炼境规则段已同步改为单一 lian-jing 寻址
|
||||||
|
|
||||||
### 📋 T6. 后置:删除旧 /mcp/group 路由与兼容层(下个版本)
|
### ✅ T6. 删除旧 /mcp/group 路由与兼容层
|
||||||
- status: todo
|
- status: done
|
||||||
- complexity: S
|
- complexity: S
|
||||||
- files: src-tauri/src/mcp_server.rs, src-tauri/src/mcp_tools.rs
|
- files: src-tauri/src/mcp_server.rs, src-tauri/src/mcp_tools.rs
|
||||||
- depends: T4(迁移验证通过后至少隔一个发版周期)
|
- depends: T3
|
||||||
- acceptance: "[HTTP] POST /mcp/group/{gid} → 404;cargo test 全绿;grep 'mcp/group' src-tauri/src/ 零命中"
|
- acceptance: "cargo test 全绿;grep 'mcp/group' src-tauri/src/ 零命中;[HTTP] POST /mcp/group/{gid} → 404(归入 T4 实测)"
|
||||||
|
- done_note: 2026-07-06 完成。原计划后置一个版本,经评估提前并入本次发版:迁移在新版启动时自动重写全部客户端配置,旧路由的实际消费者只剩升级前的存量会话(服务重启后本就断开),单人环境保留价值趋近于零。tools_call 签名去 Option 化,list_group_projects 改 required 参数
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{
|
response::{
|
||||||
sse::{Event, KeepAlive, Sse},
|
sse::{Event, KeepAlive, Sse},
|
||||||
@ -42,13 +42,10 @@ pub async fn start(port: u16) {
|
|||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/health", get(health))
|
.route("/health", get(health))
|
||||||
// 全局单一服务器路由(lian-jing):寻址不再依赖 group_id
|
// 单一全局服务器路由(lian-jing):寻址不依赖 group_id,工具显式传 project_id
|
||||||
.route("/mcp", post(handle_mcp_global).get(handle_sse_connect_global))
|
.route("/mcp", post(handle_mcp).get(handle_sse_connect))
|
||||||
.route("/mcp/messages", post(handle_sse_message_global))
|
|
||||||
// 组级路由 — 过渡期向后兼容(存量 lian-jing-{gid} 注册仍可用),T6 移除
|
|
||||||
.route("/mcp/group/:group_id", post(handle_mcp).get(handle_sse_connect))
|
|
||||||
// SSE 消息端点:客户端通过此 URL 发送 JSON-RPC,响应经 SSE 推回
|
// SSE 消息端点:客户端通过此 URL 发送 JSON-RPC,响应经 SSE 推回
|
||||||
.route("/mcp/group/:group_id/messages", post(handle_sse_message))
|
.route("/mcp/messages", post(handle_sse_message))
|
||||||
// Gitea webhook:接收 push/pull_request 事件写入飞轮
|
// Gitea webhook:接收 push/pull_request 事件写入飞轮
|
||||||
.route("/webhook/gitea/:project_id", post(crate::mcp_webhook::handle_gitea_webhook))
|
.route("/webhook/gitea/:project_id", post(crate::mcp_webhook::handle_gitea_webhook))
|
||||||
.with_state(sessions);
|
.with_state(sessions);
|
||||||
@ -88,8 +85,7 @@ struct SessionQuery {
|
|||||||
|
|
||||||
// ── 共享 RPC 处理逻辑 ─────────────────────────────────────────────────────────
|
// ── 共享 RPC 处理逻辑 ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// group_id 为 None 表示经全局 /mcp 路由进入(单一服务器寻址,工具按 project_id 定位)
|
async fn process_rpc(req: &RpcRequest) -> Result<Value, String> {
|
||||||
async fn process_rpc(group_id: Option<&str>, req: &RpcRequest) -> Result<Value, String> {
|
|
||||||
match req.method.as_str() {
|
match req.method.as_str() {
|
||||||
"initialize" => Ok(json!({
|
"initialize" => Ok(json!({
|
||||||
"protocolVersion": "2024-11-05",
|
"protocolVersion": "2024-11-05",
|
||||||
@ -97,27 +93,19 @@ async fn process_rpc(group_id: Option<&str>, req: &RpcRequest) -> Result<Value,
|
|||||||
"serverInfo": { "name": "炼境", "version": env!("CARGO_PKG_VERSION") }
|
"serverInfo": { "name": "炼境", "version": env!("CARGO_PKG_VERSION") }
|
||||||
})),
|
})),
|
||||||
"tools/list" => Ok(crate::mcp_tools::tools_list_result()),
|
"tools/list" => Ok(crate::mcp_tools::tools_list_result()),
|
||||||
"tools/call" => Ok(crate::mcp_tools::tools_call(group_id, req.params.as_ref()).await),
|
"tools/call" => Ok(crate::mcp_tools::tools_call(req.params.as_ref()).await),
|
||||||
_ => Err(format!("Method not found: {}", req.method)),
|
_ => Err(format!("Method not found: {}", req.method)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Streamable HTTP(POST)─────────────────────────────────────────────────────
|
// ── Streamable HTTP(POST)─────────────────────────────────────────────────────
|
||||||
|
|
||||||
async fn handle_mcp(Path(group_id): Path<String>, Json(req): Json<RpcRequest>) -> Response {
|
async fn handle_mcp(Json(req): Json<RpcRequest>) -> Response {
|
||||||
mcp_response(Some(group_id), req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_mcp_global(Json(req): Json<RpcRequest>) -> Response {
|
|
||||||
mcp_response(None, req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn mcp_response(group_id: Option<String>, req: RpcRequest) -> Response {
|
|
||||||
if req.id.is_none() {
|
if req.id.is_none() {
|
||||||
return StatusCode::ACCEPTED.into_response();
|
return StatusCode::ACCEPTED.into_response();
|
||||||
}
|
}
|
||||||
let id = req.id.clone();
|
let id = req.id.clone();
|
||||||
match process_rpc(group_id.as_deref(), &req).await {
|
match process_rpc(&req).await {
|
||||||
Ok(result) => Json(json!({ "jsonrpc": "2.0", "id": id, "result": result })).into_response(),
|
Ok(result) => Json(json!({ "jsonrpc": "2.0", "id": id, "result": result })).into_response(),
|
||||||
Err(msg) => Json(json!({
|
Err(msg) => Json(json!({
|
||||||
"jsonrpc": "2.0", "id": id,
|
"jsonrpc": "2.0", "id": id,
|
||||||
@ -129,39 +117,18 @@ async fn mcp_response(group_id: Option<String>, req: RpcRequest) -> Response {
|
|||||||
|
|
||||||
// ── SSE 传输(GET + POST /messages)─────────────────────────────────────────
|
// ── SSE 传输(GET + POST /messages)─────────────────────────────────────────
|
||||||
|
|
||||||
/// GET /mcp/group/:group_id — Claude Code SSE 握手,首条事件告知后续 POST URL
|
/// GET /mcp — Claude Code SSE 握手,首条事件告知后续 POST URL
|
||||||
async fn handle_sse_connect(
|
async fn handle_sse_connect(
|
||||||
State(sessions): State<Sessions>,
|
State(sessions): State<Sessions>,
|
||||||
Path(group_id): Path<String>,
|
|
||||||
) -> Sse<impl futures_core::Stream<Item = Result<Event, Infallible>>> {
|
|
||||||
sse_connect(sessions, Some(group_id)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// GET /mcp — 全局路由 SSE 握手
|
|
||||||
async fn handle_sse_connect_global(
|
|
||||||
State(sessions): State<Sessions>,
|
|
||||||
) -> Sse<impl futures_core::Stream<Item = Result<Event, Infallible>>> {
|
|
||||||
sse_connect(sessions, None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn sse_connect(
|
|
||||||
sessions: Sessions,
|
|
||||||
group_id: Option<String>,
|
|
||||||
) -> Sse<impl futures_core::Stream<Item = Result<Event, Infallible>>> {
|
) -> Sse<impl futures_core::Stream<Item = Result<Event, Infallible>>> {
|
||||||
let session_id = uuid::Uuid::new_v4().to_string();
|
let session_id = uuid::Uuid::new_v4().to_string();
|
||||||
let (tx, rx) = mpsc::channel::<(String, String)>(32);
|
let (tx, rx) = mpsc::channel::<(String, String)>(32);
|
||||||
|
|
||||||
let port = read_port();
|
let port = read_port();
|
||||||
let endpoint_uri = match &group_id {
|
let endpoint_uri = format!(
|
||||||
Some(gid) => format!(
|
"http://127.0.0.1:{}/mcp/messages?session_id={}",
|
||||||
"http://127.0.0.1:{}/mcp/group/{}/messages?session_id={}",
|
port, session_id
|
||||||
port, gid, session_id
|
);
|
||||||
),
|
|
||||||
None => format!(
|
|
||||||
"http://127.0.0.1:{}/mcp/messages?session_id={}",
|
|
||||||
port, session_id
|
|
||||||
),
|
|
||||||
};
|
|
||||||
let _ = tx.send(("endpoint".to_string(), endpoint_uri)).await;
|
let _ = tx.send(("endpoint".to_string(), endpoint_uri)).await;
|
||||||
sessions.lock().await.insert(session_id, tx);
|
sessions.lock().await.insert(session_id, tx);
|
||||||
|
|
||||||
@ -184,50 +151,24 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 全局路由(group_id=None)下 initialize / tools/list 必须可用——单一 lian-jing 服务器的握手路径
|
/// 单一 lian-jing 服务器的握手路径:initialize / tools/list 必须可用
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn process_rpc_works_without_group_context() {
|
async fn process_rpc_handshake() {
|
||||||
let init = process_rpc(None, &rpc("initialize")).await.unwrap();
|
let init = process_rpc(&rpc("initialize")).await.unwrap();
|
||||||
assert_eq!(init["serverInfo"]["name"], "炼境");
|
assert_eq!(init["serverInfo"]["name"], "炼境");
|
||||||
|
|
||||||
let tools = process_rpc(None, &rpc("tools/list")).await.unwrap();
|
let tools = process_rpc(&rpc("tools/list")).await.unwrap();
|
||||||
let list = tools["tools"].as_array().unwrap();
|
let list = tools["tools"].as_array().unwrap();
|
||||||
assert!(list.len() >= 20, "工具清单不应因去组化而缺失,实际 {}", list.len());
|
assert!(list.len() >= 20, "工具清单不应因去组化而缺失,实际 {}", list.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 过渡期组级路由(Some(gid))仍可握手
|
|
||||||
#[tokio::test]
|
|
||||||
async fn process_rpc_keeps_legacy_group_context() {
|
|
||||||
let init = process_rpc(Some("any-group"), &rpc("initialize")).await.unwrap();
|
|
||||||
assert_eq!(init["serverInfo"]["name"], "炼境");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// POST /mcp/group/:group_id/messages?session_id=xxx
|
/// POST /mcp/messages?session_id=xxx
|
||||||
|
/// 立即返回 202,后台异步处理工具调用并经 SSE channel 推回响应
|
||||||
async fn handle_sse_message(
|
async fn handle_sse_message(
|
||||||
State(sessions): State<Sessions>,
|
State(sessions): State<Sessions>,
|
||||||
Path(group_id): Path<String>,
|
|
||||||
Query(query): Query<SessionQuery>,
|
Query(query): Query<SessionQuery>,
|
||||||
Json(req): Json<RpcRequest>,
|
Json(req): Json<RpcRequest>,
|
||||||
) -> Response {
|
|
||||||
sse_message(sessions, Some(group_id), query, req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// POST /mcp/messages?session_id=xxx — 全局路由消息端点
|
|
||||||
async fn handle_sse_message_global(
|
|
||||||
State(sessions): State<Sessions>,
|
|
||||||
Query(query): Query<SessionQuery>,
|
|
||||||
Json(req): Json<RpcRequest>,
|
|
||||||
) -> Response {
|
|
||||||
sse_message(sessions, None, query, req).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 立即返回 202,后台异步处理工具调用并经 SSE channel 推回响应
|
|
||||||
async fn sse_message(
|
|
||||||
sessions: Sessions,
|
|
||||||
group_id: Option<String>,
|
|
||||||
query: SessionQuery,
|
|
||||||
req: RpcRequest,
|
|
||||||
) -> Response {
|
) -> Response {
|
||||||
if req.id.is_none() {
|
if req.id.is_none() {
|
||||||
return StatusCode::ACCEPTED.into_response();
|
return StatusCode::ACCEPTED.into_response();
|
||||||
@ -236,7 +177,7 @@ async fn sse_message(
|
|||||||
let session_id = query.session_id.clone();
|
let session_id = query.session_id.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let id = req.id.clone();
|
let id = req.id.clone();
|
||||||
let response_json = match process_rpc(group_id.as_deref(), &req).await {
|
let response_json = match process_rpc(&req).await {
|
||||||
Ok(result) => json!({ "jsonrpc": "2.0", "id": id, "result": result }),
|
Ok(result) => json!({ "jsonrpc": "2.0", "id": id, "result": result }),
|
||||||
Err(msg) => json!({
|
Err(msg) => json!({
|
||||||
"jsonrpc": "2.0", "id": id,
|
"jsonrpc": "2.0", "id": id,
|
||||||
|
|||||||
@ -14,13 +14,13 @@ pub fn tools_list_result() -> Value {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "list_group_projects",
|
"name": "list_group_projects",
|
||||||
"description": "列出产品组内所有成员项目,包含名称、路径、平台、技术栈、角色等信息。经全局 lian-jing 服务器调用时必须传 group_id(可从 list_all_projects 获取)",
|
"description": "列出产品组内所有成员项目,包含名称、路径、平台、技术栈、角色等信息(group_id 可从 list_all_projects 获取)",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"group_id": { "type": "string", "description": "产品组 ID;经组级路由调用时可省略(取挂载组)" }
|
"group_id": { "type": "string", "description": "产品组 ID" }
|
||||||
},
|
},
|
||||||
"required": []
|
"required": ["group_id"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -306,8 +306,7 @@ pub fn tools_list_result() -> Value {
|
|||||||
|
|
||||||
// ── 工具分发 ──────────────────────────────────────────────────────────────────
|
// ── 工具分发 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// group_ctx 为 None 表示经全局 /mcp 路由调用(单一服务器);Some(gid) 为过渡期组级路由
|
pub async fn tools_call(params: Option<&Value>) -> Value {
|
||||||
pub async fn tools_call(group_ctx: Option<&str>, params: Option<&Value>) -> Value {
|
|
||||||
let tool_name = params
|
let tool_name = params
|
||||||
.and_then(|p| p.get("name"))
|
.and_then(|p| p.get("name"))
|
||||||
.and_then(|n| n.as_str())
|
.and_then(|n| n.as_str())
|
||||||
@ -317,21 +316,17 @@ pub async fn tools_call(group_ctx: Option<&str>, params: Option<&Value>) -> Valu
|
|||||||
.and_then(|p| p.get("arguments"))
|
.and_then(|p| p.get("arguments"))
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or(json!({}));
|
.unwrap_or(json!({}));
|
||||||
let gid: Option<String> = group_ctx.filter(|g| !g.is_empty()).map(str::to_string);
|
|
||||||
|
|
||||||
let outcome = tokio::task::spawn_blocking(move || match tool_name.as_str() {
|
let outcome = tokio::task::spawn_blocking(move || match tool_name.as_str() {
|
||||||
"get_diagnostics" => get_diagnostics(),
|
"get_diagnostics" => get_diagnostics(),
|
||||||
"list_group_projects" => {
|
"list_group_projects" => {
|
||||||
// 显式参数优先;无参数时回退到挂载组上下文(过渡期组级路由)
|
let group_id = args
|
||||||
let effective = args
|
|
||||||
.get("group_id")
|
.get("group_id")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.filter(|s| !s.is_empty())
|
.filter(|s| !s.is_empty());
|
||||||
.map(str::to_string)
|
match group_id {
|
||||||
.or(gid);
|
Some(g) => list_group_projects(g),
|
||||||
match effective {
|
None => Err("需显式传 group_id 参数(组 ID 可从 list_all_projects 获取)".to_string()),
|
||||||
Some(g) => list_group_projects(&g),
|
|
||||||
None => Err("经全局 lian-jing 服务器调用时需显式传 group_id 参数(组 ID 可从 list_all_projects 获取)".to_string()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"get_project_blueprint" => {
|
"get_project_blueprint" => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user