返回博客

测试数据想删一条删不掉?怡途Claw 单条消息删除与会话统计 API 实战

2026年8月6日阅读约 36 分钟
测试数据想删一条删不掉?怡途Claw 单条消息删除与会话统计 API 实战

聊了 200 条想把第 137 条测试数据删了别让 AI 学偏?DELETE /api/app/chat/instances/:id/messages/:messageId + GET /stats 一次性把单条硬删、统计四件套讲清,连 system 错误消息、tokenCount 是否真有数、5xx 异常翻译 5 个坑都给到。

删一条测试消息、调一份会话体检:怡途Claw 单条消息删除与会话统计 API 实战

聊到 200 轮的时候突然想:第 137 轮那条手滑发的「/test abc」是不是把 AI 带歪了?想删掉它,但 DELETE /api/app/chat/instances/:id/sessions/:sessionId 一刀下去整段历史全没,没法精准。

或者反过来:你是产品经理,想给老板交一份「这个实例聊了多少轮、花了多少 token」的月报,对着 200 条消息手工数不现实。

这一篇就解决这两件事:精准单条删除 + 单实例会话统计,两条端点、5+4 个常见踩坑,给到能直接 curl 的版本。


一、先看官方列的两个端点到底能干嘛

来源文件:src/app-api/chat/app-chat.controller.ts:425(删除)与 :439(统计)。

端点作用返回
DELETE /api/app/chat/instances/:id/messages/:messageId单条消息硬删{ success, message: '消息已删除' }
GET /api/app/chat/instances/:id/stats单实例会话四件套{ success, data: { totalMessages, userMessages, assistantMessages, totalTokens } }

鉴权两条都走类级 JwtAuthGuard,传 Authorization: Bearer <token> 即可,token 拿法见 307 那篇登录鉴权。

跟 305 那篇会话管理的边界:DELETE /sessions/:sessionId 是清整段会话;DELETE /messages/:messageId 是清一条消息。前者适合「这个分支不想要了」,后者适合「这一句话发错了」。


二、精准删除:DELETE /messages/:messageId

curl 模板:

curl -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  "https://www.yitukeji.cn/api/app/chat/instances/$INSTANCE_ID/messages/$MESSAGE_ID"

返回:

{ "success": true, "message": "消息已删除" }

要拿 MESSAGE_ID 有两条路:

  • GET /messages?sessionId=xxx 列表里挑(305 那篇讲过)
  • 从 SSE stream event: chatdata.message.id 拿(302 讲过)

删完以后,服务端干三件事(src/chat/chat.service.ts:999-1016):

  1. 真删messageRepository.remove(message),是 DELETE 不是软删,DB 行没了。
  2. 清运行时缓存clearRuntimeSessionIdCache(instanceId, userId, sessionId),把这一个会话的运行时 session 缓存键删掉。下次再发消息时这个 session 会被重新建,副作用=已签发的 access_session token 里指向的会话元数据会被刷新。
  3. 广播 message.deleted 信号chatRunEventsService.notifyMessageChange(...) 推一条 { reason: 'message.deleted' } 到当前 SSE stream,客户端长连接能感知到。

坑 1:删 user 消息会让 SSE stream 把这条消息「彻底消失」

实现里把整条 row 删了,不是软删 isDeleted=true。所以列表接口和 stream 都看不到这条了,不是「已撤回」也不是「已隐藏」,是「从数据库里没了」。如果你的 App UI 想做「撤销」按钮并保留 UI 留痕,要自己用 status='deleted' 改模型加软删字段,目前 API 没暴露。

坑 2:删 assistant 消息会破坏配对

前端如果要展示「用户问 / AI 答」配对卡片,删了一条 assistant 后,可能留下一条孤零零的 user 消息没下文。

实现不会自动联动删 parentId 指向它的兄弟,UI 层最好按 (user.parentId, assistant.parentId) 配对渲染。

坑 3:删一条不在你这个实例 / 不是你的用户的消息 → 500 不是 404

实现里 findOne({ id, instanceId, userId }) 找不到时直接 throw new Error('Message not found')src/chat/chat.service.ts:1004),没捕获。NestJS 默认会把裸 Error 翻 500,不是 404

实操要点:

  • 删之前GET /messages?sessionId=xxx 确认这条消息的 instanceId 跟当前实例 ID 一致
  • 跨实例 / 跨用户的消息 ID 传进来,会得到 { statusCode: 500, message: 'Message not found' }
  • 客户端要按 500 兜底,或者提前校验 messageId 是不是当前 session 列表里的合法值

坑 4:删完再发新消息会重建 sessionId 缓存

实现里 clearRuntimeSessionIdCache 把该 session 缓存键删了。下次发消息时,runtime(比如 claude-code、codex)的 session id 会重新建一次。如果你自己客户端有缓存「这条消息对应的 runtime session id」,删完要清掉,否则下次 POST /messages 可能因为 stale cache 出现不期望的会话延续。

坑 5:删 system 错误消息(status='error')也行

Message 实体的 role 字段是 'user' | 'assistant' | 'system'src/chat/message.entity.ts:18),DELETE 端点不挑 role,删 user/assistant/system 都行。

只有在客户端有该 message id 的时候才能删。SSE stream event: chatdata.message.id 对所有 role 都暴露,包括 status='error' 的 system 行,UI 上加个「移除这条错误」按钮是可行的。


三、统计四件套:GET /stats

curl:

curl -H "Authorization: Bearer $TOKEN" \
  "https://www.yitukeji.cn/api/app/chat/instances/$INSTANCE_ID/stats"

返回:

{
  "success": true,
  "data": {
    "totalMessages": 237,
    "userMessages": 119,
    "assistantMessages": 117,
    "totalTokens": 184320
  }
}

