fix(mcp): SSE 消息端点立即返回 202,后台异步处理工具调用

poll_now 等耗时工具(发出 HTTP 请求到 Gitea)阻塞了 handle_sse_message,
导致客户端 POST 超时。改为 tokio::spawn 在后台处理 + SSE 推回结果,
HTTP 握手立即完成,符合 MCP SSE 传输规范。

另:新增 docs/ai-context/git-workflow.md(分支策略 + Conventional Commits),
.gitignore 排除炼境实例数据(.claude/liangjing.json / dev_manager.db)。
This commit is contained in:
lanrtop 2026-07-03 19:17:25 +09:00
parent 6d10c71d15
commit d4cb236ace
3 changed files with 109 additions and 20 deletions

4
.gitignore vendored
View File

@ -25,5 +25,9 @@ src-tauri/target/
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# 炼境实例数据(项目 ID 和端口,每台机器不同,不入库)
.claude/liangjing.json
src-tauri/dev_manager.db
# 防止签名密钥被误提交Windows ~ 路径展开问题) # 防止签名密钥被误提交Windows ~ 路径展开问题)
~/ ~/

View File

@ -0,0 +1,82 @@
# Git 工作流 — AI Agent 操作指南
> 把此文件粘贴给任何 AI Agent让它了解本项目的 git 工作方式。由炼境接入包分发。
> `ONBOARDING_MANAGED` 标记块由炼境版本化管理(重跑接入包可原位升级),块外可自由补充项目特有约定。
<!-- ONBOARDING_MANAGED_START version="1.1.0" -->
## 分支策略
```
dev ← 日常开发在这里 commit/push
main ← 稳定分支,通过 PR 合入(合入时触发 CI code review
```
- 小改动可直接 push `dev`
- 功能完整后开 PRdev → main
- 不要直接 push `main`,除非紧急热修复
## Commit 规范lefthook 强制)
格式:`<type>(<scope>): <描述>`
| type | 用途 |
|------|------|
| `feat` | 新功能 |
| `fix` | Bug 修复 |
| `chore` | 配置、依赖、脚本 |
| `docs` | 文档 |
| `refactor` | 重构(不改功能) |
| `ci` | CI 配置 |
| `perf` | 性能优化 |
| `style` | 代码格式 |
| `test` | 测试 |
`scope` 用「模块/子模块」二级结构(如 `auth/login`、`api/user`、`db/order`),对应项目功能模块。
```bash
git commit -m "feat(auth/login): 新增手机号登录"
git commit -m "fix(api/user): 修正分页 off-by-one"
```
> 炼境会解析这些 commit 的 type/scope 作为飞轮数据:`fix` 计入返工率、`scope` 归到模块。
> **commit 写得越规范,飞轮数据越准。**
## Rules-Applied trailer规则命中回传
若本次任务中触发过 CONVENTIONS 自动推荐规则R01-R11`.blueprint/CONVENTIONS.md` 规则列表),
在 commit message 末尾追加一行 trailer与正文空行分隔
```
feat(auth/login): 新增手机号登录
Rules-Applied: R01,R05
```
- 只记录**真实触发**(主动检查或执行)的规则,未触发则整行省略
- 炼境自动解析此 trailer 做「规则命中 × 返工」交叉统计,是 CONVENTIONS 置信度进化的数据来源
- 凭空罗列会污染数据——宁缺毋滥
## 一个 commit = 一件独立的事
不要 `git add -A` 一次提交所有。按功能分批:
```bash
git add <相关文件>
git commit -m "feat(模块): 一件事"
```
## PR 大小控制
PR 别攒太大(建议每 3~5 个 commit 或几百行内就开一次),否则 CI code review 易超时失效。
「所有检测通过」≠ 代码被审查过——大 PR 即使 CI 全绿review 可能已静默失效。
## 常用命令
```bash
git status --short
git log --oneline -10
git reset --soft HEAD~1 # 撤销最后一次 commit保留改动
```
<!-- ONBOARDING_MANAGED_END -->

View File

@ -323,7 +323,7 @@ async fn handle_sse_connect(
} }
/// POST /mcp/group/:group_id/messages?session_id=xxx /// POST /mcp/group/:group_id/messages?session_id=xxx
/// 客户端通过此 URL 发送 JSON-RPC 请求,响应经 SSE channel 推回。 /// 客户端通过此 URL 发送 JSON-RPC 请求,立即返回 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>, Path(group_id): Path<String>,
@ -333,8 +333,12 @@ async fn handle_sse_message(
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();
// 立即返回 202在后台 task 里处理(避免长时间工具调用阻塞 HTTP 连接)
let sessions_clone = sessions.clone();
let session_id = query.session_id.clone();
tokio::spawn(async move {
let id = req.id.clone();
let response_json = match process_rpc(&group_id, &req).await { let response_json = match process_rpc(&group_id, &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!({
@ -342,18 +346,17 @@ async fn handle_sse_message(
"error": { "code": -32601, "message": msg } "error": { "code": -32601, "message": msg }
}), }),
}; };
let mut guard = sessions_clone.lock().await;
let mut sessions_guard = sessions.lock().await; if let Some(tx) = guard.get(&session_id) {
if let Some(tx) = sessions_guard.get(&query.session_id) {
if tx if tx
.send(("message".to_string(), response_json.to_string())) .send(("message".to_string(), response_json.to_string()))
.await .await
.is_err() .is_err()
{ {
// 连接已关闭,惰性清理 guard.remove(&session_id);
sessions_guard.remove(&query.session_id);
} }
} }
});
StatusCode::ACCEPTED.into_response() StatusCode::ACCEPTED.into_response()
} }