Tinode 0.25.3 常用 CLI 命令详解

Tinode 0.25.3 常用 CLI 命令详解:tinode 启动参数、tn-cli 全命令集与宏、REST 长轮询 curl 调试矩阵与 WebSocket 帧级调试,全部实测回显逐行沉淀。

TinodeTinode即时通讯2026/10/26

Tinode 0.25.3 常用 CLI 命令详解

目标:把 Tinode v0.25.3 日常运维会用到的四类命令入口一次讲透:服务端 tinode 二进制的启动参数、官方 Python 命令行客户端 tn-cli 的完整命令集(会话、话题、账号、文件四组命令加 8 个宏)、不装客户端时用 curl 驱动 REST/长轮询通道的调试矩阵,以及用裸 WebSocket 做的帧级调试。重点沉淀实测边界:409 must attach first、useradd 缺 --cred 的 NoneType 异常、单行 .await 不被识别、file 命令 gRPC 桩缺失、大文件下载 307 双拼路径、长轮询缺 Content-Type 引发的 409 command out of sequence。本文所有命令与回显均来自真实服务器实测,与安装篇、配置篇同一环境。

一、环境信息

本文的命令实验在以下环境完成(与《Ubuntu 22.04 安装 Tinode 0.25.3 指南》同一台服务器,版本自证取自 tn-cli 连接横幅与服务器 -help 输出):

项目实测值
主机名tinode-sys
操作系统Ubuntu 22.04.5 LTS
服务端版本Tinode v0.25.3(-config 读取失败时的启动日志显示 Server v0.25:/opt/tinode-app/tinode:v0.25.3)
数据库MySQL 8.0.46(适配器 build: mysql:v0.25.3)
HTTP 端点6060(REST/长轮询/WebSocket/Web 控制台共用)
gRPC 端点16060(tn-cli 专用)
tn-cli 版本Version 3.1.0/0.22.3; gRPC/1.85.0; Python 3.10.12(启动横幅实测)
apikeyAQAAAAABAAAoeOI7tA3HsYvdzDhYhZJy(安装篇 keygen 生成,C1 演示同值)
实验账号管理员 admin(uid usr2N8sq7g8bwU,安装篇 init-db 创建);本文实验另注册 alice/bobby/grace/harry/wanda 及多个演示群组

除特别注明「在服务器上」执行数据库查询外,其余命令均在服务器本机的 shell 中执行(localhost 直连)。

二、命令工具总览

2.1 四个命令入口

v0.25.3 的日常操作分散在四个入口里,传输通道与能力边界各不相同:

入口形态连接端点定位
tinode服务端二进制监听 6060/16060唯一服务进程,所有配置经它生效(参数详见第三章)
tn-cliPython 客户端(官方 release 附件)gRPC localhost:16060运维管理客户端:账号/话题/消息/文件命令 + .await 等宏,支持脚本化
curl + REST/LPHTTP JSON/v0/channels/lp?apikey=...不装客户端时的接口级调试,逐包发收(详见第十章)
裸 WebSocketHTTP 升级帧/v0/channels?apikey=...帧级调试,验证协议包顺序(详见第十一章)

四个入口收发的业务包(hi/login/sub/pub/get/set/del/note)是同一套,差别只在封装:gRPC 走二进制桩,REST/长轮询与 WebSocket 走 JSON 文本。选型篇介绍的「三传输端点」落到命令层就是这张表。

2.2 tn-cli 与 REST 的分工

  • tn-cli 有而 REST 没有的:宏命令(useradd/userdel/usermod/userpasswd/resolve)——这类管理操作是 tn-cli 在客户端把一个命令展开成多个协议包完成的;.await/.must/.sleep 等脚本控制宏也只在 tn-cli 里存在。
  • REST 有而 tn-cli 弱的:大文件上传下载(/v0/file/u/ 与 /v0/file/s/),以及一切需要拿原始 JSON 回显做抓包分析的场景。
  • 两者共享服务器能力:hi 横幅里下发的参数(maxFileUploadSize: 8388608、msgDelAge: 600 等)在 tn-cli 连接串与 REST hi 回执里完全一致(可对照第 5.1 与 10.2 节)。

💡 技术拓展:Tinode 把协议设计成「包 + 通道」两层。同一个 {pub:{...}} 包,走 gRPC 时是 protobuf 消息、走 REST 时是 HTTP POST 的 JSON body、走 WebSocket 时是一帧文本,服务端处理逻辑只有一份(配置篇 grpc_listen、api_path 两个键分别控制 gRPC 与 HTTP 的入口)。因此本文第三章、第九~十一章的实测结论可以互相印证:409 must attach first 这类错误码在三条通道上语义相同。

三、服务端 tinode 启动参数详解

3.1 -help 实测全量输出

在服务器上执行 ./tinode -help(安装目录 /opt/tinode-app),回显共 12 个参数、25 行:

Usage of ./tinode:
  -api_path string
    	Override the base URL path where API is served.
  -cluster_self string
    	Override the name of the current cluster node.
  -config string
    	Path to config file. (default "tinode.conf")
  -expvar string
    	Override the URL path where runtime stats are exposed. Use '-' to disable.
  -grpc_listen string
    	Override address and port to listen on for gRPC clients.
  -listen string
    	Override address and port to listen on for HTTP(S) clients.
  -log_flags string
    	Comma-separated list of log flags (as defined in https://golang.org/pkg/log/#pkg-constants without the L prefix) (default "stdFlags")
  -pprof string
    	File name to save profiling info to. Disabled if not set.
  -pprof_url string
    	Debugging only! URL path for exposing profiling info. Disabled if not set.
  -server_status string
    	Override the URL path where the server's internal status is displayed. Use '-' to disable.
  -static_data string
    	File path to directory with static files to be served. (default "static")
  -tls_enabled
    	Override config value for enabling TLS.

3.2 参数逐项说明

参数作用与 tinode.conf 的关系
-config配置文件路径唯一不是「覆盖」的参数,指定 conf 本身;默认值 tinode.conf(相对当前目录)
-listen覆盖 HTTP(S) 监听地址端口对应 listen 键(conf 中 ":6060")
-grpc_listen覆盖 gRPC 监听地址端口对应 grpc_listen 键(conf 中 ":16060")
-api_path覆盖 API 基路径对应 api_path 键(默认 /v0/)
-expvar覆盖运行时指标路径,传 - 禁用对应 expvar 键(默认 /debug/vars,用法见 10.6)
-server_status覆盖服务器内部状态页路径,传 - 禁用对应 server_status 键
-static_data静态文件目录对应 static_data 键(默认 static,Web 控制台托管见安装篇)
-tls_enabled覆盖 TLS 开关对应 tls_enabled 键
-log_flags日志标志位对应 log_flags 键(默认 stdFlags)
-cluster_self覆盖当前集群节点名对应 cluster 配置中的节点名;单机部署无用
-pprof把性能剖析信息写入指定文件conf 无同名键;未设置即禁用
-pprof_url性能剖析 HTTP 路径conf 无同名键;官方注释明确「Debugging only!」

要点:命令行参数都是 conf 同名键的覆盖器(-pprof/-pprof_url 除外)。优先级为「命令行 > conf > 内置默认」。日常运维保持 tinode -config tinode.conf 一个参数即可——本文实测服务器上 systemd 托管的进程就是这一形态:

$ ps -ef | grep [t]inode
root        6307       1  0 13:06 ?        00:00:00 /opt/tinode-app/tinode -config /opt/tinode-app/tinode.conf

3.3 边界实测:-config 指向不存在的文件

$ ./tinode -config /tmp/nope.conf
I2026/10/26 13:45:54 Server v0.25:/opt/tinode-app/tinode:v0.25.3; pid 6401; 6 process(es)
I2026/10/26 13:45:54 Using config from '/tmp/nope.conf'
E2026/10/26 13:45:54 Failed to read config file: open /tmp/nope.conf: no such file or directory

版本横幅先打印,接着回显 Using config from '...',然后以 E 级错误行退出、进程不监听任何端口。这与配置篇的结论一致:配置错误一律表现为启动失败并打 E 级日志,不会静默回退默认配置。

四、tn-cli 客户端:获取、安装与连接

4.1 tn-cli 是什么与获取方式

tn-cli 是官方仓库 tn-cli/ 目录随 release 发布的 Python 客户端,v0.25.3 对应附件 tn-cli.tar.gz(约 18 KB)。解压后 10 个文件(tar tzf tn-cli.tar.gz 实测):

./
./sample-script.txt
./requirements.txt
./client.py
./tn_globals.py
./tn-cli.py
./utils.py
./sample-macro-script.txt
./macros.py
./input_handler.py
./commands.py

依赖清单 requirements.txt 实测内容:

futures>=3.2.0; python_version<'3'
grpcio>=1.40.0
Pillow>=5.4.1
requests>=2.21.0
tinode-grpc>=0.22.0
prompt_toolkit>=2.0.10
importlib-metadata>=1.0; python_version<'3.8'

服务器上有 Python 3.10 即可安装:pip3 install -r requirements.txt。注意它并不随发行包内置,需要单独下载附件(本文装在服务器本机,与 gRPC 端点同机)。

4.2 启动与登录横幅

以管理员身份启动(--host 显式指定 gRPC 端点、--no-cookie 不落登录 cookie、--login-basic 直接 basic 登录):

$ cd tn-cli
$ python3 tn-cli.py --host localhost:16060 --no-cookie --login-basic admin:q1w2e3r4
Tinode command line client. Version 3.1.0/0.22.3; gRPC/1.85.0; Python 3.10.12.
Server at 'localhost:16060' 
Logging in with login:password admin:q1w2e3r4
<= <= Connected to server: maxTagCount: 16; servingAt: http://:6060/; maxSubscriberCount: 128; reqCred: None; build: mysql:v0.25.3; maxFileUploadSize: 8388608; maxMessageSize: 131072; minTagLength: 2; maxTagLength: 96; msgDelAge: 600; clusterSize: 1; ver: 0.25 
<= <= 201 created 
<= Authenticated as usr2N8sq7g8bwU 
<= <= 200 ok 

四行关键回显的语义:

  1. 版本横幅:tn-cli 3.1.0,内嵌 gRPC 包 tinode-grpc 0.22.3 + grpcio 1.85.0,Python 3.10.12。
  2. Connected to server: ...:服务端 hi 回执的参数串,与 REST 通道 hi 返回的 JSON 完全同源(ver: 0.25、build: mysql:v0.25.3、reqCred: None——本环境未启用注册凭据强制校验,见配置篇)。
  3. 201 created:会话建立(gRPC 通道会话创建与登录合并,login 包成功前先回会话确认)。
  4. Authenticated as usr2N8sq7g8bwU + 200 ok:登录成功,uid 即安装篇 init-db 创建的 admin。

若不带 --login-basic 启动,tn-cli 会在横幅后报凭据缺失并退出,不进入交互界面;若 --host 不写,默认即 localhost:16060。

tn-cli 默认把登录 token 存到当前目录的 .tn-cli-cookie(实测为一个 165 字节文本文件,内容是 base64 的 token 与有效期),下次启动免登录。多脚本并行跑同一账号时建议统一加 --no-cookie,避免 cookie 互相覆盖——本文所有实测均带 --no-cookie。

4.4 命令总表与在线帮助

在交互界面敲一个不存在的名字(例如把启动参数误敲进来),tn-cli 会打印完整命令总表——这张表本身就是最好的帮助页(实测原文):

tn> Unrecognized: --login-basic
Possible commands:
	.await		- wait for completion of an operation
	.delmark	- custom delete marker to use instead of default DEL!
	.exit		- exit the program (also .quit)
	.log		- write value of a variable to stdout
	.must		- wait for completion of an operation, terminate on failure
	.sleep		- pause execution
	.use		- set default user (on_behalf_of) or topic
	.verbose	- toggle logging verbosity on/off
	acc		- create or alter an account
	del		- delete message(s), topic, subscription, or user
	file		- download or upload a large file
	get		- query topic for metadata or messages
	leave		- detach or unsubscribe from topic
	login		- authenticate current session
	note		- send a notification
	pub		- post message to topic
	set		- update topic metadata
	sub		- subscribe to topic
	upload		- upload file out of band over HTTP(S)
	usermod		- modify user account
	Type <command> -h for help
Macro commands:
	chacs		- Change default permissions/acs for a user (requires root privileges)
	chcred		- Add/delete/validate credentials (requires root privileges)
	passwd		- Set user's password (requires root privileges)
	resolve		- Resolve login and print the corresponding user id
	thecard		- Print theCard for a user (requires root privileges)
	useradd		- Create a new user account
	userdel		- Delete user account (requires root privileges)
	usermod		- Modify user account (requires root privileges)

每个命令都支持 <command> -h 查用法。Macro commands 段里 useradd/resolve 未标注 root 权限要求(useradd 本质是把 acc 组包),而 userdel/usermod/chacs/chcred/passwd/thecard 明确标注 requires root privileges——需要 ROOT 登录态(配置篇 6.1 的 :admin ROOT 账号场景)。

五、会话与登录命令(login / get)

5.1 login 登录

交互界面内的登录命令用法(login -h 实测 usage):

usage: login
       [-h]
       [--scheme SCHEME]
       [--secret SECRET]
       [--uname UNAME]
       [--password PASSWORD]
       [--cred CRED]
       [--as_root]
       [secret]

实测登录(进入空会话后):

tn> login --uname admin --password q1w2e3r4
<= <= Connected to server: clusterSize: 1; maxMessageSize: 131072; servingAt: http://:6060/; minTagLength: 2; maxTagLength: 96; maxFileUploadSize: 8388608; maxTagCount: 16; reqCred: None; maxSubscriberCount: 128; msgDelAge: 600; ver: 0.25; build: mysql:v0.25.3 
<= <= 201 created 
<= Authenticated as usr2N8sq7g8bwU 
<= <= 200 ok 

回显顺序:hi 能力串 → 201 created(会话)→ Authenticated as ... → 200 ok(登录成功)。

5.2 边界:同一会话重复登录

同一 tn-cli 会话内再执行一次 login(实测回显):

tn> login --uname admin --password q1w2e3r4
<= <= 409 already authenticated 

一个会话只允许认证一次;要换账号就退出重开(.exit 后重新启动 tn-cli),不要在已登录会话里重复 login。

5.3 get 查询个人资料

get 是读命令,get me --desc 拉自己的资料卡:

tn> get me --desc
<= <= meta desc me 

tn-cli 把资料卡渲染成缩进块打印(受终端宽度影响);要看原始 JSON,用第九章的 .verbose 打开原始包回显即可。get -h 的完整 usage(实测):

usage: get
       [-h]
       [--desc]
       [--sub]
       [--data]
       [--del]
       [--cred]
       [--tags]
       [--user USER]
       [topic]

六、话题操作命令(sub / pub / get / del)

6.1 sub:创建群组并订阅

sub --topic new 是「建群」语义,可同时带 --fn 公开名称、--tags 检索标签、--get-query 让本次 sub 顺带回元数据(实测,命令与回显均真实):

tn> sub --topic new --fn 项目讨论组 --tags project-x,demo --get-query sub
<= <= 200 ok (grpFUJdHPLTPCI) 
<= <= meta sub grpFUJdHPLTPCI 

服务器分配的群组 topic 名就是括号里的 grpFUJdHPLTPCI,后续所有操作都用它。注意 tn-cli 的 sub 对新话题同时完成 attach(挂接会话)与订阅,之后可直接 pub/get。

6.2 边界:不 attach 就操作 → 409/403

在新会话里对已有群组不先 sub 直接删消息、读消息(实测回显,del 与 get 的回执按序各占一行):

tn> del msg --topic grpFUJdHPLTPCI --seq 1 --hard
tn> get grpFUJdHPLTPCI --data
<= <= 409 must attach first (grpFUJdHPLTPCI) 
<= <= 403 permission denied (grpFUJdHPLTPCI) 

规律:写操作(del,pub 同理)被拒为 409 must attach first,读操作(get data)被拒为 403 permission denied——错误码不同但根因相同:会话与话题之间没有 attach 关系。这是 Tinode 实测中出现频率最高的错误码,处置永远是先 sub --topic <群组名>。

6.3 pub:发送消息与引号边界

attach 之后发送消息,--head mime:text/plain 声明正文类型,--content 是消息体(实测):

tn> .await pub --topic grpNQlnZQDtAYg --head mime:text/plain --content "tn-cli await chain test"
<= <= 200 ok (grpNQlnZQDtAYg) 

attach(sub)与 pub 的成功回执在 tn-cli 中显示形式相同,都是 200 ok (话题名),按出现顺序区分即可。两个实测边界都出在 --content 不加引号时:

输入实测回显原因
pub --content tn-cli await chain test(不带引号)pub: error: unrecognized arguments: testshlex 按空格切词,多出来的词落不进任何参数
pub --topic new --content tn-cli group message(不带引号)409 must attach first (group)第一个多余的词 group 被同名位置参数 [topic] 吃掉,覆盖了 --topic 的值,消息发去了不存在的话题 group

第二条尤其隐蔽:不报语法错误,而是把消息发到一个错误的话题名上。多词消息体必须整体加引号。

6.4 get:读取消息

attach 后拉取消息流(实测,--head 值按 tn-cli 原样打印为字节字面量):

tn> .await get grpNQlnZQDtAYg --data
From: usr2N8sq7g8bwU 
<= Topic: grpNQlnZQDtAYg 
<= Seq: 1 
<= Headers: 
<= 	mime: b'"text/plain"' 
<= tn-cli await chain test 
<= <= 208 delivered (grpNQlnZQDtAYg) 

Seq: 1 是该话题内消息序号(服务端按话题单调递增,REST 通道回执的 params.seq 与此一致),208 delivered 是 tn-cli 自动发送的已读回执确认。

6.5 del:删除消息

硬删除(--hard 对所有人删除)第 1 条消息(实测):

tn> .await del msg --topic grpNQlnZQDtAYg --seq 1 --hard
<= <= 200 ok (grpNQlnZQDtAYg) 

del 的取值边界也在实测里(客户端侧直接拒绝,不发包):

输入实测回显说明
--seq 1200 ok单条删除,--seq N 即第 N 条
--seq "1-1"Invalid message ID range 1-1区间必须低<高,1-1 不是合法区间;单条就用 --seq N

del 还支持 --topic 下的 del topic(删群)、del sub(退订他人)等形态,用法同理查 del -h。

七、账号管理命令(acc / useradd / resolve / set)

7.1 acc:创建账号

acc --user new 注册新账号(匿名会话或已登录会话均可执行;实测):

tn> acc --user new --uname grace --password q1w2e3r4 --fn "Grace Hopper" --cred email:grace@example.com
<= <= 201 created 

--cred email:... 提交邮箱凭据;本环境 reqCred: None(未强制验证),账号即建即用。同法注册了 harry(--fn "Harry Potter"),实测同样 201 created。

7.2 useradd 宏:-P 位置参数边界

useradd 是 tn-cli 客户端宏,第一个参数是位置参数 login,密码用 -P:

tn> useradd --uname wanda --password q1w2e3r4 --fn "Wanda Maximoff"
       [-h]
       [-P PASSWORD]
       [--cred CRED]
       [--name NAME]
       [--comment COMMENT]
       [--note NOTE]
       [--trusted TRUSTED]
       [--tags TAGS]
       [--avatar AVATAR]
       [--auth AUTH]
       [--anon ANON]
       [--explain]
       [--as_root]
       login
useradd: error: unrecognized arguments: --uname --fn Wanda Maximoff

要点:useradd 没有 --uname/--password/--fn 这几个参数——用户名是位置参数,密码是 -P,显示名是 --name。正确形态:

useradd <login> -P <password> --name "<显示名>" --cred email:<邮箱>

7.3 边界:缺 --cred 的 NoneType 异常

省略 --cred 执行 useradd wanda -P q1w2e3r4 --name "Wanda Maximoff"(实测):

tn> useradd wanda -P q1w2e3r4 --name "Wanda Maximoff"
<= <= 201 created 
<= Authenticated as usr2N8sq7g8bwU 
<= <= 200 ok 
<= Exception in generator: 'NoneType' object has no attribute 'synchronous' 

macro 源码在缺 --cred 时只打印一句提示并返回 None,主循环随后对该空结果取 .synchronous 属性,于是抛出 NoneType 异常。此时账号并没有被创建——服务器上直接查库确认(实测):

$ mysql tinode -e 'SELECT id,uname FROM auth WHERE uname="basic:wanda"'
(空结果,无 basic:wanda 行)

遇到 Exception in generator: 'NoneType' ...,第一反应应检查是否漏了 --cred。

7.4 完整创建 wanda 并三路验证

补上 --cred 后完整执行(实测):

tn> useradd wanda -P q1w2e3r4 --cred "email:wanda@example.com" --name "Wanda Maximoff"
<= <= 201 created 
tn> resolve wanda
<= usrpC22fQJIPU4 

三路验证互为闭环:

  1. 数据库(服务器上查询 auth 表,实测):SELECT id,uname,userid,authlvl FROM tinode.auth WHERE uname="basic:wanda" → 6 | basic:wanda | 1588833251621867520 | 20;
  2. tn-cli resolve:usrpC22fQJIPU4(与库中 userid 同一账号);
  3. REST/长轮询登录:用 basic scheme、base64 的 wanda:q1w2e3r4 发 login,实测回执 {"ctrl":{"id":"2","code":200,"text":"ok","params":{"user":"usrpC22fQJIPU4","authlvl":"ImF1dGg=",...}}}——uid 与 resolve 输出一致,账号可登录。

7.5 resolve 与 set 的边界

  • resolve 的登录名同样是位置参数,resolve --login wanda 会报 resolve: error: unrecognized arguments: --login(实测);正确写法 resolve wanda。当账号不存在时,resolve 的 fnd 查询返回空数组,宏里 .log $temp.sub[0].topic 取不到下标,回显 Error in '.log $temp.sub[0].topic': sub——这行 Error 就是「账号不存在」的信号。
  • set me 的实测边界:set me --fn "Admin Operator" 与 set me --public '{...}' 均返回 304 not modified 且数据库 users.public 维持 {"fn": "Administrator"} 不变(实测),而 set me --private "operator console" 返回 200 ok (me) 且库中 me 话题订阅行的 private 字段更新为 "operator console"。结论:tn-cli 0.22.3 的 set me --fn/--public 在 gRPC 通道上不生效(服务器按「无变化」应答 304),要改公开资料请走 REST/WS 通道的 {set desc}(安装篇 8.3 有实测 200 闭环);--private 正常可用。

八、文件命令(file / upload)

8.1 file:gRPC 通道上传(实测不可用)

file 命令设计上走 gRPC 大文件通道,实测报桩缺失(加 .verbose 抓到原始包):

tn> .await file --what up /tmp/d1-upload.txt
<= Failed to upload '/tmp/d1-upload.txt': 'NodeStub' object has no attribute 'LargeFileReceive' 
<= Timeout while waiting for 'file' response 

'NodeStub' object has no attribute 'LargeFileReceive' 是 Python 端 tinode-grpc 桩与官方 tn-cli 3.1.0 代码不匹配导致的:tn-cli 调用 LargeFileReceive 流式 RPC,而安装的桩里没有该方法,客户端在发包前就失败,随后 .await 等不到回执超时。结论:本版本组合下 file 上传不可用,文件传输用 upload。 另一处用法边界(实测):file up /tmp/d1-upload.txt(把 up 当位置参数)会打印 usage 并报 file: error: unrecognized arguments: /tmp/d1-upload.txt——方向必须写成 --what up/--what down。

8.2 upload:HTTP 通道上传(可用)

upload 走 HTTP 大文件端点(与第十章 curl 用的同一个 /v0/file/u/),需要 --web-host 指定 HTTP 地址(实测):

$ python3 tn-cli.py --host localhost:16060 --no-cookie --login-basic admin:q1w2e3r4 --web-host localhost:6060
...
tn> .await upload /tmp/d1-upload.txt
<= <= 200 ok 

服务器侧核对(实测):上传目录 /opt/tinode-app/uploads/ 同步新增一个 17 字节的对象文件(与被上传的 /tmp/d1-upload.txt 字节数一致),专名即下载 url 末段。下载侧与 REST 完全一致(返回的 params.url 拼到 http://localhost:6060 后 GET,见 10.5)。

九、宏命令与脚本化执行

9.1 宏总表

宏作用实测回显特征
.await <命令>等待该命令完成再继续无独立回显,命令回执按序出现
.must <命令>同 .await,失败即终止脚本失败时进程退出
.sleep <毫秒>暂停脚本Pausing for 1200ms...
.use --topic <t>设置默认话题Default topic='grpNQlnZQDtAYg'
.verbose开关原始包回显出现 <= => 发包行与 JSON 回包行
.log <变量>打印变量成功打印值 / Error in '.log ...': ...
.delmark自定义删除标记(本文未实测)
.exit退出(.quit 同义)Shutting down...

用户管理宏(useradd/userdel/usermod/userpasswd/resolve)在客户端展开成协议包,用法见第七章。

9.2 .await 同行衔接全链

.await 必须与被等待的命令写在同一行,脚本化才能保证顺序(实测三连):

tn> .await sub --topic grpNQlnZQDtAYg
<= <= 200 ok (grpNQlnZQDtAYg) 
tn> .await pub --topic grpNQlnZQDtAYg --head mime:text/plain --content "tn-cli await chain test"
<= <= 200 ok (grpNQlnZQDtAYg) 
tn> .await get grpNQlnZQDtAYg --data
<= <= 208 delivered (grpNQlnZQDtAYg) 
(随后打印消息内容块,见 6.4)

attach → pub → get 按序完成。把 .await 拆成单独一行喂进去则是另一个结局(见 9.3)。

9.3 边界:单行 .await 不被识别

tn> .await
Unrecognized: .await
Possible commands:
(打印 9.4 所示完整命令表)

源码层面:tn-cli 的输入解析只在 .await/.must 后面跟着命令时才进入等待分支,单独一行的 .await 解析不出命令,落到兜底分支按「不识别的名字」处理。因此**.await 永远不能独占一行**,管道喂脚本时尤其容易踩(脚本里想换行分步,实际得到一串 Unrecognized 与乱序回执)。

9.4 .use 与 .sleep

tn> .await sub --topic grpNQlnZQDtAYg
<= <= 200 ok (grpNQlnZQDtAYg) 
tn> .use --topic grpNQlnZQDtAYg
<= Default topic='grpNQlnZQDtAYg' 
tn> .sleep 1200
<= Pausing for 1200ms... 

此后 pub/get 可省略 --topic,默认落到该群组;脚本里控制节奏用 .sleep <毫秒>。

9.5 .verbose:原始包回显

打开 .verbose 后,所有 gRPC 包以 JSON 原样打印(节选实测,登录包与 hi 回包):

<= => {"hi": {"id": "15629", "userAgent": "tn-cli/3.0.1 (Linux/7.0.0-38-generic); gRPC-python/0.22.3+1.85.0", "ver": "0.22.3", "lang": "EN"}} 
<= <= {"ctrl": {"id": "15629", "code": 201, "text": "created", "params": {"msgDelAge": "NjAw", "minTagLength": "Mg==", ..., "build": "Im15c3FsOnYwLjI1LjMi", "maxMessageSize": "MTMxMDcy"}}} 
<= => {"login": {"id": "15630", "scheme": "basic", "secret": "YWRtaW46cTF3MmUzcjQ="}} 
<= <= {"ctrl": {"id": "15630", "code": 200, "text": "ok", "params": {"authlvl": "ImF1dGgi", "token": "<96 bytes: IjJOOHNxN2c4...TXdhSWs9Ig==>", "user": "InVzcjJOOHNxN2c4YndVIg==", "expires": "IjIwMjYtMTEtMDlUMTM6NDg6MjMuNjgzWiI="}}} 

gRPC 通道回执里的字符串值统一 base64 编码(ImF1dGgi 解码即 auth,InVzcjJOOHNxN2c4YndVIg== 解码即 "usr2N8sq7g8bwU"),token 超长时 tn-cli 折叠为 <96 bytes: 前 8...后 8>。对照 REST 通道同一登录(10.2 节)的明文 JSON,可以看清「同一包、两通道」的编码差异。

9.6 边界:启动参数不能当命令喂

--login-basic admin:q1w2e3r4、python3 这类非 tn-cli 词法的内容喂进交互界面,统一得到 Unrecognized: <首词> 加完整命令表(实测均已触发)。tn-cli 不是 shell:启动参数只在启动命令行上有效,会话内只有 9.1 表中的命令与宏。

十、REST API curl 调试矩阵(长轮询通道)

不装 tn-cli 也能完成全部调试——用 curl 对 /v0/channels/lp 端点「一次 POST 发包、一次 POST 收包」。以下实测共用变量:K=apikey,B=http://localhost:6060/v0/channels/lp。

10.1 建会话:空 body POST 拿 sid

$ curl -s -i -X POST "$B?apikey=$K"
HTTP/1.1 201 Created
Access-Control-Allow-Origin: *
Cache-Control: no-cache, no-store, must-revalidate
Content-Type: text/plain
Expires: 0
Vary: Accept-Encoding
Content-Length: 102

{"ctrl":{"params":{"sid":"AbbmwnbROT8"},"code":201,"text":"created","ts":"2026-10-26T13:42:49.515Z"}}

后续所有发包/收包都在 URL 上带 &sid=$SID。发包必须显式 Content-Type: application/json(反例见 10.4)。

10.2 hi 与 login(登录拿 token)

$ send '{"hi":{"id":"1","ver":"0.25","ua":"curl-rest/0.25.3"}}'; poll
{"ctrl":{"id":"1","params":{"build":"mysql:v0.25.3","maxFileUploadSize":8388608,"maxMessageSize":131072,"maxSubscriberCount":128,"maxTagCount":16,"maxTagLength":96,"minTagLength":2,"msgDelAge":600,"reqCred":null,"ver":"0.25"},"code":200,"text":"ok","ts":"2026-10-26T12:31:21.262Z"}}
$ send '{"login":{"id":"2","scheme":"basic","secret":"'"$(echo -n 'admin:q1w2e3r4' | base64 -w0)"'"}}'; poll
{"ctrl":{"id":"2","params":{"authlvl":"auth","expires":"2026-11-09T12:31:21.319Z","token":"2N8sq7g8bwUZvfFqFAABAAEA...","user":"usr2N8sq7g8bwU"},"code":200,"text":"ok","ts":"2026-10-26T12:31:21.28Z"}}

与 tn-cli 连接串(5.1 节)逐项对得上。token 与 expires(10 天后)在后续大文件请求里直接复用(10.5)。

10.3 fnd 搜索与群组消息闭环

  • 建群:{"sub":{"id":"3","topic":"new","desc":{"defaultAcs":...},"tags":["rest-demo"],"get":{"what":"sub"}}} → {"ctrl":{"id":"3","topic":"grpcUClKshckeg",...,"code":200,"text":"ok"}},群名与 tn-cli 的 sub --topic new 同语义;
  • 群组搜索:set fnd 写入查询词,再 {"get":{"id":"6","topic":"fnd","what":"sub"}} → {"meta":{...,"sub":[{"public":{"fn":"REST 演示群"},"private":["rest-demo"],"topic":"grpcUClKshckeg","subcnt":1}]}};
  • 用户搜索:用标签 basic:alice 检索 → {"meta":{...,"sub":[{"public":{"fn":"Alice"},"private":["basic:alice"],"topic":"usrumQ4gJFy6ro"}]}};
  • 发消息 + 读消息:{"pub":{"id":"4","topic":"grpFUJdHPLTPCI","head":{"mime":"text/plain"},"content":"REST curl 发出的群组消息"}} → {"ctrl":{"id":"4","topic":"grpFUJdHPLTPCI","params":{"seq":1},"code":202,"text":"accepted"}};再 {"get":{"id":"5","topic":"grpFUJdHPLTPCI","what":"data","data":{"limit":2}}} 收到 {"data":{"topic":"grpFUJdHPLTPCI","from":"usr2N8sq7g8bwU","ts":"...","seq":1,"head":{"mime":"text/plain"},"content":"REST curl 发出的群组消息"}}。

注意 REST 侧 pub 的成功码是 202 accepted(服务端确认入库并返回 seq),而 tn-cli 里显示为 200 ok(客户端把 202 归并显示);同一动作两条通道的 ctrl 码不完全同名,写脚本时以各自实测为准。

10.4 排障实录:缺 Content-Type → 400 malformed → 409 command out of sequence

首版脚本漏了发包 POST 的 -H 'Content-Type: application/json',实测两步回显:

(发包 hi,无 Content-Type)
$ poll
{"ctrl":{"id":"1","code":400,"text":"malformed"}}
(随后发 login)
$ poll
{"ctrl":{"id":"2","code":409,"text":"command out of sequence"}}

服务器把无 Content-Type 的 body 拒为 400 malformed(hi 没有生效),会话状态机仍停在「未问候」,后续 login 便报 409 command out of sequence。看到 409 command out of sequence,先回查前一个包是不是 400——补上 Content-Type 后同一序列实测 201 → 200 全通。

10.5 大文件上传与下载

上传走 /v0/file/u/,认证用两个头(X-Tinode-APIKey + X-Tinode-Auth: Token <token>),multipart 字段名固定 file(实测):

$ curl -s -X POST "http://localhost:6060/v0/file/u/?apikey=$K" \
    -H "X-Tinode-APIKey: $K" -H "X-Tinode-Auth: Token $TOKEN" \
    -F "file=@/tmp/d1-file.txt"
{"ctrl":{"params":{"expires":"2026-10-26T13:11:30.718","url":"/v0/file/s/VjeGtVOg73s.text"},"code":200,"text":"ok","ts":"2026-10-26T13:10:30.718Z"}}

不带认证头上传的对照实测:{"ctrl":{"code":401,"text":"authentication required"}}。

下载直接 GET 返回的 url(它已是完整路径):

$ curl -s -i "http://localhost:6060$FURL?apikey=$K" -H "X-Tinode-APIKey: $K" -H "X-Tinode-Auth: Token $TOKEN"
HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: max-age=86400
Content-Disposition: attachment
Content-Length: 55
Content-Type: text/plain
Etag: "agt2dnwpkkp2vb2ztqsmqhxuie"
Last-Modified: Mon, 26 Oct 2026 13:10:30 GMT
Vary: Accept-Encoding

tn file attachment body (created for REST matrix test)

两个实测边界:

操作实测回显教训
把返回的 /v0/file/s/xxx 再拼一层 /v0/file/s/ 前缀后 GETHTTP/1.1 307 Temporary Redirect,Location: /v0/file/s/v0/file/s/xxx?apikey=...(畸形路径循环指向自身)url 值已是完整路径,直接拼基址即可
下载不带认证头401文件端点同样受 token 保护,与上传同权限

10.6 运行时指标端点 /debug/vars

$ curl -s http://localhost:6060/debug/vars | head -8
{
"ClusterLeader": 0,
"CtrlCodesTotal2xx": 140,
"CtrlCodesTotal3xx": 11,
"CtrlCodesTotal4xx": 35,
"CtrlCodesTotal5xx": 0,
"DbStats": {"MaxOpenConnections":64,"OpenConnections":1,"InUse":0,"Idle":1,...

Go expvar 风格的运行时计数(2xx/3xx/4xx/5xx 各码累计与数据库连接池状态),做健康巡检或压测观察都很直接;路径可用 -expvar - 禁用(第三章)。

十一、WebSocket 裸调测试

11.1 端点与握手

WebSocket 端点是 /v0/channels?apikey=...(注意没有 /lp 后缀)。用十几行 Python 裸 socket 就能完成握手与帧收发(实测脚本骨架):

import socket, base64, os, json, time
HOST, PORT = "localhost", 6060
APIKEY = "AQAAAAABAAAoeOI7tA3HsYvdzDhYhZJy"

s = socket.create_connection((HOST, PORT), timeout=10)
key = base64.b64encode(os.urandom(16)).decode()
req = (f"GET /v0/channels?apikey={APIKEY} HTTP/1.1\r\nHost: {HOST}:{PORT}\r\n"
       "Upgrade: websocket\r\nConnection: Upgrade\r\n"
       "Sec-WebSocket-Version: 13\r\n"
       f"Sec-WebSocket-Key: {key}\r\n\r\n")
s.sendall(req.encode())
# 读取握手响应,断言状态行含 101

客户端帧必须按 RFC 6455 加掩码(0x81 文本帧 + 4 字节 mask),服务端回帧不带掩码,直接按 length + payload 解析即可。

11.2 严格顺序闭环实测

握手成功后按 hi → login → sub fnd → set → get → attach → pub 七步发包,每步只等对应 id 的回包(实测逐帧回显):

[WS] 握手成功: HTTP/1.1 101 Switching Protocols
[<- 1] {"ctrl": {"id": "1", "params": {"build": "mysql:v0.25.3", ..., "ver": "0.25"}, "code": 201, "text": "created", "ts": "2026-10-26T13:09:47.271Z"}}
[<- 2] {"ctrl": {"id": "2", "params": {"authlvl": "auth", "expires": "2026-11-09T13:09:47.313Z", "token": "2N8sq7g8bwUbxvFqFAABAAEA...", "user": "usr2N8sq7g8bwU"}, "code": 200, "text": "ok", "ts": "2026-10-26T13:09:47.272Z"}}
[<- 3] {"ctrl": {"id": "3", "topic": "fnd", "code": 200, "text": "ok", "ts": "2026-10-26T13:09:47.314Z"}}
[<- 4] {"ctrl": {"id": "4", "topic": "fnd", "code": 200, "text": "ok", "ts": "2026-10-26T13:09:47.314Z"}}
[<- 5] {"meta": {"id": "5", "topic": "fnd", "ts": "2026-10-26T13:09:47.315Z", "sub": [{"updated": "2026-10-26T11:10:04.958Z", "acs": {"mode": "JRWPS"}, "private": ["beta"], "topic": "grpeomGUOyvOTA", "subcnt": 2}]}}
[<- 6] {"ctrl": {"id": "6", "topic": "grpeomGUOyvOTA", "code": 200, "text": "ok", "ts": "2026-10-26T13:09:47.316Z"}}
[<- 7] {"ctrl": {"id": "7", "topic": "grpeomGUOyvOTA", "params": {"seq": 4}, "code": 202, "text": "accepted", "ts": "2026-10-26T13:09:47.317Z"}}
[WS] closed

11.3 WS 与 LP 的差异要点

维度长轮询(LP)WebSocket
连接建立POST /v0/channels/lp → 201 created + sid101 Switching Protocols 升级握手
会话标识每次请求带 &sid=握手后同连接直通,无 sid 参数
hi 回执码200 ok(复用已建会话时)/ 201 created201 created
收包方式显式轮询,一次一包服务端主动推帧
适用场景curl 快速调试长连接客户端行为模拟、推送时序验证

三条通道(gRPC/LP/WS)上 login 回执的 params.user、token、authlvl 完全同构(对照 5.1、10.2、11.2 三处实测),可放心把 tn-cli 里的操作翻译成任意通道的原始包。

十二、注意事项与常见问题

12.1 注意事项清单

类别要点依据(章节)
登录态一个会话只能 login 一次,重复得 409 already authenticated5.2
attach 前置pub/del 前 sub,否则 409 must attach first;get data 则 403 permission denied6.2
引号多词 --content 必须整体引号,多余词会覆盖同名位置参数 topic6.3
useradd用户名是位置参数、密码用 -P、显示名 --name、--cred 必带7.2/7.3
set me--fn/--public 经 tn-cli(gRPC)不生效(304),改公开资料走 REST/WS7.5
文件tn-cli 的 file(gRPC)桩缺失不可用,用 upload(HTTP)+ --web-host8.1/8.2
宏.await/.must 必须与命令同行,单行 .await 按不识别处理9.3
REST发包 POST 必须带 Content-Type: application/json,否则 400 连锁 40910.4
文件 url/v0/file/u/ 返回的 url 已是完整路径,勿再拼 /v0/file/s/ 前缀10.5
认证文件上传下载都要 X-Tinode-APIKey + X-Tinode-Auth: Token 两个头10.5

12.2 常见问题

Q1:409 must attach first 到底缺了哪一步? tn-cli 里先执行 sub --topic <群组名>(既订阅又挂接会话),之后 pub/get/del 才有资格。REST/WS 里对应先发 {sub} 包再发后续包。

Q2:useradd 报 Exception in generator: 'NoneType' object has no attribute 'synchronous'? 缺 --cred。该异常出现时账号未创建(可用 resolve <名> 或查 auth 表确认);补上 --cred email:... 即可 201 created。

Q3:resolve 只打出一行 Error in '.log $temp.sub[0].topic': sub? 这是「账号不存在」:fnd 查询结果为空数组,宏打印变量时取 [0] 失败。存在的账号会直接打印 uid(如 usrpC22fQJIPU4)。

Q4:长轮询接口连 login 都发不出去,409 command out of sequence? 前一个包(通常是 hi)因缺 Content-Type: application/json 被拒为 400 malformed,会话状态机没前进。先修 Content-Type,再按 hi → login 顺序重发。

Q5:tn-cli 敲任何命令都提示 Unrecognized 并刷一大屏命令表? 三种可能:把启动参数(--login-basic 等)当命令喂了;.await/.must 单独占了一行;或者命令拼写不在第四章总表里。命令表本身就是速查页,逐词对照即可。

十三、参考链接