四个字段含义(src/chat/chat.service.ts:1053-1069):

字段含义来源
totalMessages全部消息条数(含 system)messages.length
userMessages用户发的条数filter(role === 'user')
assistantMessagesAI 回复的条数filter(role === 'assistant')
totalTokens所有消息的 tokenCount 求和sum(m.tokenCount || 0)

四、stats 的 4 个真实坑

坑 1:totalMessages - userMessages - assistantMessages = system 消息数

注意 stats 没有单独的 systemMessages 字段。要算「出错多少次」只能 totalMessages - userMessages - assistantMessages

system 消息在 src/chat/chat.service.ts:707-727 里只在 LLM 调用失败时插入一条(role: 'system', content: 'Error: ...', status: 'error'),所以这个差值近似等于错误次数

  • 实现是「先 save system 消息再 throw 原 error」(src/chat/chat.service.ts:727-734),所以即使前端拿到 500,DB 里 system 消息已经在了
  • 唯一会破坏这个等式的情况:极少数边界场景下,异常抛在 messageRepository.save(errorMessage) 之前(比如 DB 连接断了),那时候 system 写库失败但实例仍记了一次失败 → stats 差值 < 真实错误次数

坑 2:tokenCount 经常是 0,不要拿来做计费

实现里 tokenCount 来自两个源(src/chat/chat.service.ts:280-283, 667):

  1. assistant 消息从 LLM 响应里 response.tokenCount 直接落库(写库时直接用,没去重没去空)
  2. 其它场景从 result.metadata?.tokenCount 读,没有就 0

真实计费走的是另一条链路src/chat/token-usage-interceptor.service.ts + 301 那篇讲过的 token_usage 表(不是 messages.tokenCount)。所以:

  • GET /stats 拿到的 totalTokens 只是 assistant 消息落库时的标记值,跟账单对不上、跟 301 /api/app/billing/token-usage 也不对得上
  • 月报场景如果要做 token 汇总,用 301 的 /token-usage 端点,不要用这个 stats

实操:把 GET /stats 定位成「聊了多少轮的体检」,token 当参考别当真。

坑 3:stats 是「全量 find」+「JS 内存 filter」,大实例会卡

实现里 getMessageStats 直接 messageRepository.find({ instanceId, userId })src/chat/chat.service.ts:1059),把所有字段全拉进 Node 内存再 filter/reduce。

如果一个实例聊了 1 万条 + 内容很长(content TEXT),一次请求会:

  • 拉 1 万行全字段(含 content 大文本)
  • Node 内存里 filter 2 次 + reduce 1 次
  • 返回的 data 只有 4 个数字,但你为此付的内存和带宽是全量 content

前端按 5-30 秒间隔轮询大实例的 stats,会拖累后端 DB。实操建议:

  • 月报场景用 301 的 /token-usage SQL 聚合,别轮询 stats
  • 如果非用 stats,给它加缓存(@CacheKey + @CacheTTL(60))或前端限制调用频率
  • 自己改实现的话,把 find 换成 count/sum/QueryBuilder,DB 端聚合只回 4 个数字

坑 4:跨 session 不区分

stats 是按 (instanceId, userId) 过滤的,不区分 sessionId。也就是说一个实例下开了 5 个会话,总数是 5 个会话加一起的体检。

要看单 session 统计,目前没有现成端点,要自己从 GET /sessions 拿 sessionId,再 GET /messages?sessionId=xxx 列表然后前端 filter role / 求和 tokenCount。


五、删除 + 统计的组合用法

实操里这两个端点经常一起用:

  1. GET /stats 看总条数,判断要不要清理
  2. GET /messages?sessionId=xxx 拉要删的那条 messageId
  3. DELETE /messages/:messageId 单条删(注意:跨实例/跨用户会返 500,按 500 兜底)
  4. GET /stats 验总数对不对

伪代码:

// 1. 看一眼现在多少条
const stats = await getStats(instanceId);
console.log(`当前 ${stats.totalMessages} 条,${stats.totalTokens} token`);

// 2. 拿到要删的消息 id 列表(自己挑,或者业务规则筛)
const toDelete = ['msg-uuid-1', 'msg-uuid-2'];

// 3. 逐条删
for (const id of toDelete) {
  try {
    await deleteMessage(instanceId, id);
  } catch (e) {
    console.warn(`删除 ${id} 失败:`, e.message);
    // 注意:5xx 也算失败,按 500 兜底
  }
}

// 4. 验
const after = await getStats(instanceId);
console.log(`删后 ${after.totalMessages} 条`);

六、和其它端点的边界

场景用哪个
整段会话都不要了DELETE /sessions/:sessionId(305)
单条消息错了DELETE /messages/:messageId(本篇)
当前消息跑飞了要中止DELETE /runs/:runId(302)
整实例对话全清clearConversation(instanceId)(走 chat.controller 的 legacy 端点,新 app-chat 没单独暴露)
单实例统计GET /stats(本篇)
真实账单 token 汇总GET /billing/token-usage(301)
媒体生图生视频消耗GET /billing/media-usage(310)

小结

单条删除和单实例统计,名字看着像管理后台功能,实际上是 App 端常用高频端点:

  • DELETE /messages/:messageId 适合做「撤销最后一条」「移除错误消息」按钮,但要知道它是真删、5xx 兜底、SSE 推 message.deleted 信号。
  • GET /stats 适合做「实例健康卡片」「聊了多少轮」展示,但别拿 totalTokens 当计费,那是另一条链路。

下一篇会展开 SSE 在大回答下的内存压力点和分片恢复策略的实操细节,跟本篇的 stats 性能坑位是一组。