说说广场 API 0.31.0 版的调用代码说明

前段时间把说说平台从 0.29.x 升到了 Memos 0.31.0,接口几乎全换了一遍。旧代码直接跑不通,最典型的就是列表接口的 parent 参数没了、附件字段从 resources 改名成 attachments。这篇就把升级后实际能用的调用方式逐个拆开讲,示例全部对着线上实例跑过,附接口路径、字段说明、返回结构和错误码,文末还有这次踩到的两个坑——其中一个是防火墙拦的,不是 Memos 的锅。

一、0.31.0 到底变了什么

先说结论:这版不是小修,是把 API 的资源定位方式整个换了一遍。 主要变化有这么几条。

1. 列表接口不再收 parent,改用 CEL filter

旧版查某人的说说,是把作者信息塞在路径里:

0.31.0 把这个参数拿掉了,换成一个 CEL 表达式风格的 filter 查询参数:

这一点很关键——creator 的值是资源名 users/{username},不是数字 ID。这一点和旧版不一样,也是最容易踩的地方。我这边实例升级后实测,users/1 这种数字 ID 已经查不到了,直接返回 user not found。

2. 附件字段更名

Memo.resources 改成了 Memo.attachments,附件的资源名格式和文件路由也跟着变了:

项目旧版0.31.0
字段名resourcesattachments
附件资源名resources/{uid}attachments/{uid}
文件地址/api/v1/resources/{uid} 类/file/attachments/{uid}/{filename}

3. 头像地址移出 API

旧版走 /api/v1/users/{id}/avatar,这版统一挪到了文件路由:

4. 几个版本级别的大改动

