Files
blog/write/docs/bot_design/design.md
T
2026-06-02 00:23:04 +08:00

915 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WriteBot 模块设计文档
## 一、架构
Bot 作为 Next.js 的 sidecar 模块运行,通过 `instrumentation.ts` 在服务端启动时自动拉起。
```
write/src/lib/bot/
├── index.ts # 启动入口
├── config.ts # Bot 专用配置
├── tg.ts # Telegram API 封装
├── sessions.ts # 写作会话状态管理
├── handlers.ts # 全部命令处理器
├── poll.ts # 长轮询循环(带指数退避)
└── helpers.ts # 工具函数
```
Bot 不再通过 HTTP 调 write API,直接调用 `src/lib/posts.ts`、`src/lib/rssapi.ts` 等 lib 函数。
---
## 二、交互方式
### 2.1 Telegram 原生组件使用规则
| 组件 | 用途 | 使用场景 |
|------|------|---------|
| ReplyKeyboard(底部键盘) | 常驻操作按钮 | 所有文本回复都带,分主键盘和写作键盘两套 |
| InlineKeyboard(消息内按钮) | 列表操作、确认、分页 | 文章列表编辑/删除、删除确认、分页导航、打开编辑器 |
| ForceReply(强制回复) | 引导输入 | 新建标题、设置分类/标签、添加友链/订阅源 |
| Web App | 内置浏览器打开编辑器 | /start 时提供 |
**关键规则:ReplyKeyboard 和 InlineKeyboard 不能混在同一个 reply_markup 里。**
### 2.2 ReplyKeyboard 布局
主键盘(非写作状态):
```
[/new 写文章] [/list 文章列表]
[/links 友链] [/feeds 订阅]
[/stats 数据] [/help 帮助]
```
写作键盘(写作会话中):
```
[/preview 预览] [/publish 发布]
[/draft 草稿] [/cancel 取消]
[/title 标题] [/categories 分类]
```
---
## 三、命令清单
### 3.1 系统命令
| 命令 | 参数 | 说明 |
|------|------|------|
| `/start` | 无 | 启动 Hugo,显示地址,发 Web App 编辑器按钮 |
| `/stop` | 无 | 停止 Hugo |
| `/status` | 无 | 显示 Hugo 运行状态和地址 |
| `/ip` | 无 | 显示局域网 IP |
| `/help` | 无 | 显示命令帮助(根据是否在写作中显示不同内容) |
### 3.2 文章命令
| 命令 | 参数 | 说明 |
|------|------|------|
| `/new` | 标题(可选) | 新建文章,见写作流程 |
| `/edit` | slug | 编辑已有文章,见编辑流程 |
| `/delete` | slug | 删除文章(二次确认),移入回收站 |
| `/list` | 页码(可选) | 文章列表,纯文本 + 分页 |
| `/view` | slug | 显示预览链接 |
| `/preview` | 无 | 预览当前写作内容 |
| `/publish` | 无 | 保存到本地(draft: false),不触发 git push |
| `/draft` | 无 | 保存到本地(draft: true) |
| `/deploy` | 无 | Git add → commit → pull --rebase → push |
| `/sync` | 无 | Git pull --rebase,同步远程 |
| `/undo` | 无 | 撤销最后一次追加的内容,自动预览 |
| `/cancel` | 无 | 丢弃当前写作会话 |
| `/title` | 新标题 | 修改文章标题(ForceReply 引导) |
| `/categories` | 分类 | 设置分类(逗号分隔) |
| `/tags` | 标签 | 设置标签(逗号分隔) |
| `/author` | 作者名 | 修改作者 |
### 3.3 友链命令
| 命令 | 参数 | 说明 |
|------|------|------|
| `/links` | 页码(可选) | 友链列表,纯文本 + 分页 + 命令提示 |
| `/link_add` | url 名称 | 添加友链(ForceReply 引导) |
| `/link_del` | url | 删除友链 |
| `/link_toggle` | url | 切换显示/隐藏 |
### 3.4 订阅命令
| 命令 | 参数 | 说明 |
|------|------|------|
| `/feeds` | 页码(可选) | 订阅源列表,纯文本 + 分页 + 命令提示 |
| `/feed_add` | url | 添加订阅源(ForceReply 引导) |
| `/feed_del` | url | 删除订阅源 |
### 3.5 数据命令
| 命令 | 参数 | 说明 |
|------|------|------|
| `/stats` | 无 | 显示文章数、友链数、订阅源数 |
---
## 四、核心流程
### 4.1 新建文章 `/new`
```
用户: /new 标题
↓ (如果没有标题,ForceReply 引导输入)
Bot: 📝 开始写:《标题》
📂 设置分类(可选)
[跳过] [设置分类] ← InlineKeyboard
↓
用户: 点 [跳过] → 直接进写作
或: 点 [设置分类] → ForceReply 引导输入分类
→ 然后同样问标签 [跳过] [设置标签]
↓
Bot: 切换写作键盘
"直接发消息写正文,完成后点「发布」"
↓
用户: 发消息 → 追加到 content
用户: 发照片 → 下载到文章文件夹,追加 markdown 引用
用户: /undo → 撤销最后一次追加,自动预览
↓
用户: /publish
↓
Bot: 保存到本地文件(draft: false)
"✅ 已保存,发 /deploy 推送到线上"
↓
用户: /deploy
↓
Bot: git add → commit → pull --rebase → push
"🚀 已发布并部署!"
```
Session 初始状态:
```typescript
{
title: "标题",
slug: "2026-06-01-标题",
content: "",
categories: [],
tags: [],
author: "小赵", // DEFAULT_AUTHOR 环境变量可覆盖
draft: true,
chunks: [], // 用于 /undo,记录每次追加的内容
mode: "write", // write | edit
}
```
### 4.2 编辑文章 `/edit`(方案 B)
```
用户: /edit slug
↓
Bot: 加载文章,显示原文内容(分条,每条 ≤3800 字)
清空 session.content(保留 title、slug、categories、tags、author)
切换写作键盘
"原文已显示,现在重新写正文,写完点「发布」"
↓
用户: 发消息 → 追加到新 content(和 /new 一样)
↓
后续流程同新建
```
关键区别:`/new` 的 slug 是新生成的,`/edit` 保留原文章的 slug。保存时调用 `updatePost` 而不是 `createPost`。
### 4.3 删除文章 `/delete`
```
用户: /delete slug
↓
Bot: "确认删除《标题》?"
[确认删除] [取消] ← InlineKeyboard
↓
用户: 点 [确认删除]
→ moveToRecycle(slug, dirPath, title)
→ "✂ 已移入回收站:《标题》\n30天内可恢复。"
↓
用户: 点 [取消]
→ "已取消"
```
poll.ts callback 处理新增 `confirm_del` 前缀:
```typescript
const CALLBACK_PREFIXES = {
edit: "edit",
confirm_del: "delete", // 确认删除
cancel_del: "noop", // 取消删除
list: "list",
links: "links",
feeds: "feeds",
skip_cat: "skip_categories", // 跳过分类
skip_tag: "skip_tags", // 跳过标签
};
```
### 4.4 文章列表 `/list`
纯文本显示,无 InlineKeyboard 操作按钮:
```
📖 文章列表 共 46 篇 · 第 1/10 页
我的第一篇文章
2026-06-01-my-first-post
另一篇文章 📝(草稿)
2026-05-28-another-post
◀ 上一页 下一页 ▶ ← InlineKeyboard(仅分页)
```
用户通过 slug 操作:`/edit slug`、`/delete slug`、`/view slug`。
### 4.5 友链列表 `/links`
纯文本 + 分页 + 命令提示:
```
🔗 友链 共 46 个 · 第 1/10 页
🌐 强仔博客
🌐 七栀
🔒 若志随笔(已隐藏)
操作命令:
/link_add url 名称 — 添加
/link_del url — 删除
/link_toggle url — 显示/隐藏
◀ 上一页 下一页 ▶
```
### 4.6 订阅列表 `/feeds`
同友链,纯文本 + 分页 + 命令提示。
---
## 五、图片处理(Hugo Page Bundle)
### 5.1 存储策略
图片存到文章自己的文件夹里,符合 Hugo page bundle 规范:
```
content/post/2026/2026-06-01-文章标题/
├── index.md
├── photo1.jpg
└── photo2.jpg
```
正文引用:`![描述](photo1.jpg)`(相对路径)
### 5.2 实现逻辑
写作中收到照片时:
1. 根据 session 的 slug 计算文章文件夹路径
- slug: `2026-06-01-文章标题`
- 文件夹: `content/post/2026/2026-06-01-文章标题/`
2. 文件夹不存在则创建
3. 下载图片到文件夹内
4. content 追加 `![caption](filename.jpg)`
保存时(draft/publish):
1. index.md 写到同一个文件夹
2. 图片已经在里面,无需移动
取消时:
1. 文件夹里只有图片没有 index.md
2. Hugo 不会渲染,无害
---
## 六、撤销 `/undo`
### 6.1 实现机制
Session 新增 `chunks: string[]` 字段:
```typescript
interface Session {
// ... 其他字段
chunks: string[]; // 每次追加的内容记录
}
```
每次用户发消息追加到 content 时,同时 push 到 chunks:
```typescript
s.chunks.push(fullText + "\n");
s.content += fullText + "\n";
```
### 6.2 `/undo` 流程
```
用户: /undo
↓
如果 chunks 为空: "没有可撤销的内容"
↓
否则:
lastChunk = chunks.pop()
content = content 去掉 lastChunk
→ 自动调用 preview 逻辑,显示当前文章内容
→ "↩ 已撤销最后一段(上方为当前内容)"
```
---
## 七、发布与部署(分离设计)
### 7.1 与网页端保持一致
| 操作 | 命令 | 效果 |
|------|------|------|
| 保存草稿 | `/draft` | 保存到本地,draft: true |
| 发布 | `/publish` | 保存到本地,draft: false,不触发 git |
| 部署 | `/deploy` | git add → commit → pull --rebase → push |
| 同步 | `/sync` | git pull --rebase,拉取远程最新 |
### 7.2 Git 冲突处理
`/deploy` 执行流程:
1. `git add content/post static`
2. `git diff --cached --quiet` → 有变更则 `git commit`
3. `git pull --rebase --autostash origin main`
- 成功 → 继续
- 冲突 → `git rebase --abort` → 回复"远程有冲突,已自动放弃合并。请先 /sync 同步,手动解决后再 deploy"
4. `git push`
---
## 八、配置
### 8.1 环境变量(write/.env)
```env
# Bot 配置
TG_BOT_TOKEN=xxx
TG_ALLOWED_CHAT_IDS=xxx # 留空允许所有人
PREFER_IFACE=WLAN
DEFAULT_AUTHOR=小赵 # 新增:默认作者名
```
### 8.2 config.ts 新增
```typescript
export const DEFAULT_AUTHOR = process.env.DEFAULT_AUTHOR || "小赵";
```
---
## 九、命令解析
### 9.1 ReplyKeyboard 按钮文字处理
按钮发出的文字如 `/new 写文章`,解析规则:
1. `split("@")[0]` 去掉 bot mention
2. `split(/\s+/)` 拆词
3. 第一个词去掉 `/` 前缀作为命令名
4. 其余词作为参数
`/new 写文章` → 命令 `new`,参数 `写文章`
但 `new` 命令的参数是标题,所以实际行为:`/new 写文章` 会创建一篇标题为"写文章"的文章。
需要特殊处理:如果参数和命令的中文描述匹配(如 `new` 的参数是 `写文章`),则忽略该参数。
或者更简单的方案:ReplyKeyboard 按钮只发命令,不带中文。
**最终方案:ReplyKeyboard 按钮文字改为纯命令:**
```
[/new] [/list]
[/links] [/feeds]
[/stats] [/help]
```
### 9.2 ForceReply 回复识别
poll.ts 通过 `reply_to_message.text` 匹配关键词来识别 ForceReply 回复:
| 关键词 | 路由到 |
|--------|--------|
| "请输入文章标题" | `handleCommand(chatId, "new", text)` |
| "请输入新标题" | `handleCommand(chatId, "title", text)` |
| "请输入友链信息" | `handleCommand(chatId, "link_add", text)` |
| "请输入订阅源 URL" | `handleCommand(chatId, "feed_add", text)` |
| "请输入分类" | `handleCommand(chatId, "categories", text)` |
| "请输入标签" | `handleCommand(chatId, "tags", text)` |
---
## 十、会话状态机
Session 有一个 `state` 字段,控制消息处理行为:
```
idle → 非写作状态,普通命令处理
writing_title → 等待 ForceReply 标题输入
writing_cat → 等待 ForceReply 分类输入(或 InlineKeyboard 跳过)
writing_tag → 等待 ForceReply 标签输入(或 InlineKeyboard 跳过)
writing → 写作中,消息追加到 content
confirm_del → 等待删除确认(InlineKeyboard)
```
状态转换:
```
idle → ( /new ) → writing_title → ( 输入标题 ) → writing_cat
→ ( 跳过/输入 ) → writing_tag → ( 跳过/输入 ) → writing → idle
idle → ( /edit slug ) → 显示原文 → 清空 content → writing → idle
idle → ( /delete slug ) → confirm_del → ( 确认 ) → idle
→ ( 取消 ) → idle
```
---
## 十一、友链模块
### 11.1 命令清单
| 命令 | 参数 | 说明 |
|------|------|------|
| `/links` | 页码(可选) | 友链列表,纯文本 + 分页 |
| `/link_add` | url 名称 | ForceReply 引导输入,添加友链 |
| `/link_edit` | url | ForceReply 引导输入新名称,通过 oldUrl 编辑 |
| `/link_del` | url | 删除友链 |
| `/link_toggle` | url | 切换显示/隐藏 |
### 11.2 列表显示格式
```
🔗 友链 共 46 个 · 第 1/10 页
🌐 吾柯 https://blog.keepke.com
🌐 强仔博客 https://loseu.cc
🔒 七栀 https://blog.qydzz.cn
操作:/link_add /link_edit /link_del /link_toggle
◀ 上一页 下一页 ▶
```
每条显示:图标(🌐 正常 / 🔒 隐藏)+ 名称 + URL。
每页 10 条。底部 InlineKeyboard 仅分页按钮。
底部文字提示可用操作命令。
### 11.3 `/link_edit` 流程
```
用户: /link_edit url
↓
Bot: 查找该友链,显示当前信息
"请输入新名称(留空保持不变):" ← ForceReply
↓
用户: 输入新名称(或发"跳过")
↓
Bot: 调用 POST /api/links { oldUrl, name: newName }
"✅ 已更新:新名称"
```
### 11.4 API 调用
- 列表:`GET /api/links?all=1`
- 添加:`POST /api/links` body: `{ name, url }`
- 编辑:`POST /api/links` body: `{ oldUrl, name, ... }`
- 删除:`DELETE /api/links?url=xxx`
- 切换:先 GET 获取 hidden 状态,再 POST `{ url, hidden: !current }`
---
## 十二、订阅模块
### 12.1 命令清单
| 命令 | 参数 | 说明 |
|------|------|------|
| `/feeds` | 页码(可选) | 订阅源列表,纯文本 + 分页 |
| `/feed_add` | url 名称 | ForceReply 引导输入 URL 和名称 |
| `/feed_edit` | url | ForceReply 引导编辑,通过 oldUrl |
| `/feed_del` | url | 删除订阅源 |
| `/feed_health` | 无 | 检测全部订阅源健康状态 |
### 12.2 列表显示格式
```
📡 订阅源 共 52 个 · 第 1/10 页
📰 吾柯 https://blog.keepke.com
📰 强仔博客 https://loseu.cc
操作:/feed_add /feed_edit /feed_del /feed_health
◀ 上一页 下一页 ▶
```
每页 10 条。
### 12.3 `/feed_health` 流程
调用 `GET /api/health`(不传 url 参数,检测全部订阅源)。
```
用户: /feed_health
↓
Bot: "⏳ 正在检测 52 个订阅源..."
↓
Bot:
📊 订阅源健康报告
✅ 存活:49 ❌ 异常:3
❌ 异常站点:
🔴 dead-blog.com — timeout
🔴 another.com — 500
🔴 third.com — 连接失败
```
只列出异常站点,正常的不逐一显示。
### 12.4 `/feed_edit` 流程
同友链编辑,通过 oldUrl 参数调用 POST /api/feeds。
### 12.5 API 调用
- 列表:`GET /api/feeds`
- 添加:`POST /api/feeds` body: `{ url, feedTitle }`
- 编辑:`POST /api/feeds` body: `{ oldUrl, url, feedTitle }`
- 删除:`DELETE /api/feeds?url=xxx`
- 健康:`GET /api/health`
---
## 十三、新增 `/read` 命令
利用 API 的 `GET /api/articles` 接口,获取友链博客的最新文章。
### 13.1 命令
| 命令 | 参数 | 说明 |
|------|------|------|
| `/read` | 数量(可选,默认 10) | 友链最新文章 |
### 13.2 显示格式
```
📰 友链最新文章(10 篇)
吾柯 — 如何在 Hugo 中实现友链
https://blog.keepke.com/xxx
强仔博客 — 2026 年度总结
https://loseu.cc/xxx
七栀 — 我的第一次旅行
https://blog.qydzz.cn/xxx
```
每条显示:博客名 — 文章标题 + 链接。
Telegram 自动将 URL 转为可点击链接。
### 13.3 API 调用
`GET /api/articles?limit=10`
响应结构:
```json
{
"total": 380,
"articles": [
{
"title": "...",
"link": "...",
"pubDate": "...",
"feedName": "吾柯",
"siteUrl": "...",
"favicon": "...",
"author": "..."
}
]
}
```
---
## 十四、完整命令注册列表
更新 `setMyCommands` 注册的命令:
```typescript
commands: [
{ command: "new", description: "写一篇文章" },
{ command: "edit", description: "编辑文章" },
{ command: "delete", description: "删除文章" },
{ command: "list", description: "文章列表" },
{ command: "publish", description: "保存发布" },
{ command: "draft", description: "保存草稿" },
{ command: "deploy", description: "推送到线上" },
{ command: "sync", description: "同步远程" },
{ command: "undo", description: "撤销最后一段" },
{ command: "preview", description: "预览" },
{ command: "links", description: "友链管理" },
{ command: "link_add", description: "添加友链" },
{ command: "link_edit", description: "编辑友链" },
{ command: "link_del", description: "删除友链" },
{ command: "link_toggle", description: "显示/隐藏友链" },
{ command: "feeds", description: "订阅管理" },
{ command: "feed_add", description: "添加订阅源" },
{ command: "feed_edit", description: "编辑订阅源" },
{ command: "feed_del", description: "删除订阅源" },
{ command: "feed_health", description: "订阅源健康检查" },
{ command: "read", description: "友链最新文章" },
{ command: "stats", description: "网站数据" },
{ command: "help", description: "帮助" },
]
```
---
## 十五、其他可考虑的功能
### 15.1 `/random` — 随机图片
利用 API 的 `GET /api/random-image` 接口,可以做一个随机壁纸/图片功能。
适合在聊天中发一张随机图片,增加趣味性。优先级低。
### 15.2 `/greeting` — 问候语
利用 API 的 `GET /api/ai/greeting` 接口,获取 AI 生成的问候语。
适合每天早上发一条问候。可以和定时任务结合。优先级低。
### 15.3 公众号同步 `/wechat`
利用 API 的 `POST /api/wechat-material` 接口,在 bot 中直接把文章同步到微信公众号草稿箱。
当前 write 已有公众号页面,bot 可以加一个 `/wechat slug` 命令。
优先级中——如果经常在手机上写文章,同步到公众号是个高频操作。
### 15.4 定时任务
利用 Cowork 的 scheduled-tasks 功能,可以设置:
- 每天早上 9 点发一条问候语 + 友链最新文章摘要
- 每周检测一次订阅源健康状态
- 每天自动部署未发布的草稿(可选)
这些功能当前不急,后续按需添加。
---
## 十六、确认的补充项
### 16.1 写作键盘更新
加入 `/undo` 和 `/deploy`:
```
[/preview] [/undo]
[/publish] [/deploy]
[/draft] [/cancel]
[/title] [/categories]
```
### 16.2 主键盘更新
按钮文字改为纯命令:
```
[/new] [/list]
[/links] [/feeds]
[/stats] [/help]
```
### 16.3 /stats 增强
显示本地数据 + API 数据:
```
📊 网站数据
📝 本地文章:128 篇
🔗 友链:46 个
📡 订阅源:52 个
📰 友链文章:380 篇(来自 52 个博客)
🌐 usj.cc
```
### 16.4 API 不可用降级
所有调用 `api.usj.cc` 的操作统一处理:
- 超时:10 秒
- 失败回复:"⚠️ API 暂时不可用,请稍后再试"
- 涉及命令:/links、/link_add、/link_edit、/link_del、/link_toggle、/feeds、/feed_add、/feed_edit、/feed_del、/feed_health、/read、/stats(API 部分)
---
## 十七、完整的 poll.ts 命令解析更新
### 17.1 ARG_COMMANDS 更新
```typescript
const ARG_COMMANDS = new Set([
"new", "title", "categories", "tags", "author",
"edit", "view", "delete",
"link_add", "link_edit", "link_del", "link_toggle",
"feed_add", "feed_edit", "feed_del",
"list", "links", "feeds", "read",
]);
```
### 17.2 CALLBACK_PREFIXES 更新
```typescript
const CALLBACK_PREFIXES: Record<string, string> = {
edit: "edit",
confirm_del: "delete",
cancel_del: "noop",
list: "list",
links: "links",
feeds: "feeds",
skip_cat: "skip_categories",
skip_tag: "skip_tags",
};
```
### 17.3 ForceReply 关键词更新
```typescript
if (original.includes("请输入文章标题")) → new
if (original.includes("请输入新标题")) → title
if (original.includes("请输入分类")) → categories
if (original.includes("请输入标签")) → tags
if (original.includes("请输入友链信息")) → link_add
if (original.includes("请输入新名称")) → link_edit
if (original.includes("请输入订阅源 URL")) → feed_add
if (original.includes("请输入新的订阅信息")) → feed_edit
```
### 17.4 InlineKeyboard callback 处理
新增 `confirm_del` 和 `cancel_del` 处理:
- `confirm_del_slug` → 执行删除
- `cancel_del_slug` → 取消,回复"已取消"
新增 `skip_cat` 和 `skip_tag` 处理:
- `skip_cat` → 跳过分类设置
- `skip_tag` → 跳过标签设置
---
## 十八、sessions.ts 更新
### 18.1 Session 接口
```typescript
interface Session {
title: string;
slug: string;
content: string;
categories: string[];
tags: string[];
author: string;
draft: boolean;
chunks: string[]; // 每次追加的内容,用于 /undo
originalSlug?: string; // /edit 时保留原 slug
}
```
### 18.2 默认作者
从 `DEFAULT_AUTHOR` 环境变量读取,默认 "小赵"。
---
## 十九、helpers.ts 更新
### 19.1 makeSlug 保持不变
`2026-06-01-标题拼音`
### 19.2 新增 getArticleDirPath
根据 slug 计算文章文件夹路径:
```typescript
function getArticleDirPath(slug: string): string {
// slug: "2026-06-01-文章标题"
// 从 slug 提取年份前四位
const year = slug.slice(0, 4);
return path.join(CONTENT_DIR, year, slug);
}
```
### 19.3 新增 truncateText
截断文本到指定长度,用于 Telegram 消息:
```typescript
function truncateText(text: string, maxLen: number): string {
if (text.length <= maxLen) return text;
return text.slice(0, maxLen) + "\n\n... 前 " + maxLen + " 字";
}
```
---
## 二十、config.ts 更新
新增环境变量:
```typescript
export const DEFAULT_AUTHOR = process.env.DEFAULT_AUTHOR || "小赵";
export const UAPI_BASE = process.env.RSS_API_BASE || "https://api.usj.cc";
export const UAPI_TOKEN = process.env.RSS_API_TOKEN || "";
```
---
## 二十一、rssapi.ts 新增函数
### 21.1 getArticles
```typescript
export async function getArticles(limit = 10): Promise<Article[]> {
const data = await rssFetch<{ articles: Article[] }>(`/api/articles?limit=${limit}`);
return data.articles || [];
}
```
### 21.2 checkHealth
```typescript
export async function checkHealth(): Promise<HealthResult> {
return rssFetch("/api/health");
}
```
### 21.3 updateLink 和 updateFeed 已存在
rssapi.ts 已有 `updateLink(oldUrl, data)` 函数,用于友链编辑。
需要新增 `updateFeed(oldUrl, data)` 函数用于订阅编辑。
---
## 二十二、git.ts 更新
### 22.1 GitDeployQueue 修改
`enqueue` 方法不再自动 add static 目录,改为只 add content:
```typescript
const addResult = await this.runGit(["add", "content"]);
```
### 22.2 新增 sync 方法
```typescript
async sync(): Promise<DeployResult> {
try {
const result = await this.runGit(["pull", "--rebase", "--autostash", "origin", "main"]);
return { success: true, output: result.stdout + result.stderr };
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
if (this.isRebaseConflict(msg)) {
await this.runGit(["rebase", "--abort"], true);
return { success: false, conflict: true, error: "远程冲突,已放弃合并" };
}
return { success: false, error: msg };
}
}
```
---
## 二十三、实施清单
按依赖顺序:
### 第一步:基础设施修改
1. config.ts — 新增 DEFAULT_AUTHOR、UAPI_BASE、UAPI_TOKEN
2. git.ts — 修改 add 路径、新增 sync 方法
3. rssapi.ts — 新增 getArticles、checkHealth、updateFeed
4. helpers.ts — 新增 getArticleDirPath、truncateText
5. sessions.ts — 新增 chunks、originalSlug 字段、默认作者从 config 读取
### 第二步:poll.ts 重写
6. 更新 ARG_COMMANDS
7. 更新 CALLBACK_PREFIXES
8. 更新 ForceReply 关键词路由
9. 新增 confirm_del / cancel_del / skip_cat / skip_tag 处理
### 第三步:handlers.ts 重写
10. 系统命令(start/stop/status/ip/help)
11. 文章命令(new/edit/delete/list/view/title/categories/tags/author)
12. 写作命令(preview/publish/draft/deploy/sync/undo/cancel)
13. 友链命令(links/link_add/link_edit/link_del/link_toggle)
14. 订阅命令(feeds/feed_add/feed_edit/feed_del/feed_health)
15. 数据命令(stats/read)
### 第四步:键盘和命令注册
16. mainKeyboard — 纯命令
17. writingKeyboard — 加 /undo /deploy
18. setMyCommands — 完整命令列表
### 第五步:验证
19. TypeScript 类型检查
20. 本地 npm run dev 测试