把 Habitica 接进 MCP,本来以为难点是把工具调用接通。真正耗时间的,是请求看起来成功了,状态却没按预期变。

这次我连续踩了四个 API 坑:任务 ID 大小写混杂、字段名写错后静默无效、周期规则部分更新被旧值“补回来”,还有新建 tag 忘了带 id。它们有个共同点:请求发出去了,不代表数据改对了;很多时候只有回读才能知道。

下面按现场排查顺序复盘。示例只保留必要字段,不是完整 API 请求。

坑一:任务 ID 别先替服务端统一大小写

同一个账号里的任务 ID 并不全是同一种大小写:我实际见到过 BE457DF1 和 08986d5c。第一次写匹配逻辑时,我把输入和返回值都转成小写再比。

结果其中一个任务被正确匹配,另一个却报“任务找不到”。问题不是任务不存在,而是我自作主张把 ID 当成了大小写不敏感的标识符。只要服务端返回值和请求路径按原值区分,归一化就可能把有效 ID 改坏。

修正很简单:把 ID 当不透明字符串,原样保存、原样传递、按原值匹配。 不要为了“整齐”改写它。报找不到时,先分别打印原始 ID、长度和待匹配 ID;排查输出时也要避免把多个字段拼成一个看不清的日志。

1
2
3
# 保留服务端给出的原始 ID,不做 lower()/upper()
task_id = task["id"]
matched = next((item for item in tasks if item["id"] == task_id), None)

如果确实需要做不区分大小写的搜索,那是搜索策略,不是 ID 的存储或写回策略;两者不要混为一谈。

坑二:difficulty 写得再像人话,字段也不会替你猜

我想调整任务难度,凭字段含义写了 difficulty。接口没有给出一个直观的失败信号:批量改了 24 条,调用过程看起来完成,回读后发现一条都没变。

实际字段名是 priority,取值为 0.1、1、1.5、2。也就是说,客户端传了一个自己觉得合理、服务端却不认的字段,结果不是帮忙转换,而是没有产生预期更新。

这个坑最容易让人误以为批处理成功:循环跑完了、请求也发出去了,日志里没有明显异常,只有读回对象才能揭穿它。修复后我把 difficulty 改成 priority,并且不再把“调用成功”当成“字段已更新”。

1
2
3
{
  "priority": 1.5
}

批量更新时,至少要抽查或逐条核验返回对象里的 priority;如果 API 返回的是任务列表,就比较目标 ID 对应的实际值。对重要批量修改,保存修改前的值和目标值,方便回滚与对账。

坑三:repeat 不是“给几个键就补几个键”

周期任务的 repeat 是一周七天的配置,键分别是 su、m、t、w、th、f、s。服务端要求完整的七键结构。

我最初只传要改的部分,或漏掉 th。结果服务端把更新和旧值合并,没提交的键继续保留原配置。于是那条我以为已改成每月一次的 daily 任务,还是照常天天 due。

这里的陷阱不是“请求被拒绝”,而是部分更新造成了混合状态:新配置生效一部分,旧配置又留下另一部分。漏掉的 th 尤其难从只看新提交内容的日志里发现。

构造 repeat 时,明确写满七个键,并且根据目标规则逐一设置 true 或 false,不要依赖服务端把缺项解释成关闭。下面是结构示意,具体启用日要按任务规则填写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
  "repeat": {
    "su": false,
    "m": false,
    "t": false,
    "w": false,
    "th": false,
    "f": false,
    "s": false
  }
}

更新后回读整个 repeat 对象,再检查任务的实际 due 行为。不能只确认“请求里有我想要的那几个日子”,还要确认其余旧值没有残留。

坑四:新建 tag 的 id 不能是空字符串

新建 tag 时,除了名字还得带 id。我第一次传空字符串,服务端直接报:Path id is required。

这次错误反而比前三个清楚:它没有静默失败,而是明确指出路径参数缺失。修复方式是在客户端创建新 tag 时生成 UUID v4,把它作为 id 一起提交;已有 tag 则复用已有 ID,不要每次更新都生成新 ID。

1
2
3
4
import uuid

tag_id = str(uuid.uuid4())
new_tag = {"id": tag_id, "name": "阅读"}

核心是区分“创建”和“引用”:创建时提供合法的新 ID,引用已有 tag 时用已存在的 ID。空字符串不是“稍后由服务端生成”的占位符。

四个坑,其实是同一种排查盲区

现象 真正原因 该看什么
任务找不到 ID 大小写被客户端改写 原始 ID 是否逐字一致
改了 24 条却全没变 字段名应为 priority,不是 difficulty 写后回读目标字段
改成每月仍天天 due repeat 缺键,旧值被合并保留 完整七键和更新后的实际对象
新 tag 创建失败 id 为空 创建请求是否提供 UUID v4

这四件事里,只有 tag 的问题立即报错。前面几个表面上更像“做完了”,实际却没有得到想要的状态。不报错不等于成功,HTTP 请求成功也不等于业务数据正确。

我现在会把回读当成写入的一部分

这次之后,处理类似 API 更新,我会按一个短闭环来做:

  1. 先读现有对象,记录要改的 ID 和相关字段。
  2. 按 API 实际字段名与完整结构构造请求,不猜字段、不省略必需键。
  3. 发出写请求后,重新读取对象,不只看请求是否返回成功。
  4. 把读回值与预期值逐字段比较;不一致就停止后续批量操作,先查明原因。
  5. 批量更新保留前后值,确认抽样或逐项通过后再把任务标记完成。

最值得记住的不是 Habitica 的某个字段名,而是这个习惯:写入是意图,回读才是证据。 API 封装越顺手,越容易把“函数跑完”误当成“数据已改变”。静默失败不比报错温和;它只是把故障发现时间推迟到了你回头核账的时候。