# twitter-cli 全量 AI 参考文档

> 版本：1.0.0 / 2026-04-17
> 适用对象：AI agent、自动化脚本、开发者
> stdout 严格 JSONL；stderr 为 tracing 日志（默认关闭）

---

# 1. 概述

`twitter-cli` 是 x-api-rs 库附带的命令行工具，面向 AI agent 自动化场景设计。

核心特性：
- stdout 仅输出机器可读的 JSONL，无任何人类美化装饰
- stderr 承载 tracing 日志，与 stdout 完全分离
- 所有错误包含结构化 code、retryable、recovery_actions 字段
- 12 种确定性退出码，便于 shell 脚本条件判断
- 批量命令采用三段式 header/item/summary 格式，支持流式处理

当前实装 32 个 action，覆盖 8 个功能模块（dm / posts / user / search / upload / settings / xchat / raw）。

新增清理类 action（2026-05-08）：
- posts clear-bookmarks — 清空所有书签
- user block / unblock — 拉黑 / 解除拉黑
- user remove-profile-image / remove-profile-banner — 还原默认头像 / Banner
- user get-lists — 获取已加入/订阅的 Lists
- user unsubscribe-list — 退订 List

新增 XChat E2E 加密私信 action（2026-05-12, Phase 3.x）：
- xchat enroll / status / reset-pin / delete-account — 设备注册管理（Envelope 输出）
- xchat send — 单条私信，自动按收件人 enrollment 状态选路（明文/加密）
- xchat send-batch / send-batch-custom — 批量并发发送（三段式 BatchLine）
- xchat receive — 拉取对话最近消息 + 解码 + 解密 + 验签（三段式 BatchLine）

⚠️ XChat 加密 wire 格式当前是推测式实现（基于 xchat-kmp.js 反混淆），未在真实双端
   enrolled 抓包对账过；send 返回 `warning="encryption_implemented_unverified"` 时
   调用方应感知风险。详见 docs/api/xchat-enrollment.md。

---

# 2. 快速开始

## 安装

```bash
cargo install x-api-rs --features cli
# 首次编译约 10-15 分钟

twitter-cli --version
# twitter-cli 1.0.0
```

## 获取 cookies

1. 浏览器登录 twitter.com
2. 开发者工具（F12）→ Network 标签 → 任意请求的 Cookie 头
3. 复制包含 ct0、auth_token、twid 的完整 cookies 字符串
4. 保存到文件并设置权限：

```bash
echo 'ct0=xxx; auth_token=xxx; twid=u%3Dxxx; ...' > ~/.x-api/cookies.txt
chmod 600 ~/.x-api/cookies.txt
```

## 第一条命令

```bash
twitter-cli --cookies-file cookies.txt user get --screen-name twitter
```

成功输出（实际为一行 JSONL）：

```json
{"success":true,"data":{"id":"783214","name":"Twitter","screen_name":"twitter","followers_count":65000000,"verified":true},"error":null,"meta":{"module":"user","action":"get","elapsed_ms":342,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

---

# 3. 全局 flags

所有子命令共享以下全局 flags，必须放在子命令名称之前：

```
twitter-cli [全局 flags] <模块> <子命令> [子命令参数]
```

| Flag | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `--cookies-file <PATH>` | path | — | 从文件读取 cookies（认证优先级最高） |
| `--cookies-stdin` | bool flag | false | 显式从 stdin 读取 cookies（5s 超时） |
| `--profile <NAME>` | string | — | 使用 ~/.x-api/config.toml 中的 profile |
| `--proxy <URL>` | string | — | 代理 URL（覆盖 profile.proxy），支持 http/socks5 |
| `--request-timeout-secs <N>` | u64 | 30 | 单次 HTTP 请求超时（秒） |
| `--log-level <LEVEL>` | string | off | stderr 日志级别：off\|error\|warn\|info\|debug\|trace |
| `--log-format <FORMAT>` | string | json | stderr 日志格式：json\|compact |
| `--fingerprint <SPEC>` | string | chrome:windows:x64 | 浏览器指纹 profile（TLS+HTTP 一体），见下方语法 |

**`--fingerprint` 语法**（TLS 指纹 + HTTP 头一体，不填默认 `chrome:windows:x64`）：

| SPEC | 说明 |
|------|------|
| `chrome:<os>:<arch>` | Chrome 136。os: windows/macos/linux；arch: x64/arm64 |
| `safari-mac:<ver>` | Safari macOS：17.5 / 18 / 18.2 / 18.3 / 18.3.1 |
| `safari-ios:<ver>` | Safari iOS：17.4.1 / 18.1.1 |
| `safari-ipad` | Safari iPadOS 18 |
| `firefox:<ver>` | Firefox：133 / 135 / 136 / private136 / android135 |
| `edge:<ver>` | Edge：131 / 134 |
| `okhttp:<ver>` | OkHttp（Android app）：5 / 4.12 |
| `none` | 禁用 TLS 模拟（调试） |

跨账号真实 JA3 差异需跨家族；HTTP/2 始终开启。Chrome 锚定 136（rquest-util TLS 上限）。

**指纹示例**：
```bash
# Chrome macOS arm64
twitter-cli --fingerprint chrome:macos:arm64 --cookies-file c.txt doctor

# Safari iOS（跨家族 → 真实 JA3 差异）
twitter-cli --fingerprint safari-ios:18.1.1 --cookies-file c.txt dm send --user-id 123 --text "hi"
```

---

# 4. 认证优先级链

CLI 按以下顺序查找认证信息（从高到低，找到即停止）：

```
1. --cookies-file <path>          最高优先级，明确指定文件
2. --cookies-stdin / 非 tty stdin  从 stdin pipe 读取，5 秒超时后报错
3. X_API_COOKIES_FILE 环境变量     指向 cookies 文件路径
4. ~/.x-api/config.toml           profile.cookies_file 字段
```

如果四种方式都未找到认证信息，CLI 退出码 3（AUTH_FAILED）。

## Cookies 文件权限规则（Unix）

| 文件权限 | 行为 |
|----------|------|
| `0o600` 或更严格 | 正常运行 |
| `0o644`（其他用户可读） | stderr 输出 WARNING，继续运行 |
| `0o660` 以上（group/other 可写） | 拒绝运行，退出码 3 |

推荐：`chmod 600 ~/.x-api/cookies.txt`

## config.toml 格式

```toml
[profile.default]
cookies_file = "~/.x-api/cookies.txt"
proxy = "http://127.0.0.1:8080"   # 可选