除了上面这些实测差异,官方 Release 里还提到几个影响较大的变更,简单列一下:

  • Shortcuts 改名 Saved Views:API 从 users/*/shortcuts/* 变成 users/*/views/*,老客户端必须跟着改
  • 评论可见性独立:CreateMemoComment 不再继承主 memo 的可见性,不传就默认 PRIVATE
  • Reaction 移除 contentId:只能从请求路径或资源名里拿 memo 标识
  • 管理员不再是隐式协作者:写操作全部走作者权限校验,管理员也改不了别人的 memo

二、调用前提:实例必须是公开访问

这一条得单独拎出来讲,因为它是所有后续问题的根源。

Memos 的实例访问策略和它的公开 URL 是两件事。实例权限如果是「公开访问」,匿名请求才能读到 PUBLIC 的 memo;如果是「私有访问」,不登录就是 authentication required。

我这边实测的状态码对照:

场景HTTP返回体
公开实例 + 匿名读 PUBLIC200正常列表
未认证访问写操作401{"code":16,"message":"authentication required"}
带无效 token 读公开 memo200正常返回(token 被忽略)
未认证读用户列表401{"code":16,...}

理解这条链路很重要:请求从浏览器出去,先过反向代理,再过防火墙,最后才到 Memos 服务。中间那层防火墙是有自己规则的,后面第五节会讲它怎么坑了我。

三、接口逐个说明

3.1 列出 memo —— 核心接口

这是用得最多的接口,也是这版改动最狠的地方。

请求

参数类型说明
filterstringCEL 表达式,筛选条件
pageSizeint每页条数,不传默认 50,非法值也回落成 50
pageTokenstring上一页返回的 nextPageToken

实测 pageSize 的行为:pageSize=0 返回 50 条,pageSize=1000 也能正常给 1000 条,没有上限保护,所以别拿它当无脑拉全量的工具——真有需要就循环 pageToken。

返回

几个字段值得单独说:

  • name 是 memo 的资源名,格式 memos/{uid},后面查详情、改内容全靠它
  • creator 是资源名 users/{username},不是数字 ID
  • property 是这版新加的预解析字段,前端要显示「这条有没有链接 / 有没有待办 / 有没有代码块」,不用再自己解析一遍 Markdown,直接读它就行
  • snippet 是纯文本摘要,适合做列表预览

3.2 filter 能用什么,不能用什么

这是最容易踩坑的地方。filter 不是随便写都能过的,我把这版实测能用的表达式列一下。

可以正常用的:

不能用的(会直接报错):

写法结果
create_time > "..."400 undeclared reference to 'create_time'
createTime > "..."400 undeclared reference to 'createTime'
state == "NORMAL"400 undeclared reference to 'state'
has(tags)400 invalid argument to has() macro
tags == tags400 field "tags" does not support comparison

注意几个字段名和返回体里的驼峰不一样:返回体是 createTime,但筛选表达式里根本没有时间字段可筛。state、createTime、visibility 之外的字段基本都不可筛。

还有个坑:tag 字段的 exists 宏在某些环境下会被防火墙拦掉(第五节细说),所以标签过滤要换种写法。

3.3 分页:nextPageToken

这版没有 offset 页码,只用游标。逻辑很简单:拿上一页返回的 nextPageToken 当请求参数传回去。

两个注意点:

  1. nextPageToken 必须原样回传,不要解码也不要拼路径。它是 Base64 变体,里面带 ==,直接 encodeURIComponent 就行
  2. nextPageToken 是空字符串表示到底了,不是 null 也不是缺字段。判断结束条件要用 !pageToken

实测这个游标是稳定的,连着翻两页拿到的 name 不重复、不跳条。

3.4 查单条 memo

返回结构和列表里的一条完全一致。查不到的时候:

HTTP 状态是 404,注意这版错误响应体统一带 code 字段,而且错误的时候 HTTP 状态码和 body 里的 code 是两套编号,别混:

情况HTTPbody.codebody.message
资源不存在4045memo not found
路由不存在4045Not Found
用户不存在4045user not found
filter 语法错4003invalid filter: ...
未认证40116authentication required

这版是 gRPC 风格的错误码(5 = NOT_FOUND、3 = INVALID_ARGUMENT、16 = UNAUTHENTICATED),不是 HTTP 码直接搬过来。

3.5 用户信息与头像

两个和旧版不一样的地方:

  • 用户名精确匹配,大小写敏感。实测 GET /api/v1/users/Penn 直接返回 404 user not found,只有 users/penn 能命中,这版用户名不再做大小写归一化了
  • avatarUrl 是相对路径 /file/users/penn/avatar,拼接时别忘了加站点域名,而且要写成 /file/... 而不是 /api/v1/...

另外,用户名走 users/{username},数字 ID 这版已经失效——GET /api/v1/users/1 返回 404。老代码里存的用户 ID 全得换成用户名。

想批量拿头像信息的话,GET /api/v1/users 这个用户列表接口是需要认证的,匿名会返回 401。

3.6 memo 的子资源

挂在单条 memo 下的子资源,都用 memo 的资源名做父级:

子资源路径返回
附件GET /api/v1/memos/{uid}/attachments{"attachments":,"nextPageToken":""}
关联GET /api/v1/memos/{uid}/relations{"relations":}
表情回应GET /api/v1/memos/{uid}/reactions{"reactions":,"nextPageToken":""}
评论GET /api/v1/memos/{uid}/comments{"memos":,"nextPageToken":""}

注意评论接口的返回字段叫 memos 而不是 comments——评论本身也是 memo,接口是复用的。想取附件文件本体,地址是 /file/attachments/{uid}/{filename},别再用旧的 resources 路径。

四、页面嵌入代码:完整调用结构

前面讲的是接口本身,这节是把它套到页面上的实际代码。我这边说说广场用的就是这套,跑在 Hexo 页面的

里。

4.1 完整结构

加载顺序有硬约束:配置对象 memo 必须在 main.js 之前定义(main.js 顶层直接读 memo.host,配置没到位会抛 ReferenceError),marked.umd.min.js 也要在前面,否则正文 Markdown 渲染不出来。custom.js 和 custom.css 都是空文件占位(实测 custom.js 返回 0 字节),留给你放自己的覆盖逻辑。

4.2 配置项逐个说明

这里有个必须说的事:配置里十个字段,main.js 实际只读了三个。

字段是否被 main.js 使用说明
host✅Memos 实例地址,末尾 / 会被自动去掉(replace(/\/+$/, ''))
limit✅每页条数,会过 parseInt,NaN 或 < 1 回落成 10
creatorId✅Memos 用户名,拼成 users/{creatorId} 塞进 filter
domId❌容器选择器没被读,实际写死在代码里
username❌未被读取
name❌未被读取
language❌未被读取
total❌未被读取
doubanAPI❌未被读取

我 grep 过 main.js 全文(158 行),里面出现的配置引用只有 memo.host、memo.limit、memo.creatorId 三处。所以后面那六个字段目前是声明了但不生效——留着无害(将来版本可能用上),但别指望改它们能改变行为。

limit 的默认值行为要留意:limit: '1' 是字符串,会被 parseInt 成数字 1。传 '0'、'abc' 或负数都会回落到 10,不报错。

4.3 main.js 里的请求是怎么拼的

核心就一个 buildListUrl(),把配置和筛选条件拼成最终 URL:

分页逻辑是预取下一页:loadFirst() 拿到第一页后,如果 nextPageToken 非空就立刻调 preloadNext() 把第二页拉进 nextDom,用户点「加载更多」时直接渲染已缓存的 nextDom,感知不到延迟。终止条件有两个:返回条数少于 limit,或者 nextPageToken 为空串。

4.4 ⚠️ 这套代码当前有三个问题

写在这里不是要拆台,是这几个坑排查时踩过,别人复用大概率也会撞上。这三个都出在 CDN 上的 penndu@21.0.0,本地仓库没有源码。

一、标签过滤必然 403。 就是第五节坑一那个问题——buildListUrl() 里用的是 tags.exists(t, t == "..."),而 exists( 会被防火墙拦。改成 "' + tagName + '" in tags 即可,两种写法实测返回条数一致。

二、main.js 在 CDN 上是截断的。 实测该文件只有 5912 字节、158 行,且最后一行停在 } else { 后面:

文件到这儿就没了——renderMemos() 函数没有定义完。浏览器解析到这儿会抛 SyntaxError: Unexpected end of input,整个 main.js 都不会执行,页面容器

会一直是空的。我抓了线上说说页的 HTML 确认:容器确实是 空标签, 也还是初始值 0,说明渲染没跑起来。

三、memoDom 永远是 null。 全文只有 var memoDom = null; 这一处赋值,之后标签点击时却直接用 memoDom.innerHTML = '':

domId 配置项本来应该用来给 memoDom 赋值,但因为 main.js 被截断在 renderMemos() 内部,那段赋值代码没被上传,所以点标签会抛 TypeError。这三个问题里,第二个是根因——补齐 main.js 后,后两个大概率一并解决。

所以现在这份代码贴出来是「存档 + 说明」性质的,真要用建议先核对这几点。

五、两个实打实的坑

前面讲的接口细节都是「怎么用」,这节讲「哪些地方会莫名其妙失败」。

坑一:防火墙把 exists( 拦了

这个坑排查了挺久,起因是我这边标签过滤点了没反应,控制台报错才注意到——返回的根本不是 JSON,是 1Panel 的拦截页:

HTTP 状态 403。我把规则缩小到了很具体的一步:

filter 内容结果
tags.exists(t)403 被拦
exists(t)403 被拦
foo.exists(t)403 被拦
tags.exists400 正常报错(没到 WAF)
"读书" in tags200 正常
size(tags) > 0200 正常
content.contains("简报")200 正常

规律很清楚:只要 exists 后面紧跟左括号 exists(,就被拦。 不带括号的 tags.exists 能过(虽然它本身语法也不对,会返回 400)。而且这层防护拦的是请求参数,跟路径无关——我试过 ?x=exists(t) 一样 403。

所以标签过滤要改成 in 写法:

两种写法实测返回条数一致,语义也等价。如果你自己也是套了防护面板,自查一下有没有把 CEL 表达式里的关键字误伤。 好消息是 in 写法本身语法更短,也不挑环境,比 exists 更值得当默认写法。

坑二:has_link 筛不出「没有链接」的 memo

这个不算 bug,算个语义陷阱。

基线里明明全是 hasLink=false,筛 false 反而一条都不出。翻这 5 条命中的记录,property.hasLink 字段确实都是 true,内容也确实带链接。所以这个字段是能筛的,但 false 这个取值的行为跟直觉对不上,用之前先拿小样本验一下,别直接拿去做生产筛选。

顺带一个:页面大小没有上限

pageSize=1000 实测真的返回 1000 条,没有服务端截断。这个在自用场景无害,但如果你的调用方不可信,一个 pageSize=100000 就能把内存打满。自己在前面兜一层就行。

六、代码错误怎么兜

这一节把前面的错误码整理成可直接用的处理逻辑。核心原则:先判 HTTP 状态,再判 body 里的 code。

通用错误处理

上面 !contentType.includes('application/json') 那个判断是实战里加的——不加的话,await res.json() 会直接抛一个语法错误异常,信息全丢了,根本看不出是被防火墙拦的。

按状态码分支

关于 filter 语法错(400)

filter 报 400 时,错误消息里会带 CEL 编译器的原始报错,能直接看出错在哪。实测两种典型错法:

注意看这个报错——它把整个 filter 表达式原样回显在消息里。如果你的 filter 里带了敏感信息,注意一下。

七、完整可用示例

把前面几节串起来,一个能直接跑的完整抓取脚本:

跑一下就知道条数对不对:

分页也顺手验一下,游标翻到底不重不漏:

八、迁移检查清单

从旧版升过来,按这个顺序过一遍基本不会漏:

  1. parent 全部换成 filter,作者写成 users/{username},别用数字 ID
  2. resources 全部换成 attachments,文件地址换成 /file/attachments/{uid}/{filename}
  3. 头像地址换成 /file/users/{username}/avatar
  4. 筛选表达式里的 exists( 换成 "标签" in tags,尤其自己套了防护面板的
  5. 用户名改成精确匹配,别再依赖大小写不敏感
  6. shortcuts 相关调用换成 views
  7. 评论请求显式指定可见性,别依赖继承
  8. 错误处理同时判 HTTP 状态和 body.code

九、写在最后

0.31.0 这版改动集中在一个点上:把所有「定位资源」的方式统一到资源名 + CEL filter 上了。 users/penn、memos/g48Yif2Bd66WvDfuFkeSwe、attachments/{uid} 这类资源名成了唯一入口,字段名也统一成了驼峰。

代价是迁移必须跟着改,好处是筛选能力上了一个台阶——过去只能拉全量再前端过滤,现在可以直接在服务端按作者、标签、可见性、正文内容筛。property 字段的预解析也省掉了前端一层 Markdown 解析。

前面第二节那个 pageSize 没上限、第五节那两个坑,都是实测撞出来的,不是文档里写的。0.31.0 的 API 文档还在跟着版本走,示例里 parent 这类老写法也能搜到不少,所以别完全照抄官方示例——跑一遍才知道合不合你的部署。

本文所有接口路径、字段名、返回结构、错误码和请求结果,都是对着线上实例逐个实测的(curl 直接打 s.dusays.com)。示例代码里的筛选表达式也都在真实环境下验证过可用性。

第四节那段嵌入代码是我自己说说广场在用的,贴出来时也顺手核了一遍——结果发现三个问题,其中 main.js 在 CDN 上是被截断的(5912 字节,renderMemos() 写了一半),这大概也解释了为什么线上说说页现在是空的。补齐这个文件之后,标签过滤和 memoDom 相关的两个问题应该能一起解决。


说说平台搬到了 Memos,接口细节和踩坑记录都写在这了,欢迎留言交流。
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论