477 lines
15 KiB
Markdown
477 lines
15 KiB
Markdown
# Bot + Write 整合架构方案
|
||||
|
|
|
|||
|
|
## 目标
|
|||
|
|
|
|||
|
|
将 `bot/`(Telegram Bot)的全部功能合并进 `write/`(Next.js 16)项目,消除双进程问题,统一文件操作、Hugo 管理、Git 部署。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、整合后的目录结构
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
write/
|
|||
|
|
├── src/
|
|||
|
|
│ ├── app/ # Next.js 页面 + API 路由(现有)
|
|||
|
|
│ ├── components/ # React 组件(现有)
|
|||
|
|
│ ├── lib/
|
|||
|
|
│ │ ├── config.ts # 统一配置(合并 bot 的 .env 字段)
|
|||
|
|
│ │ ├── posts.ts # 文章 CRUD(现有,改异步)
|
|||
|
|
│ │ ├── ai.ts # AI 功能(现有)
|
|||
|
|
│ │ ├── artalk.ts # 评论管理(现有)
|
|||
|
|
│ │ ├── recycle.ts # 回收站(现有)
|
|||
|
|
│ │ ├── rssapi.ts # 友链/订阅 API(现有)
|
|||
|
|
│ │ ├── hugo.ts # ★ 新增:Hugo 进程单例管理器
|
|||
|
|
│ │ ├── git.ts # ★ 新增:Git 操作队列
|
|||
|
|
│ │ └── bot/ # ★ 新增:Telegram Bot 模块
|
|||
|
|
│ │ ├── index.ts # bot 启动入口,导出 startBot()
|
|||
|
|
│ │ ├── config.ts # bot 专用配置(TG_TOKEN, ALLOWED_IDS)
|
|||
|
|
│ │ ├── tg.ts # Telegram API 封装(tg(), sendMessage(), handleCallback())
|
|||
|
|
│ │ ├── sessions.ts # 写作会话状态管理
|
|||
|
|
│ │ ├── handlers.ts # 全部 /command 处理器
|
|||
|
|
│ │ ├── poll.ts # 长轮询循环(带指数退避)
|
|||
|
|
│ │ └── helpers.ts # 工具函数(getLanIP, makeSlug, sessionSummary)
|
|||
|
|
│ └── instrumentation.ts # ★ 新增:Next.js 服务端启动钩子,启动 bot
|
|||
|
|
├── .env # 合并后的环境变量
|
|||
|
|
├── start.bat # 启动脚本(不变)
|
|||
|
|
└── ...(其余不变)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、三个核心共享模块
|
|||
|
|
|
|||
|
|
### 2.1 Hugo 进程管理器 — `src/lib/hugo.ts`
|
|||
|
|
|
|||
|
|
**解决的问题:** 当前 bot 的 `write-server.js` 和 write 的 `hugo/route.ts` 各自管理 Hugo 进程,互不知道对方状态,可能互相杀进程。
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/hugo.ts
|
|||
|
|
|
|||
|
|
class HugoManager {
|
|||
|
|
private process: ChildProcess | null = null;
|
|||
|
|
private ready = false;
|
|||
|
|
private starting = false;
|
|||
|
|
private healthTimer: NodeJS.Timeout | null = null;
|
|||
|
|
|
|||
|
|
/** 启动 Hugo server,如果已在运行则跳过 */
|
|||
|
|
async start(): Promise<{ ok: boolean; alreadyRunning?: boolean }> { ... }
|
|||
|
|
|
|||
|
|
/** 优雅停止 Hugo */
|
|||
|
|
stop(): void { ... }
|
|||
|
|
|
|||
|
|
/** 当前状态 */
|
|||
|
|
status(): { running: boolean; starting: boolean } { ... }
|
|||
|
|
|
|||
|
|
/** 确保 Hugo 在运行,不在则启动并等待就绪 */
|
|||
|
|
async ensureRunning(): Promise<boolean> { ... }
|
|||
|
|
|
|||
|
|
/** 每 30 秒检查一次 Hugo 是否还活着,死了自动重启 */
|
|||
|
|
private startHealthCheck(): void { ... }
|
|||
|
|
|
|||
|
|
/** 用 HTTP 请求检查 Hugo 是否响应 */
|
|||
|
|
private async ping(): Promise<boolean> { ... }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 全局单例
|
|||
|
|
export const hugo = new HugoManager();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
关键设计点:
|
|||
|
|
- `start()` 内部有互斥锁,两个并发调用不会重复启动
|
|||
|
|
- 健康检查每 30 秒 ping 一次 Hugo 的 localhost:1313,失败则标记 ready=false
|
|||
|
|
- Web 端的 `/api/hugo/route.ts` 改为调用 `hugo.start()` / `hugo.stop()` / `hugo.status()`
|
|||
|
|
- Bot 的 `/start` 命令改为调用 `hugo.ensureRunning()`
|
|||
|
|
- 启动参数统一为 `hugo server --bind 0.0.0.0 --port 1313 --disableFastRender --noHTTPCache`
|
|||
|
|
|
|||
|
|
### 2.2 Git 部署队列 — `src/lib/git.ts`
|
|||
|
|
|
|||
|
|
**解决的问题:** 当前 deploy route 没有并发控制,两个同时触发的 deploy 会导致 rebase 冲突或仓库损坏。
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/lib/git.ts
|
|||
|
|
|
|||
|
|
class GitDeployQueue {
|
|||
|
|
private queue: Array<{
|
|||
|
|
message: string;
|
|||
|
|
resolve: (result: DeployResult) => void;
|
|||
|
|
reject: (err: Error) => void;
|
|||
|
|
}> = [];
|
|||
|
|
private running = false;
|
|||
|
|
|
|||
|
|
/** 入队一次部署任务,返回 Promise 等待结果 */
|
|||
|
|
async enqueue(commitMessage: string): Promise<DeployResult> { ... }
|
|||
|
|
|
|||
|
|
/** 实际执行 git add → commit → pull --rebase → push */
|
|||
|
|
private async execute(message: string): Promise<DeployResult> { ... }
|
|||
|
|
|
|||
|
|
/** 处理队列中的下一个任务 */
|
|||
|
|
private async next(): Promise<void> { ... }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export interface DeployResult {
|
|||
|
|
success: boolean;
|
|||
|
|
output?: string;
|
|||
|
|
error?: string;
|
|||
|
|
conflict?: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export const deployQueue = new GitDeployQueue();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
关键设计点:
|
|||
|
|
- 同一时间只有一个 git 操作在执行,其余排队等待
|
|||
|
|
- Web 端的 `/api/deploy/route.ts` 改为调用 `deployQueue.enqueue(title)`
|
|||
|
|
- Bot 的 `/publish` 保存文章后也调用 `deployQueue.enqueue()` 触发自动部署
|
|||
|
|
- 返回 `{ conflict: true }` 时前端提示用户手动解决
|
|||
|
|
|
|||
|
|
### 2.3 Bot 模块 — `src/lib/bot/`
|
|||
|
|
|
|||
|
|
**解决的问题:** bot 作为独立进程通过 HTTP 调 write API,引入了不必要的网络依赖和状态不一致。
|
|||
|
|
|
|||
|
|
整合后 bot 是 Next.js 进程内的一个模块,handler 直接调用 `src/lib/posts.ts`、`src/lib/rssapi.ts` 等函数,不再走 HTTP。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、Bot 模块详细设计
|
|||
|
|
|
|||
|
|
### 3.1 启动方式 — `src/instrumentation.ts`
|
|||
|
|
|
|||
|
|
Next.js 13+ 支持 `instrumentation.ts`,在服务端进程启动时执行一次:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// src/instrumentation.ts
|
|||
|
|
export async function register() {
|
|||
|
|
// 仅在 Node.js 运行时执行(不是 Edge Runtime)
|
|||
|
|
if (process.env.NEXT_RUNTIME === "nodejs") {
|
|||
|
|
const { startBot } = await import("@/lib/bot");
|
|||
|
|
startBot();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
需要在 `next.config.ts` 中启用:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const nextConfig: NextConfig = {
|
|||
|
|
devIndicators: false,
|
|||
|
|
experimental: {
|
|||
|
|
instrumentationHook: true, // Next.js 15+ 已默认开启,16 可能不需要
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 Telegram API 封装 — `src/lib/bot/tg.ts`
|
|||
|
|
|
|||
|
|
从 bot 的 `tg-core.js` 移植,改为 TypeScript:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const API = `https://api.telegram.org/bot${TG_TOKEN}`;
|
|||
|
|
|
|||
|
|
export async function tg<T = unknown>(method: string, body: Record<string, unknown>): Promise<TgResponse<T>> {
|
|||
|
|
const res = await fetch(`${API}/${method}`, {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify(body),
|
|||
|
|
});
|
|||
|
|
return res.json();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function allowed(chatId: number): boolean {
|
|||
|
|
if (ALLOWED_IDS.length === 0) return true;
|
|||
|
|
return ALLOWED_IDS.includes(chatId);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function sendMessage(chatId: number, text: string, opts?: Record<string, unknown>): Promise<void> {
|
|||
|
|
// 先尝试 HTML 解析,失败则降级纯文本
|
|||
|
|
try {
|
|||
|
|
await tg("sendMessage", { chat_id: chatId, text, parse_mode: "HTML", disable_web_page_preview: true, ...opts });
|
|||
|
|
} catch {
|
|||
|
|
await tg("sendMessage", { chat_id: chatId, text, ...opts });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 写作会话 — `src/lib/bot/sessions.ts`
|
|||
|
|
|
|||
|
|
从 bot 的 `sessions.js` 移植,改为直接调用 `src/lib/posts.ts`:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
interface Session {
|
|||
|
|
title: string;
|
|||
|
|
slug: string;
|
|||
|
|
content: string;
|
|||
|
|
categories: string[];
|
|||
|
|
tags: string[];
|
|||
|
|
author: string;
|
|||
|
|
draft: boolean;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const sessions = new Map<number, Session>();
|
|||
|
|
|
|||
|
|
export function getSession(chatId: number): Session { ... }
|
|||
|
|
export function clearSession(chatId: number): void { ... }
|
|||
|
|
|
|||
|
|
// 关键变化:直接调用 lib/posts.ts,不再走 HTTP
|
|||
|
|
export async function saveSession(s: Session, isDraft: boolean): Promise<void> {
|
|||
|
|
const { createPost, updatePost, getPost } = await import("@/lib/posts");
|
|||
|
|
const existing = getPost(s.slug);
|
|||
|
|
if (existing) {
|
|||
|
|
updatePost(s.slug, {
|
|||
|
|
title: s.title,
|
|||
|
|
date: new Date().toISOString().slice(0, 10),
|
|||
|
|
draft: isDraft,
|
|||
|
|
categories: s.categories,
|
|||
|
|
tags: s.tags,
|
|||
|
|
author: s.author,
|
|||
|
|
}, s.content);
|
|||
|
|
} else {
|
|||
|
|
createPost({
|
|||
|
|
title: s.title,
|
|||
|
|
slug: s.slug,
|
|||
|
|
date: new Date().toISOString().slice(0, 10),
|
|||
|
|
draft: isDraft,
|
|||
|
|
categories: s.categories,
|
|||
|
|
tags: s.tags,
|
|||
|
|
author: s.author,
|
|||
|
|
}, s.content);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.4 命令处理器 — `src/lib/bot/handlers.ts`
|
|||
|
|
|
|||
|
|
从 bot 的 `tg-handlers.js` 移植,核心变化:
|
|||
|
|
|
|||
|
|
| 原来(HTTP 调 write API) | 现在(直接调 lib) |
|
|||
|
|
|---|---|
|
|||
|
|
| `fetch("http://127.0.0.1:8016/api/posts/" + slug)` | `getPost(slug)` |
|
|||
|
|
| `fetch("http://127.0.0.1:8016/api/posts", { method: "POST" })` | `createPost(frontMatter, content)` |
|
|||
|
|
| `fetch("http://127.0.0.1:8016/api/posts/" + slug, { method: "DELETE" })` | `moveToRecycle(slug, dirPath, title)` |
|
|||
|
|
| `fetch("http://127.0.0.1:8016/api/rss/links")` | `getLinks(true)` |
|
|||
|
|
| `fetch("http://127.0.0.1:8016/api/rss/feeds")` | `getFeeds()` |
|
|||
|
|
| `fetch("http://127.0.0.1:8016/api/deploy", ...)` | `deployQueue.enqueue(title)` |
|
|||
|
|
|
|||
|
|
另外,`/publish` 命令现在会自动触发 git 部署:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// /publish handler
|
|||
|
|
async function handlePublish(chatId: number) {
|
|||
|
|
const s = sessions.get(chatId);
|
|||
|
|
await saveSession(s, false); // 保存文章
|
|||
|
|
const result = await deployQueue.enqueue(`发布: ${s.title}`); // 自动部署
|
|||
|
|
if (result.success) {
|
|||
|
|
sendMessage(chatId, `✅ 已发布并部署: ${s.title}`);
|
|||
|
|
} else if (result.conflict) {
|
|||
|
|
sendMessage(chatId, `⚠️ 已保存但部署冲突,请手动解决`);
|
|||
|
|
} else {
|
|||
|
|
sendMessage(chatId, `❌ 部署失败: ${result.error}`);
|
|||
|
|
}
|
|||
|
|
clearSession(chatId);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.5 长轮询 — `src/lib/bot/poll.ts`
|
|||
|
|
|
|||
|
|
从 bot 的 `tg-poll.js` 移植,修复两个问题:
|
|||
|
|
|
|||
|
|
1. **添加指数退避**:出错后等待 3s → 6s → 12s → 最大 60s,避免 Telegram API 限流
|
|||
|
|
2. **添加正常间隔**:`getUpdates` 返回后等待 100ms 再发起下一次,避免空转
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export async function startPolling(): Promise<void> {
|
|||
|
|
let lastOffset = 0;
|
|||
|
|
let backoff = 3000;
|
|||
|
|
|
|||
|
|
async function poll() {
|
|||
|
|
if (!TG_TOKEN) return;
|
|||
|
|
try {
|
|||
|
|
const data = await tg("getUpdates", {
|
|||
|
|
offset: lastOffset + 1,
|
|||
|
|
timeout: 30,
|
|||
|
|
allowed_updates: ["message", "callback_query"],
|
|||
|
|
});
|
|||
|
|
if (data.ok && data.result) {
|
|||
|
|
for (const update of data.result) {
|
|||
|
|
lastOffset = update.update_id;
|
|||
|
|
// ... 处理消息(逻辑不变)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
backoff = 3000; // 成功则重置退避
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error("[bot:poll]", e.message);
|
|||
|
|
await sleep(backoff);
|
|||
|
|
backoff = Math.min(backoff * 2, 60000);
|
|||
|
|
}
|
|||
|
|
await sleep(100); // 正常间隔
|
|||
|
|
setImmediate(poll);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
poll();
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.6 图片处理改进
|
|||
|
|
|
|||
|
|
当前 bot 直接引用 Telegram 临时 URL,改为下载到本地:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// 在 poll.ts 中处理 photo 消息
|
|||
|
|
if (msg.photo && sessions.has(chatId)) {
|
|||
|
|
const s = getSession(chatId);
|
|||
|
|
const largestPhoto = msg.photo[msg.photo.length - 1];
|
|||
|
|
const fileRes = await tg("getFile", { file_id: largestPhoto.file_id });
|
|||
|
|
if (fileRes.ok) {
|
|||
|
|
const filePath = fileRes.result.file_path;
|
|||
|
|
const fileUrl = `https://api.telegram.org/file/bot${TG_TOKEN}/${filePath}`;
|
|||
|
|
const imgBuffer = await fetch(fileUrl).then(r => r.arrayBuffer());
|
|||
|
|
|
|||
|
|
// 保存到 static/upload/ 目录
|
|||
|
|
const filename = `${Date.now()}-${largestPhoto.file_id.slice(0, 8)}.jpg`;
|
|||
|
|
const savePath = path.join(STATIC_DIR, "upload", filename);
|
|||
|
|
fs.writeFileSync(savePath, Buffer.from(imgBuffer));
|
|||
|
|
|
|||
|
|
// 引用本地路径
|
|||
|
|
const caption = msg.caption || "";
|
|||
|
|
s.content += `\n\n`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、Web 端 API 路由适配
|
|||
|
|
|
|||
|
|
### 4.1 `/api/hugo/route.ts` — 改用 HugoManager
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { hugo } from "@/lib/hugo";
|
|||
|
|
|
|||
|
|
export async function GET() {
|
|||
|
|
const result = await hugo.ensureRunning();
|
|||
|
|
return NextResponse.json({ running: result, url: "http://localhost:1313" });
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4.2 `/api/deploy/route.ts` — 改用 GitDeployQueue
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
import { deployQueue } from "@/lib/git";
|
|||
|
|
|
|||
|
|
export async function POST(request: NextRequest) {
|
|||
|
|
const { title } = await request.json();
|
|||
|
|
const result = await deployQueue.enqueue(`发布: ${title}`);
|
|||
|
|
return NextResponse.json(result);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、配置合并
|
|||
|
|
|
|||
|
|
将 bot 的 `.env` 字段合入 write 的 `.env`:
|
|||
|
|
|
|||
|
|
```env
|
|||
|
|
# ---- Write 原有 ----
|
|||
|
|
RSS_API_BASE=https://api.usj.cc
|
|||
|
|
RSS_API_TOKEN=xxx
|
|||
|
|
WECHAT_APP_ID=xxx
|
|||
|
|
WECHAT_APP_SECRET=xxx
|
|||
|
|
DEEPSEEK_API_KEY=xxx
|
|||
|
|
ARTALK_SERVER=https://artalk.usj.cc
|
|||
|
|
|
|||
|
|
# ---- Bot 合并过来 ----
|
|||
|
|
TG_BOT_TOKEN=xxx
|
|||
|
|
TG_ALLOWED_CHAT_IDS=xxx
|
|||
|
|
PREFER_IFACE=WLAN
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`src/lib/config.ts` 新增:
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
export const TG_BOT_TOKEN = process.env.TG_BOT_TOKEN || "";
|
|||
|
|
export const TG_ALLOWED_IDS = (process.env.TG_ALLOWED_CHAT_IDS || "")
|
|||
|
|
.split(",").map(s => s.trim()).filter(Boolean).map(Number);
|
|||
|
|
export const PREFER_IFACE = process.env.PREFER_IFACE || "WLAN";
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、Bot 管理页面(替代 admin-panel)
|
|||
|
|
|
|||
|
|
将 bot 的 `admin-panel.js`(独立 HTTP 服务器)改为 Next.js 页面 `/bot/page.tsx`:
|
|||
|
|
|
|||
|
|
- 显示 Telegram Bot 连接状态(是否在轮询、最后收到消息的时间)
|
|||
|
|
- 显示 Hugo 进程状态
|
|||
|
|
- 显示最近的 bot 操作日志(最近 10 条)
|
|||
|
|
- 提供重启 bot 的按钮
|
|||
|
|
|
|||
|
|
对应 API 路由 `/api/bot/status/route.ts` 和 `/api/bot/restart/route.ts`。
|
|||
|
|
|
|||
|
|
这样 bot 的管理界面和 write 的其他页面共用同一个 Next.js 服务器,不再需要单独的 8017 端口。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、实施步骤
|
|||
|
|
|
|||
|
|
按依赖关系排序:
|
|||
|
|
|
|||
|
|
### 第一步:基础设施(无现有代码改动)
|
|||
|
|
1. 创建 `src/lib/hugo.ts` — HugoManager 单例
|
|||
|
|
2. 创建 `src/lib/git.ts` — GitDeployQueue
|
|||
|
|
3. 更新 `src/lib/config.ts` — 合并 bot 环境变量
|
|||
|
|
4. 更新 `.env` — 合并 bot 配置项
|
|||
|
|
|
|||
|
|
### 第二步:移植 Bot 模块
|
|||
|
|
5. 创建 `src/lib/bot/helpers.ts` — 工具函数
|
|||
|
|
6. 创建 `src/lib/bot/config.ts` — bot 专用配置
|
|||
|
|
7. 创建 `src/lib/bot/tg.ts` — Telegram API 封装
|
|||
|
|
8. 创建 `src/lib/bot/sessions.ts` — 会话管理
|
|||
|
|
9. 创建 `src/lib/bot/handlers.ts` — 命令处理器(直接调 lib)
|
|||
|
|
10. 创建 `src/lib/bot/poll.ts` — 长轮询(带退避)
|
|||
|
|
11. 创建 `src/lib/bot/index.ts` — 启动入口
|
|||
|
|
|
|||
|
|
### 第三步:Next.js 集成
|
|||
|
|
12. 创建 `src/instrumentation.ts` — 服务端启动钩子
|
|||
|
|
13. 更新 `next.config.ts` — 确保 instrumentation 启用
|
|||
|
|
|
|||
|
|
### 第四步:适配现有 API 路由
|
|||
|
|
14. 改造 `src/app/api/hugo/route.ts` — 使用 HugoManager
|
|||
|
|
15. 改造 `src/app/api/deploy/route.ts` — 使用 GitDeployQueue
|
|||
|
|
|
|||
|
|
### 第五步:新增 Bot 管理页面
|
|||
|
|
16. 创建 `src/app/api/bot/status/route.ts`
|
|||
|
|
17. 创建 `src/app/api/bot/restart/route.ts`
|
|||
|
|
18. 创建 `src/app/bot/page.tsx` — Bot 管理 UI
|
|||
|
|
|
|||
|
|
### 第六步:清理
|
|||
|
|
19. 删除 `bot/` 目录(或归档)
|
|||
|
|
20. 更新 `start.bat` — 不再需要分别启动两个进程
|
|||
|
|
21. 更新 `.gitignore` — 移除 bot 相关条目
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、解决的问题清单
|
|||
|
|
|
|||
|
|
| 原问题 | 解决方式 |
|
|||
|
|
|---|---|
|
|||
|
|
| 两个进程各自操作同一份文件系统 | 合并为一个进程,所有文件操作走同一层 lib |
|
|||
|
|
| Hugo 两边各管各的 | HugoManager 单例,一个进程只启动一个 Hugo |
|
|||
|
|
| Git deploy 无并发控制 | GitDeployQueue 串行执行 |
|
|||
|
|
| bot 调 write API 走 HTTP 网络 | 直接调 lib 函数,零网络开销 |
|
|||
|
|
| bot 的 writeReady 不会自动恢复 | HugoManager 带健康检查 + 自动重启 |
|
|||
|
|
| 长轮询无退避策略 | 指数退避 3s→60s |
|
|||
|
|
| Telegram 图片用临时 URL | 下载到 static/upload/ 本地引用 |
|
|||
|
|
| bot 的 admin-panel 单独占一个端口 | 改为 Next.js 内页面 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 九、不变的部分
|
|||
|
|
|
|||
|
|
以下保持不变,不做改动:
|
|||
|
|
- `src/app/` 下所有页面(前端 UI)
|
|||
|
|
- `src/components/` 下所有组件
|
|||
|
|
- `src/lib/posts.ts` 的核心逻辑(仅改为在 handler 中直接调用)
|
|||
|
|
- `src/lib/ai.ts`、`src/lib/artalk.ts`、`src/lib/recycle.ts`、`src/lib/rssapi.ts`
|
|||
|
|
- `hugo.toml`、`content/`、`themes/`、`static/`、`data/`
|
|||
|
|
- GitHub Actions workflows 和 `scripts/` 目录
|
|||
|
|
- `package.json` 的根目录脚本依赖
|