[profile.account2]
cookies_file = "~/.x-api/cookies2.txt"
```

使用指定 profile：`twitter-cli --profile account2 ...`

---

# 5. 输出格式规范

## 5.1 stdout vs stderr 分离

| 流 | 内容 | 格式 |
|----|------|------|
| stdout | 命令结果（JSONL） | 严格 JSON，每行一个对象，无额外空白 |
| stderr | tracing 日志 | 由 --log-format 控制（json / compact） |

AI agent 只解析 stdout，忽略 stderr：

```bash
result=$(twitter-cli ... 2>/dev/null)
```

## 5.2 Envelope 格式（单条命令）

所有非批量命令输出单行 JSON：

```
{"success":bool,"data":T|null,"error":ErrorObj|null,"meta":{...}}
```

### Envelope 字段

| 字段 | 类型 | 说明 |
|------|------|------|
| success | bool | true=操作成功，false=失败 |
| data | T\|null | 成功时的结果；失败时为 null |
| error | ErrorObj\|null | 失败时的错误；成功时为 null |
| meta.module | string | 模块名（dm/posts/user/search/upload/settings） |
| meta.action | string | 操作名（send/create/get 等） |
| meta.elapsed_ms | number | 命令总耗时（毫秒） |
| meta.timestamp | string | ISO 8601 UTC 时间戳 |
| meta.cli_version | string | CLI 版本（如 "1.0.0"） |
| meta.schema_version | string | 输出格式版本（如 "1.0"） |

### 完整成功示例

```json
{"success":true,"data":{"user_id":"783214","event_id":"1811234567890123456","message":"Message sent successfully","media_id":null},"error":null,"meta":{"module":"dm","action":"send","elapsed_ms":519,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

### 完整失败示例

```json
{"success":false,"data":null,"error":{"code":"AUTH_FAILED","message":"Twitter 返回 401，cookies 可能已过期","retryable":false,"retry_after_secs":null,"docs_url":"https://x-api-rs.es007.com/cli/getting-started/#cookies","recovery_actions":["重新从浏览器导出 cookies","确认 cookies 包含 ct0 和 auth_token 字段"],"issue_url":null,"details":{"http_status":401}},"meta":{"module":"dm","action":"send","elapsed_ms":128,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

## 5.3 BatchLine 三段式格式（批量命令）

批量命令（`dm send-batch`）输出多行 JSONL，用 `type` 字段区分三种行类型：

### header 行（第 1 行）

```json
{"type":"header","command":"dm send-batch","schema_version":"1.0","expected_count":3,"meta":{"module":"dm","action":"send-batch","timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0"}}
```

### item 行（每个目标一行）

成功：
```json
{"type":"item","index":0,"success":true,"data":{"user_id":"783214","event_id":"1811234567890123456","message":"Message sent successfully","media_id":null}}
```

失败：
```json
{"type":"item","index":1,"success":false,"error":{"code":"NOT_FOUND","message":"用户 999999 不存在或无法接收私信","retryable":false,"retry_after_secs":null,"recovery_actions":["确认 user_id 是否正确"]}}
```

### summary 行（最后 1 行）

```json
{"type":"summary","total":3,"success_count":2,"fail_count":1,"elapsed_ms":1820}
```

### 完整 5 行示例（3 个目标）

```jsonl
{"type":"header","command":"dm send-batch","schema_version":"1.0","expected_count":3,"meta":{"module":"dm","action":"send-batch","timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0"}}
{"type":"item","index":0,"success":true,"data":{"user_id":"783214","event_id":"1811234567890123456","message":"Message sent successfully","media_id":null}}
{"type":"item","index":1,"success":false,"error":{"code":"NOT_FOUND","message":"用户 6253282 不存在","retryable":false,"retry_after_secs":null,"recovery_actions":["确认 user_id 是否正确"]}}
{"type":"item","index":2,"success":true,"data":{"user_id":"1234567890","event_id":"1811234567890123457","message":"Message sent successfully","media_id":null}}
{"type":"summary","total":3,"success_count":2,"fail_count":1,"elapsed_ms":1820}
```

## 5.4 ErrorObject 结构

```json
{
  "code": "INVALID_ARGS",
  "message": "人类可读的错误描述",
  "retryable": false,
  "retry_after_secs": null,
  "docs_url": "https://...",
  "recovery_actions": ["步骤1", "步骤2"],
  "issue_url": null,
  "details": {"http_status": 422}
}
```

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| code | string | 是 | ErrorCode 枚举值 |
| message | string | 是 | 人类可读描述 |
| retryable | bool | 是 | 是否值得重试 |
| retry_after_secs | number\|null | 否 | 建议等待秒数（来自 Retry-After 响应头） |
| docs_url | string\|null | 否 | 相关文档链接 |
| recovery_actions | string[] | 是 | 建议修复步骤 |
| issue_url | string\|null | 否 | GitHub issue 链接（已知 Bug） |
| details | object\|null | 否 | 额外调试信息（如 http_status） |

---

# 6. ErrorCode 完整说明

| code | exit_code | retryable | 典型触发场景 |
|------|-----------|-----------|-------------|
| INVALID_ARGS | 2 | false | 参数缺失/格式错误，如 --text 超过字符限制，--user-id 为空 |
| AUTH_FAILED | 3 | false | cookies 过期、ct0 缺失、Twitter 返回 401 |
| NETWORK | 4 | true | 连接超时、DNS 解析失败、代理不可达 |
| RATE_LIMIT | 5 | true | Twitter 返回 429，触发频率限制 |
| NOT_FOUND | 6 | false | 用户不存在、帖子已删除、404 响应 |
| SERVER | 7 | true | Twitter 服务端 5xx 错误 |
| DUPLICATE | 8 | false | 重复操作（如重复转发同一帖子） |
| MEDIA_UPLOAD_FAILED | 9 | false | 图片格式不支持、超过大小限制、上传失败 |
| CONTENT_VIOLATION | 10 | false | 内容被判定为垃圾内容或违规 |
| DEPRECATED_COMMAND | 11 | false | 调用已废弃的子命令 |
| CONFIG_PARSE_ERROR | 12 | false | ~/.x-api/config.toml 格式错误或字段缺失 |
| UNKNOWN | 1 | false | 未分类的意外错误 |

---

# 7. 退出码

```
0   成功
1   UNKNOWN
2   INVALID_ARGS
3   AUTH_FAILED
4   NETWORK
5   RATE_LIMIT
6   NOT_FOUND
7   SERVER
8   DUPLICATE
9   MEDIA_UPLOAD_FAILED
10  CONTENT_VIOLATION
11  DEPRECATED_COMMAND
12  CONFIG_PARSE_ERROR
```

---

# 8. 命令参考

## 8.1 dm send — 发送单条私信

```
twitter-cli [全局 flags] dm send \
  --user-id <ID> \
  --text <STRING> \
  [--media-id <ID>]
```

| 参数 | 必填 | 说明 |
|------|------|------|
| --user-id | 是 | 目标用户数字 ID |
| --text | 是 | 消息内容，最大 10000 字符 |
| --media-id | 否 | 来自 upload image 的 media_id_string |

成功输出：

```json
{"success":true,"data":{"user_id":"783214","event_id":"1811234567890123456","message":"Message sent successfully","media_id":null},"error":null,"meta":{"module":"dm","action":"send","elapsed_ms":519,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出：

```json
{"success":false,"data":null,"error":{"code":"NOT_FOUND","message":"用户 999999 不存在或无法接收私信","retryable":false,"retry_after_secs":null,"recovery_actions":["确认 user_id 是否正确","检查目标用户是否开放私信权限"]},"meta":{"module":"dm","action":"send","elapsed_ms":312,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：user_id (string), event_id (string), message (string), media_id (string|null)

---

## 8.2 dm send-batch — 批量发送私信

```
twitter-cli [全局 flags] dm send-batch \
  --user-ids <ID,ID,...> \
  (--text <STRING> | --texts-file <PATH>) \
  [--media-id <ID>]
```

| 参数 | 必填 | 说明 |
|------|------|------|
| --user-ids | 是 | 逗号分隔的用户 ID 列表 |
| --text | 二选一 | 所有用户统一文案 |
| --texts-file | 二选一 | 自定义文案文件（每行一条，行数 = user-ids 数量） |
| --media-id | 否 | 所有用户共享的媒体 ID |

texts-file 格式（每行一条，UTF-8）：

```
你好，张三！欢迎使用。
你好，李四！感谢支持。
你好，王五！祝使用愉快。
```

输出格式：BatchLine 三段式（见第 5.3 节），退出码以 summary 为准（任何失败不改变退出码 0，但 fail_count > 0 时建议检查 item 行）。

成功 header 行：

```json
{"type":"header","command":"dm send-batch","schema_version":"1.0","expected_count":3,"meta":{"module":"dm","action":"send-batch","timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0"}}
```

成功 summary 行：

```json
{"type":"summary","total":3,"success_count":3,"fail_count":0,"elapsed_ms":1234}
```

---

## 8.3 dm inbox — 查询收件箱

```
twitter-cli [全局 flags] dm inbox [--max <N>]
```

| 参数 | 默认值 | 说明 |
|------|--------|------|
| --max | 20 | 返回最大条目数 |

成功输出：

```json
{"success":true,"data":{"entries":[{"conversation_id":"783214-6253282","last_message_id":"1811234567890123456","last_message_text":"Hello from twitter-cli!","participants":["783214","6253282"],"unread_count":0}],"next_cursor":"1811234567890123300","entry_count":1},"error":null,"meta":{"module":"dm","action":"inbox","elapsed_ms":421,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出：

```json
{"success":false,"data":null,"error":{"code":"AUTH_FAILED","message":"Twitter 返回 401","retryable":false,"retry_after_secs":null,"recovery_actions":["重新从浏览器导出 cookies"]},"meta":{"module":"dm","action":"inbox","elapsed_ms":128,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：entries (array), entries[].conversation_id (string), entries[].last_message_id (string), entries[].last_message_text (string), entries[].participants (string[]), entries[].unread_count (number), next_cursor (string|null), entry_count (number)

---

## 8.4 posts create — 发布帖子

```
twitter-cli [全局 flags] posts create \
  --text <STRING> \
  [--media-ids <ID,ID,...>] \
  [--reply-to <TWEET_ID>] \
  [--made-with-ai] \
  [--warning-for <MEDIA_ID>=<flags> ...]
```

| 参数 | 必填 | 说明 |
|------|------|------|
| --text | 是 | 帖子内容，最大 280 字符 |
| --media-ids | 否 | 逗号分隔的媒体 ID（最多 4 张图片） |
| --reply-to | 否 | 回复的帖子 ID |
| --made-with-ai | 否 | 帖子级 "Made with AI" 声明（顶部显示 AI 生成提示） |
| --warning-for | 否 | 给指定 media_id 设置图片级 content warning。语法 `<MEDIA_ID>=<flags>`，flags 用逗号分隔。可多次出现 |

`--warning-for` 的 flags 关键字：

| flag | 含义 |
|------|------|
| `nudity` | Nudity 标签（sensitive_media_warning: adult_content）|
| `violence` | Violence 标签（graphic_violence）|
| `sensitive` | Sensitive 标签（other）|
| `ai` | 图片级 "Generated with AI" 标签（与 `--made-with-ai` 不同，后者是帖子级）|
| `block-grok` | 阻止 Grok 修改 |
| `no-download` | 显式禁止下载 |

示例：

```bash
# 帖子级 AI 声明
twitter-cli posts create --text "Generated art" --made-with-ai

# 单张图片打 Nudity + 图片级 AI
twitter-cli posts create --text "post body" \
  --media-ids 123456 \
  --warning-for 123456=nudity,ai

# 多图各自不同 warning
twitter-cli posts create --text "post body" \
  --media-ids 111,222 \
  --warning-for 111=violence,block-grok \
  --warning-for 222=sensitive
```

成功输出：

```json
{"success":true,"data":{"tweet_id":"1811234567890123456","text":"Hello from twitter-cli! #rust","author_id":"6253282","created_at":"2026-04-17T10:45:00Z","url":"https://x.com/i/status/1811234567890123456"},"error":null,"meta":{"module":"posts","action":"create","elapsed_ms":612,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出：

```json
{"success":false,"data":null,"error":{"code":"CONTENT_VIOLATION","message":"帖子内容被 Twitter 检测为垃圾内容或违规","retryable":false,"retry_after_secs":null,"recovery_actions":["修改帖子内容后重试","检查是否包含违禁词或链接"]},"meta":{"module":"posts","action":"create","elapsed_ms":389,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：tweet_id (string), text (string), author_id (string), created_at (string ISO8601), url (string)

---

## 8.5 posts delete — 删除帖子

```
twitter-cli [全局 flags] posts delete --id <TWEET_ID>
```

成功输出：

```json
{"success":true,"data":{"tweet_id":"1811234567890123456","deleted":true},"error":null,"meta":{"module":"posts","action":"delete","elapsed_ms":298,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出：

```json
{"success":false,"data":null,"error":{"code":"NOT_FOUND","message":"帖子不存在或已被删除","retryable":false,"retry_after_secs":null,"recovery_actions":["确认帖子 ID 是否正确","确认帖子是否属于当前账号"]},"meta":{"module":"posts","action":"delete","elapsed_ms":201,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：tweet_id (string), deleted (bool, 固定 true)

---

## 8.6 posts like — 点赞帖子

```
twitter-cli [全局 flags] posts like --id <TWEET_ID>
```

**幂等性**：重复点赞返回 success:true，不报错。

成功输出：

```json
{"success":true,"data":{"tweet_id":"1811234567890123456","liked":true},"error":null,"meta":{"module":"posts","action":"like","elapsed_ms":215,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出：

```json
{"success":false,"data":null,"error":{"code":"NOT_FOUND","message":"帖子不存在或已删除","retryable":false,"retry_after_secs":null,"recovery_actions":["确认帖子 ID 是否正确"]},"meta":{"module":"posts","action":"like","elapsed_ms":187,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：tweet_id (string), liked (bool, 固定 true)

---

## 8.7 posts unlike — 取消点赞

```
twitter-cli [全局 flags] posts unlike --id <TWEET_ID>
```

**幂等性**：重复取消点赞返回 success:true。

成功输出：

```json
{"success":true,"data":{"tweet_id":"1811234567890123456","liked":false},"error":null,"meta":{"module":"posts","action":"unlike","elapsed_ms":231,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出：

```json
{"success":false,"data":null,"error":{"code":"NOT_FOUND","message":"帖子不存在或已删除","retryable":false,"retry_after_secs":null,"recovery_actions":["确认帖子 ID 是否正确"]},"meta":{"module":"posts","action":"unlike","elapsed_ms":198,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：tweet_id (string), liked (bool, 固定 false)

---

## 8.8 posts retweet — 转发帖子

```
twitter-cli [全局 flags] posts retweet --id <TWEET_ID>
```

**注意**：重复转发返回 DUPLICATE 错误（退出码 8），与 like 行为不同。

成功输出：

```json
{"success":true,"data":{"tweet_id":"1811234567890123456","retweet_id":"1811234567890123999","retweeted":true},"error":null,"meta":{"module":"posts","action":"retweet","elapsed_ms":334,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出（重复转发）：

```json
{"success":false,"data":null,"error":{"code":"DUPLICATE","message":"已经转发过该帖子","retryable":false,"retry_after_secs":null,"recovery_actions":["如需取消转发，使用 posts unretweet 命令"]},"meta":{"module":"posts","action":"retweet","elapsed_ms":198,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：tweet_id (string), retweet_id (string), retweeted (bool, 固定 true)

---

## 8.9 posts unretweet — 取消转发

```
twitter-cli [全局 flags] posts unretweet --id <TWEET_ID>
```

**注意**：`--id` 是原帖子 ID，不是转发产生的帖子 ID。

成功输出：

```json
{"success":true,"data":{"tweet_id":"1811234567890123456","retweeted":false},"error":null,"meta":{"module":"posts","action":"unretweet","elapsed_ms":287,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出：

```json
{"success":false,"data":null,"error":{"code":"NOT_FOUND","message":"帖子不存在或已删除","retryable":false,"retry_after_secs":null,"recovery_actions":["确认帖子 ID 是否正确"]},"meta":{"module":"posts","action":"unretweet","elapsed_ms":234,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：tweet_id (string), retweeted (bool, 固定 false)

---

## 8.9.1 posts clear-bookmarks — 清空所有书签

```
twitter-cli [全局 flags] posts clear-bookmarks
```

无参数。一次性清空当前账号的所有书签，**不可撤销**。等同于 X 网页版「Clear all bookmarks」。

成功输出：

```json
{"success":true,"data":{"success":true,"message":"Done","error_msg":"","http_status":200},"error":null,"meta":{"module":"posts","action":"clear-bookmarks","elapsed_ms":890,"timestamp":"2026-05-08T12:00:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：success (bool), message (string, 成功时 "Done"), error_msg (string), http_status (number)

---

## 8.10 user get — 通过用户名查询

```
twitter-cli [全局 flags] user get --screen-name <NAME>
```

screen-name 不含 @ 前缀（如 `twitter` 而非 `@twitter`）。

成功输出：

```json
{"success":true,"data":{"id":"783214","name":"Twitter","screen_name":"twitter","description":"What's happening?!","followers_count":65000000,"following_count":0,"tweet_count":15234,"listed_count":89123,"verified":true,"protected":false,"created_at":"2007-02-20T14:35:54Z","profile_image_url":"https://pbs.twimg.com/profile_images/880136122604507136/xHrnqf1T_normal.jpg","profile_banner_url":"https://pbs.twimg.com/profile_banners/783214/1594913664","location":"127.0.0.1","url":"https://about.twitter.com/"},"error":null,"meta":{"module":"user","action":"get","elapsed_ms":342,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出：

```json
{"success":false,"data":null,"error":{"code":"NOT_FOUND","message":"用户 @nonexistent_xyz_abc 不存在或已停用","retryable":false,"retry_after_secs":null,"recovery_actions":["确认用户名拼写是否正确","检查用户是否已注销或被封禁"]},"meta":{"module":"user","action":"get","elapsed_ms":189,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：id (string), name (string), screen_name (string), description (string), followers_count (number), following_count (number), tweet_count (number), listed_count (number), verified (bool), protected (bool), created_at (string ISO8601), profile_image_url (string|null), profile_banner_url (string|null), location (string|null), url (string|null)

---

## 8.11 user get-by-id — 通过 ID 查询

```
twitter-cli [全局 flags] user get-by-id --rest-id <ID>
```

返回结构与 `user get` 完全相同。用户 ID 永不变，适合长期追踪特定用户。

成功输出：

```json
{"success":true,"data":{"id":"783214","name":"Twitter","screen_name":"twitter","description":"What's happening?!","followers_count":65000000,"following_count":0,"tweet_count":15234,"listed_count":89123,"verified":true,"protected":false,"created_at":"2007-02-20T14:35:54Z","profile_image_url":"https://pbs.twimg.com/profile_images/880136122604507136/xHrnqf1T_normal.jpg","profile_banner_url":"https://pbs.twimg.com/profile_banners/783214/1594913664","location":"127.0.0.1","url":"https://about.twitter.com/"},"error":null,"meta":{"module":"user","action":"get-by-id","elapsed_ms":287,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出：

```json
{"success":false,"data":null,"error":{"code":"NOT_FOUND","message":"用户 ID 999999999999999 不存在","retryable":false,"retry_after_secs":null,"recovery_actions":["确认用户 ID 是否正确"]},"meta":{"module":"user","action":"get-by-id","elapsed_ms":201,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

---

## 8.11.1 user block / unblock — 拉黑与解除拉黑

```
twitter-cli user block --user-id <ID>
twitter-cli user unblock --user-id <ID>
```

「强制清除粉丝」一键写法：`block <UID> && unblock <UID>`（拉黑会强制对方取消关注）。

成功输出（block）：

```json
{"success":true,"data":{"success":true,"user_id":"44196397","screen_name":"elonmusk","error_msg":"","http_status":200},"error":null,"meta":{"module":"user","action":"block","duration_ms":421,"cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：success (bool), user_id (string), screen_name (string|null), error_msg (string), http_status (number)

---

## 8.11.2 user remove-profile-image / remove-profile-banner — 还原默认头像/Banner

```
twitter-cli user remove-profile-image
twitter-cli user remove-profile-banner
```

无参数。⚠️ 不可逆。

成功输出：

```json
{"success":true,"data":{"success":true,"error_msg":"","http_status":200},"error":null,"meta":{"module":"user","action":"remove-profile-image","duration_ms":318,"cli_version":"1.0.0","schema_version":"1.0"}}
```

---

## 8.11.3 user get-lists — 获取已加入/订阅的 Lists

```
twitter-cli user get-lists [--count <N>]
```

| 参数 | 默认 | 说明 |
|------|------|------|
| --count | 100 | 单次返回的最大数量 |

成功输出（节选）：

```json
{"success":true,"data":{"success":true,"lists":[{"id_str":"1528554046273253376","name":"Majulah Singapora","mode":"Public","member_count":59,"subscriber_count":5795,"owner_screen_name":"m4rinos","following":false,"is_member":false,"pinning":false}],"next_cursor":"DAAH...","error_msg":"","http_status":200},"error":null,"meta":{"module":"user","action":"get-lists","duration_ms":890,"cli_version":"1.0.0","schema_version":"1.0"}}
```

data.lists[] 字段：id_str (string), name (string), description (string|null), mode (string "Public"/"Private"), member_count (number), subscriber_count (number), owner_id_str (string|null), owner_screen_name (string|null), owner_display_name (string|null), following (bool), is_member (bool), pinning (bool), created_at (number|null, ms timestamp)

---

## 8.11.4 user unsubscribe-list — 退订 List

```
twitter-cli user unsubscribe-list --list-id <ID>
```

成功输出：

```json
{"success":true,"data":{"success":true,"list_id":"1528554046273253376","still_following":false,"error_msg":"","http_status":200},"error":null,"meta":{"module":"user","action":"unsubscribe-list","duration_ms":482,"cli_version":"1.0.0","schema_version":"1.0"}}
```

批量退订（POSIX）：

```bash
twitter-cli --cookies-file cookies.txt user get-lists | \
  jq -r '.data.lists[] | select(.following == true) | .id_str' | \
  while read lid; do
    twitter-cli --cookies-file cookies.txt user unsubscribe-list --list-id "$lid"
  done
```

---

## 8.12 search top — 搜索热门帖子

```
twitter-cli [全局 flags] search top \
  --query <QUERY> \
  [--cursor <CURSOR>]
```

按相关性和热度排序（对应 Twitter 的 "Top" 标签）。

| 参数 | 必填 | 说明 |
|------|------|------|
| --query | 是 | 搜索关键词，支持 Twitter 搜索语法 |
| --cursor | 否 | 分页游标（来自上一页的 next_cursor） |

搜索语法示例：
- 基础：`"rust programming"`
- 指定用户：`"from:elonmusk"`
- 时间范围：`"rust since:2026-01-01 until:2026-04-17"`
- 组合：`"#rustlang has:media min_faves:100 lang:en"`

成功输出：

```json
{"success":true,"data":{"tweets":[{"tweet_id":"1811234567890123456","text":"Rust is amazing! #rust","author_id":"6253282","author_name":"Rustacean","author_screen_name":"rustacean_dev","created_at":"2026-04-17T09:30:00Z","like_count":1523,"retweet_count":342,"reply_count":87,"view_count":45123,"lang":"en"}],"next_cursor":"DAABCgABGBi-NkIAAA","prev_cursor":null,"result_count":20},"error":null,"meta":{"module":"search","action":"top","elapsed_ms":687,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出：

```json
{"success":false,"data":null,"error":{"code":"RATE_LIMIT","message":"搜索 API 请求过于频繁，Twitter 返回 429","retryable":true,"retry_after_secs":900,"recovery_actions":["等待 retry_after_secs 秒后重试","降低搜索请求频率","避免并发运行多个搜索命令"]},"meta":{"module":"search","action":"top","elapsed_ms":145,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：tweets (array), tweets[].tweet_id (string), tweets[].text (string), tweets[].author_id (string), tweets[].author_name (string), tweets[].author_screen_name (string), tweets[].created_at (string ISO8601), tweets[].like_count (number), tweets[].retweet_count (number), tweets[].reply_count (number), tweets[].view_count (number|null), tweets[].lang (string), next_cursor (string|null), prev_cursor (string|null), result_count (number)

---

## 8.13 search latest — 搜索最新帖子

```
twitter-cli [全局 flags] search latest \
  --query <QUERY> \
  [--cursor <CURSOR>]
```

按时间倒序排序（对应 Twitter 的 "Latest" 标签），适合实时监控。

参数、返回结构与 `search top` 完全相同，仅排序逻辑不同（时间倒序 vs 热度排序）。

成功输出：

```json
{"success":true,"data":{"tweets":[{"tweet_id":"1811234567890123999","text":"Just published a new Rust blog post!","author_id":"1234567890","author_name":"Dev Guy","author_screen_name":"devguy","created_at":"2026-04-17T10:30:00Z","like_count":12,"retweet_count":3,"reply_count":1,"view_count":234,"lang":"en"}],"next_cursor":"DAABCgABGBi-NkIBBB","prev_cursor":null,"result_count":20},"error":null,"meta":{"module":"search","action":"latest","elapsed_ms":531,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出（临时限流，返回 404）：

```json
{"success":false,"data":null,"error":{"code":"NOT_FOUND","message":"搜索 API 返回 404，可能是临时频率限制","retryable":true,"retry_after_secs":30,"recovery_actions":["等待 30 秒后单独重试此命令","避免并发运行多个搜索"]}}
```

**频率限制警告**：搜索 API 对并发敏感，不要同时运行多个 search 命令。

---

## 8.14 upload image — 上传图片

```
twitter-cli [全局 flags] upload image \
  --file <PATH> \
  [--category tweet-image|dm-image|banner-image]
```

| 参数 | 必填 | 默认 | 说明 |
|------|------|------|------|
| --file | 是 | — | 本地图片路径（JPEG/PNG/GIF/WebP） |
| --category | 否 | tweet-image | 媒体类别 |

category 说明：
- `tweet-image`：用于 posts create --media-ids
- `dm-image`：用于 dm send --media-id
- `banner-image`：用于用户 Banner（Python API）

大小限制：JPEG/PNG/WebP 最大 5MB；GIF 最大 15MB。

成功输出：

```json
{"success":true,"data":{"media_id":1811234567890123456,"media_id_string":"1811234567890123456","size":245678,"image":{"image_type":"image/jpeg","w":1920,"h":1080},"expires_after_secs":86400},"error":null,"meta":{"module":"upload","action":"image","elapsed_ms":1234,"timestamp":"2026-04-17T10:45:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

失败输出（文件过大）：

```json
{"success":false,"data":null,"error":{"code":"MEDIA_UPLOAD_FAILED","message":"图片文件超过大小限制（最大 5MB）","retryable":false,"retry_after_secs":null,"recovery_actions":["压缩图片后重试","使用 ImageMagick: convert input.jpg -resize 1920x1080\\> -quality 85 output.jpg"],"details":{"file_size":8234567,"max_size":5242880}},"meta":{"module":"upload","action":"image","elapsed_ms":89,"timestamp":"2026-04-17T10:46:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

data 字段：media_id (number, 不推荐), **media_id_string** (string, 推荐使用), size (number bytes), image.image_type (string MIME), image.w (number), image.h (number), expires_after_secs (number, 媒体 ID 有效期)

**重要**：始终使用 `media_id_string` 而非 `media_id`（大整数可能有精度损失）。

---

## 8.14b upload set-metadata — 设置图片 content warning / AI 标签

给已上传的图片设置图片级 content warning、AI 生成声明、Block-Grok 修改、下载控制。底层调用 `POST /1.1/media/metadata/create.json`。

通常推荐用 `posts create --warning-for` 一步完成；该命令仅在分步流程（先上传再独立标注，最后另起命令发帖）时使用。

```
twitter-cli [全局 flags] upload set-metadata \
  --media-id <ID> \
  [--warning <nudity|violence|sensitive> ...] \
  [--ai-generated] \
  [--block-grok-edit] \
  [--disallow-download]
```

| 参数 | 必填 | 默认 | 说明 |
|------|------|------|------|
| --media-id | 是 | — | 已上传图片的 media_id_string |
| --warning | 否 | 无 | content warning 类别，可多次出现：`nudity` / `violence` / `sensitive` |
| --ai-generated | 否 | false | 图片级 "Generated with AI" 标签 |
| --block-grok-edit | 否 | false | 阻止 Grok 修改 |
| --disallow-download | 否 | false | 显式禁止下载 |

成功输出：

```json
{"success":true,"data":{"media_id":"1811234567890123456","success":true},"error":null,"meta":{"module":"upload","action":"set-metadata","elapsed_ms":89,"timestamp":"2026-06-01T13:00:00Z","cli_version":"1.0.0","schema_version":"1.0"}}
```

示例：

```bash
# 给图片打 Nudity + AI 标签
twitter-cli upload set-metadata --media-id 1811234567890123456 \
  --warning nudity --ai-generated

# 多分类 + Block-Grok
twitter-cli upload set-metadata --media-id 1811234567890123456 \
  --warning violence --warning sensitive --block-grok-edit
```

---

## 8.15 settings get-account — 查询完整账号设置

```
twitter-cli [全局 flags] settings get-account
```

无需额外参数。查询 account/settings.json 端点，返回 18 个账号设置字段。

成功输出：

```json
{"success":true,"data":{"screen_name":"myaccount","language":"zh-cn","country_code":"CN","protected":false,"geo_enabled":false,"nsfw_user":false,"nsfw_admin":false,"display_sensitive_media":false,"allow_dms_from":"following","allow_dm_groups_from":"following","allow_media_tagging":"all","discoverable_by_email":true,"discoverable_by_mobile_phone":false,"dm_receipt_setting":"all_enabled","dm_quality_filter":"enabled","mention_filter":"unfiltered","allow_ads_personalization":true,"allow_sharing_data_for_third_party_personalization":true},"error":null,"meta":{"module":"settings","action":"get-account","elapsed_ms":312,"timestamp":"2026-06-04T10:00:00Z","cli_version":"2.0.0","schema_version":"1.0"}}
```

失败输出：

```json
{"success":false,"data":null,"error":{"code":"AUTH_FAILED","message":"Twitter 返回 401","retryable":false,"retry_after_secs":null,"recovery_actions":["重新从浏览器导出 cookies"]},"meta":{"module":"settings","action":"get-account","elapsed_ms":132,"timestamp":"2026-06-04T10:00:01Z","cli_version":"2.0.0","schema_version":"1.0"}}
```

data 字段：screen_name (string), language (string), country_code (string), protected (bool), geo_enabled (bool), nsfw_user (bool), nsfw_admin (bool), display_sensitive_media (bool), allow_dms_from (string: "following"|"verified"|"all"), allow_dm_groups_from (string), allow_media_tagging (string: "all"|"following"|"none"), discoverable_by_email (bool), discoverable_by_mobile_phone (bool), dm_receipt_setting (string: "all_enabled"|"all_disabled"), dm_quality_filter (string: "enabled"|"disabled"), mention_filter (string: "unfiltered"|"following"|"verified"), allow_ads_personalization (bool), allow_sharing_data_for_third_party_personalization (bool)

## 8.15b settings update-account — 批量更新账号设置

```
twitter-cli [全局 flags] settings update-account [flags...]
```

一次 POST 请求批量修改多个账号设置，只需传入要修改的 flag，未传入的字段不会被修改。返回更新后的完整 AccountSettings。

可用 flag（均为可选）：--protected BOOL, --nsfw-user BOOL, --display-sensitive-media BOOL, --geo-enabled BOOL, --allow-dms-from STR (following|verified|all), --allow-dm-groups-from STR, --allow-media-tagging STR (all|following|none), --discoverable-by-email BOOL, --discoverable-by-mobile BOOL, --dm-receipt-setting STR (all_enabled|all_disabled), --dm-quality-filter STR (enabled|disabled), --mention-filter STR (unfiltered|following|verified), --allow-ads-personalization BOOL, --allow-third-party-personalization BOOL

成功输出：Envelope，data 为更新后的完整 AccountSettings（字段与 get-account 相同）

## 8.15c settings get-search-safety — 查询搜索安全设置

```
twitter-cli [全局 flags] settings get-search-safety
```

成功输出：

```json
{"success":true,"data":{"opt_in_filtering":false,"opt_in_blocking":true},"error":null,"meta":{"module":"settings","action":"get-search-safety","elapsed_ms":120,"timestamp":"2026-06-04T10:02:00Z","cli_version":"2.0.0","schema_version":"1.0"}}
```

data 字段：opt_in_filtering (bool, true=搜索中隐藏敏感内容), opt_in_blocking (bool, true=从搜索结果中移除已屏蔽/静音账号)

## 8.15d settings set-search-safety — 设置搜索安全

```
twitter-cli [全局 flags] settings set-search-safety --filtering BOOL --blocking BOOL
```

两个参数均为必填，一次 POST 同时写入两个字段，无需预读当前值。成功输出：Envelope，data.setting_name = "search_safety"

## 8.15e settings get-audience — 查询受众设置

```
twitter-cli [全局 flags] settings get-audience
```

使用 GraphQL AudienceAndTaggingQuery 查询视频保护设置。成功输出：

```json
{"success":true,"data":{"protect_videos":false},"error":null,"meta":{"module":"settings","action":"get-audience","elapsed_ms":205,"timestamp":"2026-06-04T10:04:00Z","cli_version":"2.0.0","schema_version":"1.0"}}
```

## 8.15f settings set-protect-videos — 设置视频下载保护

```
twitter-cli [全局 flags] settings set-protect-videos --enabled BOOL
```

使用 GraphQL AudienceAndTaggingAllowVideoDownloadsMutation。--enabled true = 开启保护（禁止下载），--enabled false = 允许下载。成功输出：Envelope，data.setting_name = "protect_videos"

## 8.15g settings get-explore — 查询 Explore 设置

```
twitter-cli [全局 flags] settings get-explore
```

成功输出：

```json
{"success":true,"data":{"use_personalized_trends":true,"use_current_location":false},"error":null,"meta":{"module":"settings","action":"get-explore","elapsed_ms":95,"timestamp":"2026-06-04T10:06:00Z","cli_version":"2.0.0","schema_version":"1.0"}}
```

## 8.15h settings set-explore — 更新 Explore 设置

```
twitter-cli [全局 flags] settings set-explore [--personalized-trends BOOL] [--current-location BOOL]
```

至少传入一个 flag。注意：部分地区账号（如日本区）服务端可能忽略 --personalized-trends false。成功输出：Envelope，data.setting_name = "explore_settings"

---

## 8.16 xchat enroll — 启用 XChat E2E 加密私信

```
twitter-cli [全局 flags] xchat enroll --pin <PIN> --keystore <PATH>
```

| 参数 | 环境变量 | 类型 | 说明 |
|---|---|---|---|
| `--pin` | `XCHAT_PIN` | string | 用户 PIN（任意 unicode，推荐 4-6 位） |
| `--keystore` | `XCHAT_KEYSTORE` | path | 密钥文件路径，**会被创建**（0600 权限） |

`hide_env_values=true` —— `--help` 输出不显示 PIN 实际值。

成功输出：
```json
{"success":true,"data":{"user_id":"1973098228334960640","key_version":"1778485658072","registration_method":"CustomPin","realms_registered":3},"error":null,"meta":{"module":"xchat","action":"enroll",...}}
```

错误码：`DUPLICATE`（已 enroll）、`AUTH_FAILED`（PIN 错/cookies 失效）、`UNKNOWN`（复合错误 `EnrollRollbackFailed`，需手动 `delete-account --yes` 清理）。

## 8.17 xchat status — 查询本地+服务端状态

```
twitter-cli [全局 flags] xchat status --keystore <PATH>
```

成功输出：
```json
{"success":true,"data":{"enrolled":true,"key_version":"1778485658072","registration_method":"CustomPin","local_keystore_exists":true,"key_version_synced":true},"error":null,"meta":{...}}
```

## 8.18 xchat reset-pin — 修改 PIN

```
twitter-cli [全局 flags] xchat reset-pin --old-pin <P> --new-pin <P> --keystore <PATH>
```

环境变量：`XCHAT_OLD_PIN` / `XCHAT_NEW_PIN`。

## 8.19 xchat delete-account — 注销（不可逆）

```
twitter-cli [全局 flags] xchat delete-account --pin <PIN> --keystore <PATH> --yes
```

环境变量：`XCHAT_CONFIRM_PIN`（独立变量，避免与 enroll 的 `XCHAT_PIN` 冲突误触发）。

`--yes` 缺失时返回 `INVALID_ARGS`，避免脚本误删。

## 8.20 xchat send — 单条加密/明文私信

```
twitter-cli [全局 flags] xchat send --user-id <ID> --text <TEXT> --keystore <PATH>
```

| 参数 | 说明 |
|---|---|
| `--user-id` | 收件人 Twitter user_id（数字串） |
| `--text` | 消息内容（UTF-8，最大 10000 字符） |

SDK 按收件人状态自动选路（双独立判定，2026-05-12 抓包确认）：
- `has_keys=false` → 明文+签名，`warning="recipient_not_enrolled"`
- `has_keys=true && can_dm_on_xchat=false` → 明文兜底，`warning="recipient_disabled_xchat"` ⚠️
- `has_keys=true && can_dm_on_xchat=true` → AES-GCM 加密+签名，`warning="encryption_implemented_unverified"` ⚠️

`can_dm_on_xchat` 是**双边**字段（依赖 sender/recipient 关系 + 隐私设置），不等于"对方是否已 enroll"。

成功输出：
```json
{"success":true,"data":{"success":true,"message_id":"550e8400-...","event_id":"19854...","sequence_id":"...","timestamp_ms":1762202353456,"warning":"recipient_not_enrolled","conversation_key_version":null},"error":null,"meta":{...}}
```

## 8.21 xchat send-batch — 批量统一文案

```
twitter-cli [全局 flags] xchat send-batch --user-ids u1,u2,u3 --text <TEXT> --keystore <PATH>
```

三段式 BatchLine 输出（header + items + summary）。退出码：全部成功 `0`，任一失败 `1`。

单条失败 Item：`{"type":"item","index":N,"success":false,"data":{...,"warning":"send_failed: <reason>"},"error":null}`。

## 8.22 xchat send-batch-custom — 批量自定义文案

```
twitter-cli [全局 flags] xchat send-batch-custom \
  --user-ids u1,u2 --texts-file <PATH> --keystore <PATH>
```

`--texts-file` 每行一条文案，**与 `--user-ids` 严格按行号一一对应**：
- 不静默过滤空行/纯空白（避免索引错位）
- 行数必须等于 `--user-ids` 数量，否则 `INVALID_ARGS`
- 空白文案由业务层 `validate_batch_message_input` 拒绝

## 8.23 xchat receive — 拉取对话最近消息

```
twitter-cli [全局 flags] xchat receive \
  --conv-id <PEER_ID:SELF_ID> --limit 20 --keystore <PATH>
```

| 参数 | 默认 | 说明 |
|---|---|---|
| `--conv-id` | — | 对话 ID（格式 `<peer_id>:<self_id>`） |
| `--limit` | 20 | 拉取条数上限 |

三段式 BatchLine 输出，每条 Item 是一个 `DecryptedMessage`：

```json
{"type":"item","index":0,"success":true,"data":{
  "event_id":"...","message_id":"...","sender_id":"...","conversation_id":"...",
  "timestamp_ms":1762202353456,"sequence_id":1234,
  "text":"hello","encrypted":false,"signature_valid":true,"warning":null
},"error":null}
```

`success = (warning is null)`。失败 Item 仍带 `data`（含 elaborate warning）。

`data.warning` 取值：
- `"conversation_key_unknown"` — 无对话密钥（先发一条消息触发协商）
- `"decrypt_failed"` — AES-GCM 解密失败
- `"signature_input_unknown"` — inner_payload 字节形态不明，跳过验签
- `"sender_not_enrolled"` — 拉不到发送方公钥，无法验签

⚠️ 加密 wire 格式 / 验签字节选取未在真实双端抓包对账过，多数场景 `signature_valid` 可能返回 `null`。详见 docs/api/xchat-enrollment.md。

---

# 9. 常见错误及修复

## AUTH_FAILED（退出码 3）

**现象**：Twitter 返回 401，cookies 认证失败。

**修复**：
1. 重新从浏览器导出 cookies（登录 twitter.com → F12 → Network → Cookie 头）
2. 确认 cookies 包含 `ct0=` 和 `auth_token=` 字段
3. 检查文件权限：`chmod 600 cookies.txt`

## RATE_LIMIT（退出码 5）

**现象**：Twitter 返回 429。

**修复**：
1. 读取 `error.retry_after_secs`，等待对应秒数后重试
2. 降低请求频率（批量操作间添加延迟）
3. 搜索 API 尤其敏感：不要并发运行多个 search 命令

```bash
# 读取 retry_after 并等待
retry_after=$(echo "$result" | jq '.error.retry_after_secs // 60')
sleep "$retry_after"
```

## NETWORK（退出码 4）

**现象**：连接超时、DNS 失败。

**修复**：
1. 检查网络连接
2. 尝试添加 `--proxy http://127.0.0.1:8080`
3. 增加超时：`--request-timeout-secs 60`

## CONTENT_VIOLATION（退出码 10）

**现象**：帖子/私信内容被 Twitter 判定为违规。

**修复**：
1. 修改内容，避免包含违禁词、垃圾链接
2. 避免重复发布完全相同的内容（DUPLICATE，退出码 8）

## CONFIG_PARSE_ERROR（退出码 12）

**现象**：config.toml 格式错误。

**修复**：检查 TOML 语法，确认必要字段：

```toml
[profile.default]
cookies_file = "/absolute/path/to/cookies.txt"
```

---

# 10. AI agent 集成示例

## Python 解析单条命令

```python
import json
import subprocess

def run_cli(*args):
    result = subprocess.run(
        ["twitter-cli"] + list(args),
        capture_output=True, text=True
    )
    envelope = json.loads(result.stdout.strip())
    return envelope, result.returncode

envelope, code = run_cli(
    "--cookies-file", "cookies.txt",
    "user", "get", "--screen-name", "twitter"
)

if envelope["success"]:
    user = envelope["data"]
    print(f"ID: {user['id']}, followers: {user['followers_count']}")
else:
    error = envelope["error"]
    if error["retryable"]:
        wait = error.get("retry_after_secs") or 30
        time.sleep(wait)
        # 重试...
    else:
        raise Exception(f"不可重试错误: {error['code']}: {error['message']}")
```

## Python 解析批量命令

```python
import json
import subprocess

proc = subprocess.Popen(
    ["twitter-cli", "--cookies-file", "cookies.txt",
     "dm", "send-batch", "--user-ids", "783214,6253282",
     "--text", "Hello batch!"],
    stdout=subprocess.PIPE, text=True
)

items = []
summary = None

for line in proc.stdout:
    obj = json.loads(line.strip())
    t = obj.get("type")
    if t == "header":
        print(f"预期处理 {obj['expected_count']} 个用户")
    elif t == "item":
        items.append(obj)
        if obj["success"]:
            print(f"  [{obj['index']}] 成功: {obj['data']['event_id']}")
        else:
            print(f"  [{obj['index']}] 失败: {obj['error']['code']}")
    elif t == "summary":
        summary = obj

proc.wait()
print(f"完成：{summary['success_count']}/{summary['total']} 成功")

# 提取失败的索引用于重试
failed = [item for item in items if not item["success"]]
```

## Shell 脚本模式

```bash
#!/bin/bash
set -euo pipefail

COOKIES="$HOME/.x-api/cookies.txt"

# 运行命令并检查退出码
run_and_check() {
  local output
  output=$(twitter-cli --cookies-file "$COOKIES" "$@" 2>/dev/null)
  local code=$?

  case $code in
    0)  echo "$output" ;;
    3)  echo "AUTH ERROR: 请更新 cookies" >&2; exit 3 ;;
    5)
        local wait
        wait=$(echo "$output" | jq '.error.retry_after_secs // 60')
        echo "RATE LIMIT: 等待 ${wait}s..." >&2
        sleep "$wait"
        run_and_check "$@"  # 递归重试（注意：生产环境需限制重试次数）
        ;;
    *)  echo "ERROR $code: $(echo "$output" | jq -r '.error.message')" >&2; exit "$code" ;;
  esac
}

# 使用示例
result=$(run_and_check user get --screen-name twitter)
echo "$result" | jq '.data.id'
```

---

# 11. schema_version 版本化规则

| 变更类型 | 版本变化 | 向后兼容 |
|----------|----------|----------|
| 新增可选字段 | Minor 升级（1.0 → 1.1） | 是 |
| 修改字段名或类型 | Major 升级（1.0 → 2.0） | 否 |
| 删除已有字段 | Major 升级 | 否 |

AI agent 应检查 Major 版本：

```python
schema_ver = envelope["meta"]["schema_version"]
major = int(schema_ver.split(".")[0])
assert major == 1, f"不支持的 schema_version: {schema_ver}"
```

---

# 12. 命令速查表

| 命令 | 必填参数 | 输出格式 |
|------|---------|---------|
| `dm send` | --user-id, --text | Envelope |
| `dm send-batch` | --user-ids, --text 或 --texts-file | BatchLine |
| `dm inbox` | — | Envelope |
| `posts create` | --text | Envelope |
| `posts delete` | --id | Envelope |
| `posts like` | --id | Envelope |
| `posts unlike` | --id | Envelope |
| `posts retweet` | --id | Envelope |
| `posts unretweet` | --id | Envelope |
| `user get` | --screen-name | Envelope |
| `user get-by-id` | --rest-id | Envelope |
| `search top` | --query | Envelope |
| `search latest` | --query | Envelope |
| `upload image` | --file | Envelope |
| `settings get-account` | — | Envelope |
| `settings update-account` | 多个可选 flag（见下方） | Envelope |
| `settings get-search-safety` | — | Envelope |
| `settings set-search-safety` | --filtering BOOL --blocking BOOL | Envelope |
| `settings get-audience` | — | Envelope |
| `settings set-protect-videos` | --enabled BOOL | Envelope |
| `settings get-explore` | — | Envelope |
| `settings set-explore` | [--personalized-trends BOOL] [--current-location BOOL] | Envelope |
| `raw get` | --url | Envelope |
| `raw post` | --url [--json STR] [--form KEY=VALUE] | Envelope |
| `doctor` | — | Envelope（含 profile 字段） |
