# 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 = { 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 { const data = await rssFetch<{ articles: Article[] }>(`/api/articles?limit=${limit}`); return data.articles || []; } ``` ### 21.2 checkHealth ```typescript export async function checkHealth(): Promise { 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 { 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 测试