8da5f40c9f
多平台社交自动化 MCP 服务,首批支持小红书。 - 13 个 MCP 工具:登录管理、内容浏览、发布、互动 - 13 个 REST API 端点,支持 Bearer token 认证和限流 - BrowserManager:串行队列、背压、崩溃恢复 - Cookie 持久化:原子写入、0600 权限 - 安全:DNS rebinding 防御、错误脱敏、深层日志 redact - Docker 部署支持 - 28 个单元测试全部通过
912 lines
38 KiB
Markdown
912 lines
38 KiB
Markdown
# Social Auto Hub — 多平台社交自动化 MCP 服务
|
||
|
||
## 一、项目定位
|
||
|
||
通过浏览器自动化,让 AI 助手(Claude 等)能操控多个社交平台。
|
||
首批支持:**小红书**。后续按需扩展小黑盒、B站、微博等。
|
||
|
||
---
|
||
|
||
## 二、技术栈选型
|
||
|
||
| 组件 | 选择 | 理由 |
|
||
|------|------|------|
|
||
| 语言 | **TypeScript 5.x** | 浏览器自动化生态最强,MCP SDK 是官方参考实现 |
|
||
| 运行时 | **Node.js 22 LTS** | 当前 LTS,Node 20 即将进入 maintenance |
|
||
| 浏览器自动化 | **rebrowser-playwright** | Playwright 的 drop-in 替代,内置反自动化检测补丁,活跃维护 |
|
||
| MCP SDK | **@modelcontextprotocol/sdk ^1.27** | 官方 TypeScript 参考实现,锁定 v1.x 稳定版 |
|
||
| HTTP 服务 | **Express ^4** | MCP SDK 原生支持 Express 集成 |
|
||
| 包管理 | **pnpm** | 快,磁盘占用小 |
|
||
| 构建 | **tsup** | 零配置 TS 打包,基于 esbuild |
|
||
| 日志 | **pino ^9** | 高性能结构化日志,内置 redact 脱敏 |
|
||
| 校验 | **zod ^3.25** | 运行时类型校验,MCP SDK 原生支持。**不用 v4**(与 SDK 有兼容问题) |
|
||
| 容器化 | **Docker** | 含 Playwright 浏览器 |
|
||
|
||
### 为什么用 rebrowser-playwright 而不是 playwright-extra?
|
||
|
||
`playwright-extra` 最后发布 4.3.6 是 3 年前,已停止维护,随时会与新版 Playwright 不兼容。
|
||
`rebrowser-playwright` 是 Playwright 的 patched fork(2026.2 仍在更新),直接 `npm install rebrowser-playwright` 即可,
|
||
代码中 `import { chromium } from 'rebrowser-playwright'` 替代原 `playwright`,零改动。
|
||
它在底层修复 CDP 泄漏等检测点,比 JS 层面的 stealth 脚本更彻底。
|
||
|
||
### 为什么锁定 zod ^3.25?
|
||
|
||
MCP SDK v1.x 的 `server.tool()` 内部依赖 zod v3 的 schema 结构。
|
||
zod v4 有 breaking change(`.describe()` 丢失、`_parse` 不兼容),
|
||
参见 [Issue #925](https://github.com/modelcontextprotocol/typescript-sdk/issues/925)。
|
||
|
||
---
|
||
|
||
## 三、架构设计:插件式多平台
|
||
|
||
### 3.1 设计原则
|
||
|
||
**不做统一 Platform 接口。** 各平台业务差异大(小红书的笔记 vs B站的弹幕 vs 微博的转发),强行抽象只会产生大量 `NotSupported`。
|
||
|
||
真正共享的是**基础设施**,业务逻辑各平台独立:
|
||
|
||
```
|
||
共享基础设施 各平台独立
|
||
────────── ──────────
|
||
BrowserManager MCP 工具定义 + zod schema
|
||
CookieStore actions(业务逻辑层)
|
||
Config / Logger 页面操作逻辑
|
||
MCP Server 框架 CSS 选择器
|
||
Express + 中间件 业务类型定义
|
||
错误处理包装 REST API handler
|
||
```
|
||
|
||
### 3.2 平台插件契约
|
||
|
||
每个平台导出一个 `PlatformPlugin`,把自己的 MCP 工具注册进去:
|
||
|
||
```typescript
|
||
export interface PlatformPlugin {
|
||
name: string;
|
||
registerTools(server: McpServer, browser: BrowserManager): void;
|
||
registerRoutes?(router: express.Router, browser: BrowserManager): void;
|
||
// 生命周期钩子(可选,第二个平台接入时再强制要求)
|
||
init?(): Promise<void>;
|
||
shutdown?(): Promise<void>;
|
||
healthCheck?(): Promise<{ healthy: boolean; message?: string }>;
|
||
}
|
||
```
|
||
|
||
新增平台只需要:
|
||
1. 在 `src/platforms/` 下新建目录
|
||
2. 实现 `PlatformPlugin`
|
||
3. 在 `src/index.ts` 中 import 并注册
|
||
|
||
> **Review 备注**:`init/shutdown/healthCheck` 当前为可选钩子。
|
||
> Phase 1 只有小红书一个平台,暂不强制。等第二个平台接入时评估是否改为必选。
|
||
|
||
### 3.3 平台内部分层
|
||
|
||
每个平台内部统一采用 **actions 层** 分离业务逻辑和 handler:
|
||
|
||
```
|
||
platforms/xiaohongshu/
|
||
├── index.ts # PlatformPlugin: 注册 MCP 工具,调用 actions
|
||
├── actions.ts # 业务逻辑层(纯函数,接收 Page 返回数据)
|
||
├── selectors.ts # CSS 选择器常量
|
||
├── schemas.ts # zod schema(MCP 工具参数)
|
||
├── types.ts # 业务类型
|
||
├── login.ts # 登录操作(页面交互)
|
||
├── search.ts # 搜索操作
|
||
├── ... # 其他操作
|
||
```
|
||
|
||
**actions.ts** 是核心:MCP handler 和 REST handler 都调用 actions 中的函数,不重复业务逻辑。
|
||
|
||
```typescript
|
||
// actions.ts — 纯业务逻辑,不关心 MCP/REST
|
||
export async function searchFeeds(page: Page, keyword: string, filters?: FilterOption): Promise<Feed[]> { ... }
|
||
export async function checkLoginStatus(page: Page): Promise<LoginStatus> { ... }
|
||
|
||
// index.ts — MCP handler 只做参数解析 + 调用 action + 格式化输出
|
||
server.tool('xhs_search', '搜索小红书笔记', SearchSchema, async (args) => {
|
||
return browser.withPage('xiaohongshu', async (page) => {
|
||
const feeds = await searchFeeds(page, args.keyword, args.filters);
|
||
return { content: [{ type: 'text', text: JSON.stringify(feeds) }] };
|
||
});
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 四、项目结构
|
||
|
||
```
|
||
social-auto-hub/
|
||
├── package.json
|
||
├── tsconfig.json
|
||
├── tsup.config.ts
|
||
├── .env.example
|
||
├── .gitignore
|
||
├── CLAUDE.md
|
||
├── Dockerfile
|
||
│
|
||
├── src/
|
||
│ ├── index.ts # 入口:配置、注册插件、启动、优雅关闭(drain→browser→http→log)
|
||
│ │
|
||
│ ├── server/
|
||
│ │ ├── app.ts # AppServer: Express + MCP 生命周期
|
||
│ │ └── middleware.ts # DNS rebinding 防御 + 错误处理 + 优雅关闭 503(不加 CORS)
|
||
│ │
|
||
│ ├── browser/
|
||
│ │ └── manager.ts # BrowserManager: 浏览器 + Context + 串行队列 + 背压 + 启动锁
|
||
│ │
|
||
│ ├── cookie/
|
||
│ │ └── store.ts # CookieStore: 按平台隔离,文件权限 0600,原子写入
|
||
│ │
|
||
│ ├── config/
|
||
│ │ └── index.ts # 全局配置(环境变量,默认 127.0.0.1)
|
||
│ │
|
||
│ ├── utils/
|
||
│ │ ├── logger.ts # pino 日志(深层 redact 脱敏 + 自定义错误序列化)
|
||
│ │ ├── errors.ts # 错误分类 + 消息脱敏 + withErrorHandling 包装
|
||
│ │ └── downloader.ts # 图片下载 + 媒体路径校验
|
||
│ │
|
||
│ └── platforms/
|
||
│ └── xiaohongshu/
|
||
│ ├── index.ts # PlatformPlugin 注册
|
||
│ ├── actions.ts # 业务逻辑层(MCP/REST 共享)
|
||
│ ├── selectors.ts # CSS 选择器常量
|
||
│ ├── types.ts # 小红书业务类型
|
||
│ ├── schemas.ts # MCP 工具参数 zod schema
|
||
│ ├── login.ts # 登录(特殊 Page 生命周期)
|
||
│ ├── search.ts # 搜索
|
||
│ ├── feeds.ts # Feed 列表
|
||
│ ├── feed-detail.ts # 笔记详情 + 评论
|
||
│ ├── publish.ts # 图文发布
|
||
│ ├── publish-video.ts # 视频发布
|
||
│ ├── comment.ts # 评论 + 回复
|
||
│ ├── interaction.ts # 点赞 / 收藏
|
||
│ └── user-profile.ts # 用户主页
|
||
│
|
||
└── deploy/
|
||
└── docker-compose.yml
|
||
```
|
||
|
||
---
|
||
|
||
## 五、小红书 MCP 工具清单(13 个)
|
||
|
||
### 5.1 登录管理(3 个)
|
||
|
||
| MCP 工具名 | 说明 | 读/写 | 参数 |
|
||
|-----------|------|-------|------|
|
||
| `xhs_check_login` | 检查登录状态 | 只读 | 无 |
|
||
| `xhs_get_login_qrcode` | 获取登录二维码图片 | 只读 | 无 |
|
||
| `xhs_delete_cookies` | 删除 Cookie,重置登录 | 写(破坏性) | 无 |
|
||
|
||
### 5.2 内容浏览(4 个)
|
||
|
||
| MCP 工具名 | 说明 | 读/写 | 参数 |
|
||
|-----------|------|-------|------|
|
||
| `xhs_list_feeds` | 获取首页推荐 Feed 列表 | 只读 | 无 |
|
||
| `xhs_search` | 搜索笔记 | 只读 | `keyword`, `filters?`(排序/类型/时间/范围/位置) |
|
||
| `xhs_get_feed_detail` | 获取笔记详情 + 评论 | 只读 | `feed_id`, `xsec_token`, `load_all_comments?` |
|
||
| `xhs_get_user_profile` | 获取用户主页信息 | 只读 | `user_id`, `xsec_token` |
|
||
|
||
> **注意**:`xhs_get_feed_detail` 相比参考项目精简了参数。
|
||
> `scroll_speed`、`click_more_replies`、`reply_limit` 是浏览器实现细节,
|
||
> 不暴露给 AI,改为服务端内部配置(在 config 或 actions 层处理)。
|
||
|
||
### 5.3 内容发布(2 个)
|
||
|
||
| MCP 工具名 | 说明 | 读/写 | 参数 |
|
||
|-----------|------|-------|------|
|
||
| `xhs_publish_image` | 发布图文笔记 | 写 | `title`, `content`, `images[]`, `tags?[]`, `schedule_at?`, `is_original?`, `visibility?` |
|
||
| `xhs_publish_video` | 发布视频笔记 | 写 | `title`, `content`, `video`, `tags?[]`, `schedule_at?`, `visibility?` |
|
||
|
||
### 5.4 互动操作(4 个)
|
||
|
||
| MCP 工具名 | 说明 | 读/写 | 参数 |
|
||
|-----------|------|-------|------|
|
||
| `xhs_post_comment` | 发表评论 | 写 | `feed_id`, `xsec_token`, `content` |
|
||
| `xhs_reply_comment` | 回复评论 | 写 | `feed_id`, `xsec_token`, `comment_id?`, `user_id?`, `content` |
|
||
| `xhs_like` | 点赞/取消点赞 | 写 | `feed_id`, `xsec_token`, `unlike?` |
|
||
| `xhs_favorite` | 收藏/取消收藏 | 写 | `feed_id`, `xsec_token`, `unfavorite?` |
|
||
|
||
### 5.5 REST API(Phase 5,可选)
|
||
|
||
```
|
||
GET /api/xhs/login/status POST /api/xhs/publish/image
|
||
GET /api/xhs/login/qrcode POST /api/xhs/publish/video
|
||
DELETE /api/xhs/login/cookies POST /api/xhs/comment
|
||
GET /api/xhs/feeds POST /api/xhs/comment/reply
|
||
POST /api/xhs/search POST /api/xhs/like
|
||
POST /api/xhs/feeds/detail POST /api/xhs/favorite
|
||
POST /api/xhs/user/profile
|
||
```
|
||
|
||
---
|
||
|
||
## 六、核心模块设计
|
||
|
||
### 6.1 BrowserManager
|
||
|
||
```typescript
|
||
import { Browser, BrowserContext, Page } from 'rebrowser-playwright';
|
||
|
||
class BrowserManager {
|
||
private browser: Browser | null = null;
|
||
private contexts = new Map<string, BrowserContext>();
|
||
private queues = new Map<string, Promise<void>>(); // per-platform 串行队列
|
||
private queueDepths = new Map<string, number>(); // 队列深度计数
|
||
private launchPromise: Promise<Browser> | null = null; // 启动锁
|
||
|
||
private readonly MAX_QUEUE_DEPTH = 10;
|
||
|
||
// 分级超时:不同操作类型使用不同超时时间
|
||
static readonly OPERATION_TIMEOUTS: Record<string, number> = {
|
||
like: 15_000, // 15s — 快速交互
|
||
comment: 20_000, // 20s
|
||
feed_list: 30_000, // 30s — 页面加载 + 提取
|
||
search: 30_000, // 30s
|
||
feed_detail: 60_000, // 60s — 含滚动加载
|
||
publish: 300_000, // 5min — 上传可能较慢
|
||
login: 300_000, // 5min — 用户交互
|
||
default: 60_000, // 1min — 兜底
|
||
};
|
||
|
||
// 核心方法:串行执行 + 超时控制 + 错误恢复 + 背压
|
||
async withPage<T>(
|
||
platform: string,
|
||
fn: (page: Page) => Promise<T>,
|
||
timeoutMs?: number,
|
||
): Promise<T> {
|
||
// 0. 背压:队列深度限制,防止无限排队
|
||
const depth = this.queueDepths.get(platform) ?? 0;
|
||
if (depth >= this.MAX_QUEUE_DEPTH) {
|
||
throw new Error(`平台 ${platform} 队列已满 (${this.MAX_QUEUE_DEPTH}),请稍后重试`);
|
||
}
|
||
this.queueDepths.set(platform, depth + 1);
|
||
|
||
const effectiveTimeout = timeoutMs ?? BrowserManager.OPERATION_TIMEOUTS.default;
|
||
|
||
// 1. 串行化:同一平台的操作排队执行,避免多 Page 互相干扰
|
||
const prev = this.queues.get(platform) ?? Promise.resolve();
|
||
const task = prev.then(async () => {
|
||
// 2. 检查浏览器存活
|
||
await this.ensureBrowser();
|
||
const ctx = await this.getContext(platform);
|
||
const page = await ctx.newPage();
|
||
// 设置 Playwright 级别超时,确保底层操作也受控
|
||
page.setDefaultTimeout(effectiveTimeout);
|
||
page.setDefaultNavigationTimeout(effectiveTimeout);
|
||
try {
|
||
// 3. 超时控制(带 timer 清理,避免泄漏)
|
||
let timer: ReturnType<typeof setTimeout>;
|
||
const result = await Promise.race([
|
||
fn(page),
|
||
new Promise<never>((_, reject) => {
|
||
timer = setTimeout(
|
||
() => reject(new Error(`操作超时: ${effectiveTimeout}ms`)),
|
||
effectiveTimeout,
|
||
);
|
||
}),
|
||
]);
|
||
clearTimeout(timer!);
|
||
return result;
|
||
} catch (err) {
|
||
// 超时时 fn 可能仍在运行,page.close() 会中断它
|
||
throw err;
|
||
} finally {
|
||
await page.close().catch(() => {});
|
||
}
|
||
});
|
||
this.queues.set(platform, task.then(() => {}, () => {}));
|
||
|
||
// 队列深度计数还原
|
||
task.finally(() => {
|
||
this.queueDepths.set(platform, (this.queueDepths.get(platform) ?? 1) - 1);
|
||
});
|
||
|
||
return task;
|
||
}
|
||
|
||
// 登录专用:返回 Page 不自动关闭,调用方负责生命周期
|
||
// release 必须是幂等的(内部用 released 标志位防止重复调用)
|
||
async acquirePage(platform: string): Promise<{ page: Page; release: () => Promise<void> }> {
|
||
await this.ensureBrowser();
|
||
const ctx = await this.getContext(platform);
|
||
const page = await ctx.newPage();
|
||
|
||
let released = false;
|
||
const release = async () => {
|
||
if (released) return; // 幂等
|
||
released = true;
|
||
await page.close().catch(() => {});
|
||
};
|
||
|
||
// 安全网:最长 5 分钟自动释放,防止 release 忘记调用
|
||
const maxLifetime = setTimeout(() => {
|
||
logger.warn({ platform }, '页面超过最大生命周期,强制释放');
|
||
release();
|
||
}, 5 * 60 * 1000);
|
||
|
||
return {
|
||
page,
|
||
release: async () => {
|
||
clearTimeout(maxLifetime);
|
||
await release();
|
||
},
|
||
};
|
||
}
|
||
|
||
// 检查浏览器存活,崩溃则重建
|
||
// 用 launchPromise 作为启动锁,防止并发调用时启动多个浏览器进程
|
||
private async ensureBrowser(): Promise<Browser> {
|
||
if (this.browser?.isConnected()) return this.browser;
|
||
|
||
if (!this.launchPromise) {
|
||
this.launchPromise = (async () => {
|
||
// 如果 browser 存在但已断开,先清理
|
||
if (this.browser) {
|
||
logger.warn('浏览器连接断开,正在重新启动');
|
||
this.contexts.clear();
|
||
this.browser = null;
|
||
}
|
||
const browser = await chromium.launch({ headless: config.headless });
|
||
// 监听断开事件,主动清理状态
|
||
browser.on('disconnected', () => {
|
||
logger.error('浏览器进程意外断开');
|
||
this.browser = null;
|
||
this.contexts.clear();
|
||
});
|
||
this.browser = browser;
|
||
return browser;
|
||
})().finally(() => {
|
||
this.launchPromise = null;
|
||
});
|
||
}
|
||
|
||
return this.launchPromise;
|
||
}
|
||
|
||
private async getContext(platform: string): Promise<BrowserContext>;
|
||
async saveCookies(platform: string): Promise<void>;
|
||
|
||
// 等待所有队列排空(用于优雅关闭)
|
||
async drain(): Promise<void> {
|
||
await Promise.allSettled(Array.from(this.queues.values()));
|
||
}
|
||
|
||
async close(): Promise<void>;
|
||
}
|
||
```
|
||
|
||
关键设计点:
|
||
- **per-platform 串行队列**:同一平台操作排队,不同平台可并行
|
||
- **队列背压**:`MAX_QUEUE_DEPTH = 10`,超出直接拒绝,防止无限排队
|
||
- **分级超时**:按操作类型设置不同超时(like 15s vs publish 5min),避免快操作被慢操作的默认超时卡住
|
||
- **超时 timer 清理**:`Promise.race` 成功后 `clearTimeout`,避免 timer 泄漏和 `unhandledRejection`
|
||
- **Playwright 级别超时**:`page.setDefaultTimeout()` 确保底层操作也受控,超时时 `page.close()` 会中断正在运行的 fn
|
||
- **启动锁**:`launchPromise` 防止并发调用 `ensureBrowser()` 时启动多个浏览器进程
|
||
- **崩溃恢复**:`browser.on('disconnected')` 主动清理状态,下次调用自动重建
|
||
- **登录特殊处理**:`acquirePage()` 返回幂等的 `release` 函数 + 5 分钟安全网超时
|
||
- **优雅关闭**:`drain()` 等待所有队列排空后再关闭浏览器
|
||
|
||
### 6.2 登录流程(特殊 Page 生命周期)
|
||
|
||
```typescript
|
||
// login.ts — 扫码登录不能用 withPage(需要页面保持打开等用户扫码)
|
||
export async function getLoginQRCode(browser: BrowserManager): Promise<QRCodeResult> {
|
||
const { page, release } = await browser.acquirePage('xiaohongshu');
|
||
|
||
try {
|
||
await page.goto('https://www.xiaohongshu.com/explore');
|
||
// ... 检查是否已登录 ...
|
||
const qrcodeData = await page.getAttribute('.login-container .qrcode-img', 'src');
|
||
|
||
// 后台等待扫码完成,然后保存 Cookie 并释放 Page
|
||
// 必须 catch 错误,fire-and-forget 不能让异常逃逸
|
||
waitForLoginAndRelease(page, browser, release).catch(err => {
|
||
logger.error({ err }, '登录等待流程异常');
|
||
});
|
||
|
||
return { qrcodeData, alreadyLoggedIn: false, timeout: '4m' };
|
||
} catch (err) {
|
||
await release();
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
async function waitForLoginAndRelease(page: Page, browser: BrowserManager, release: () => Promise<void>) {
|
||
// release 已经是幂等的(BrowserManager.acquirePage 保证),
|
||
// 所以即使 setTimeout 和 finally 都触发,也不会重复释放。
|
||
const timeout = setTimeout(() => release(), 4 * 60 * 1000);
|
||
try {
|
||
await page.waitForSelector('.user .link-wrapper .channel', { timeout: 4 * 60 * 1000 });
|
||
await browser.saveCookies('xiaohongshu');
|
||
} catch {} finally {
|
||
clearTimeout(timeout);
|
||
await release();
|
||
}
|
||
}
|
||
```
|
||
|
||
> **Review 修正**:原设计中 `release()` 没有幂等保护,`setTimeout` 和 `finally` 可能双重触发。
|
||
> 现在 `acquirePage` 内部保证 `release` 幂等(`released` 标志位),彻底消除该竞态。
|
||
> 同时 `waitForLoginAndRelease` 的 fire-and-forget 调用必须 `.catch()` 防止异常逃逸。
|
||
|
||
### 6.3 CookieStore
|
||
|
||
```typescript
|
||
class CookieStore {
|
||
// 目录: ~/.social-auto-hub/<platform>/
|
||
// 文件权限: 0o600(仅 owner 可读写)
|
||
// 目录权限: 0o700
|
||
|
||
getPath(platform: string): string;
|
||
async load(platform: string): Promise<StorageState | null>;
|
||
|
||
// 原子写入:先写临时文件再 rename,防止崩溃导致文件损坏
|
||
async save(platform: string, state: StorageState): Promise<void> {
|
||
const filePath = this.getPath(platform);
|
||
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
||
await fs.writeFile(tmpPath, JSON.stringify(state), { mode: 0o600 });
|
||
await fs.rename(tmpPath, filePath);
|
||
}
|
||
|
||
async delete(platform: string): Promise<void>;
|
||
}
|
||
```
|
||
|
||
> **Review 补充**:Cookie 加密(AES-256-GCM)暂不实现。
|
||
> 当前安全模型:文件权限 0600 + 监听 127.0.0.1,对本地自动化工具足够。
|
||
> 如果未来需要更高安全级别(多用户共享机器),再引入加密 + OS keychain 集成。
|
||
|
||
### 6.4 统一错误处理 + 错误分类
|
||
|
||
浏览器自动化的错误类型多样,AI 助手需要根据错误类型决定下一步操作(重试?重新登录?报告失败?)。
|
||
因此引入**错误分类体系**,让 MCP 响应携带结构化的错误信息。
|
||
|
||
```typescript
|
||
// utils/errors.ts
|
||
|
||
// 错误分类枚举
|
||
enum ErrorCategory {
|
||
TIMEOUT = 'TIMEOUT', // 操作超时
|
||
AUTH_REQUIRED = 'AUTH_REQUIRED', // 需要登录
|
||
SELECTOR_NOT_FOUND = 'SELECTOR_NOT_FOUND', // 选择器未找到(平台 UI 可能已变)
|
||
NETWORK = 'NETWORK', // 网络错误
|
||
PLATFORM_ERROR = 'PLATFORM_ERROR', // 平台返回错误
|
||
INTERNAL = 'INTERNAL', // 内部错误
|
||
}
|
||
|
||
// 错误分类函数
|
||
function classifyError(err: Error): ErrorCategory {
|
||
const msg = err.message.toLowerCase();
|
||
if (msg.includes('timeout') || err.name === 'TimeoutError') return ErrorCategory.TIMEOUT;
|
||
if (msg.includes('net::err_')) return ErrorCategory.NETWORK;
|
||
if (msg.includes('login') || msg.includes('登录')) return ErrorCategory.AUTH_REQUIRED;
|
||
if (msg.includes('waiting for selector') || msg.includes('找不到元素')) return ErrorCategory.SELECTOR_NOT_FOUND;
|
||
return ErrorCategory.INTERNAL;
|
||
}
|
||
|
||
// 错误消息脱敏:去掉文件路径、长 token 等内部信息
|
||
function sanitizeErrorMessage(message: string): string {
|
||
return message
|
||
.replace(/\/[^\s:]+/g, '[path]') // 文件系统路径
|
||
.replace(/https?:\/\/[^\s]+/g, '[url]') // URL(可能含 token)
|
||
.replace(/[a-f0-9]{32,}/gi, '[hash]') // 长 hex 串
|
||
.substring(0, 200);
|
||
}
|
||
|
||
// 统一错误包装(注意:必须是 async 函数)
|
||
export async function withErrorHandling(
|
||
toolName: string,
|
||
fn: () => Promise<McpToolResult>,
|
||
): Promise<McpToolResult> {
|
||
try {
|
||
return await fn();
|
||
} catch (err) {
|
||
const error = err instanceof Error ? err : new Error(String(err));
|
||
const category = classifyError(error);
|
||
const safeMessage = sanitizeErrorMessage(error.message);
|
||
|
||
logger.error({ tool: toolName, category, err: error }, '工具执行失败');
|
||
|
||
return {
|
||
content: [{
|
||
type: 'text',
|
||
text: JSON.stringify({
|
||
tool: toolName,
|
||
error: category,
|
||
message: safeMessage,
|
||
}),
|
||
}],
|
||
isError: true,
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
每个 MCP 工具 handler 用 `withErrorHandling` 包裹,Playwright 的 `TimeoutError`、
|
||
`TargetClosedError` 等异常统一转为 MCP 的 `isError: true` 响应,不会导致连接断开。
|
||
AI 助手可以根据 `error` 字段判断是否需要重试、重新登录、或报告错误。
|
||
|
||
> **Review 修正**:原设计中 `withErrorHandling` 不是 `async` 却用了 `await`(编译不通过),
|
||
> 且返回类型 `T` 和 `McpToolResult` 不匹配。已修正。
|
||
> 新增错误分类 + 消息脱敏,防止泄露文件路径等内部信息。
|
||
|
||
### 6.5 日志脱敏
|
||
|
||
```typescript
|
||
// utils/logger.ts
|
||
import pino from 'pino';
|
||
|
||
export const logger = pino({
|
||
// 使用深层通配 ** 匹配任意嵌套层级,避免漏掉嵌套对象中的敏感字段
|
||
redact: {
|
||
paths: [
|
||
'**.cookie', '**.cookies', '**.set-cookie',
|
||
'**.authorization', '**.password', '**.secret',
|
||
'**.token', '**.xsec_token', '**.access_token', '**.refresh_token',
|
||
'**.api_key', '**.apikey',
|
||
'**.sessionid', '**.session_id',
|
||
'**.cookies[*].value', // Playwright StorageState 中的 cookie 值
|
||
'**.origins[*].localStorage[*].value', // Playwright StorageState 中的 localStorage 值
|
||
],
|
||
censor: '[REDACTED]',
|
||
},
|
||
// 自定义错误序列化:生产环境不输出 stack
|
||
serializers: {
|
||
err: (err: Error) => ({
|
||
type: err.constructor.name,
|
||
message: err.message,
|
||
...(process.env.NODE_ENV === 'development' ? { stack: err.stack } : {}),
|
||
}),
|
||
},
|
||
transport: process.env.NODE_ENV !== 'production'
|
||
? { target: 'pino-pretty' }
|
||
: undefined,
|
||
});
|
||
|
||
// 生产环境禁用 Playwright 调试日志(会绕过 pino 直接输出到 stdout)
|
||
if (process.env.NODE_ENV === 'production') {
|
||
delete process.env.DEBUG;
|
||
}
|
||
```
|
||
|
||
> **Review 修正**:原 `*.field` 只匹配一层嵌套,改为 `**.field` 深层匹配。
|
||
> 补充了 Playwright StorageState 中 cookie value 的脱敏规则。
|
||
> 生产环境禁用 `DEBUG=pw:*` 防止 Playwright 调试日志泄露敏感信息。
|
||
|
||
---
|
||
|
||
## 七、安全基线(Phase 1 必须落地)
|
||
|
||
| 措施 | 说明 |
|
||
|------|------|
|
||
| **默认监听 127.0.0.1** | 配置项 `HOST=127.0.0.1`,不暴露到局域网/公网 |
|
||
| **Host header 校验** | 中间件校验 Host 头只允许 `127.0.0.1` / `localhost`,防御 DNS rebinding 攻击 |
|
||
| **拒绝监听 0.0.0.0** | 如果用户配置 `HOST=0.0.0.0`,启动时警告并要求设置 `ALLOW_REMOTE=yes-i-understand-the-risk` |
|
||
| **Cookie 文件权限 0600** | `fs.writeFile` 时指定 `mode: 0o600`,目录 0o700 |
|
||
| **Cookie 原子写入** | 先写 `.tmp` 文件再 `rename`,防止崩溃导致文件损坏 |
|
||
| **不加 CORS** | MCP 客户端走 HTTP 不走浏览器,不需要 CORS header |
|
||
| **日志深层脱敏** | pino `**` 深层通配,token/cookie/password 永远不入日志 |
|
||
| **错误消息脱敏** | `sanitizeErrorMessage()` 去掉文件路径、长 token,不返回堆栈 |
|
||
| **媒体文件路径校验** | 发布图片/视频时校验路径无 `..` 穿越,限制文件大小和 MIME 类型 |
|
||
|
||
### 7.1 DNS Rebinding 防御
|
||
|
||
即使监听 `127.0.0.1`,攻击者网页仍可通过 DNS rebinding 向本地服务发送请求。
|
||
由于浏览器对 simple POST(`Content-Type: text/plain` 等)不做 preflight,写操作会被执行。
|
||
|
||
```typescript
|
||
// server/middleware.ts
|
||
function dnsRebindingGuard(req: Request, res: Response, next: NextFunction) {
|
||
const host = req.headers.host?.toLowerCase();
|
||
const allowed = ['127.0.0.1', 'localhost', `127.0.0.1:${PORT}`, `localhost:${PORT}`];
|
||
if (!host || !allowed.includes(host)) {
|
||
logger.warn({ host }, 'DNS rebinding 请求被拦截');
|
||
return res.status(403).json({ error: 'Forbidden' });
|
||
}
|
||
next();
|
||
}
|
||
```
|
||
|
||
### 7.2 REST API 认证(Phase 5 实现)
|
||
|
||
REST API 启用时必须加 Bearer token 认证。首次启动生成随机 token 并显示给用户。
|
||
使用 `crypto.timingSafeEqual` 做时序安全比较。
|
||
|
||
### 7.3 媒体文件安全
|
||
|
||
```typescript
|
||
// 发布图片/视频时的路径校验
|
||
function validateMediaPath(filePath: string): string {
|
||
const resolved = path.resolve(filePath);
|
||
if (filePath.includes('..')) throw new Error('路径穿越不允许');
|
||
// 校验文件大小(图片 20MB,视频 500MB)
|
||
// 通过 magic bytes 校验 MIME 类型,不信任扩展名
|
||
return resolved;
|
||
}
|
||
```
|
||
|
||
> **Review 说明**:DNS rebinding 是安全审计提出的高优先级风险,实现成本极低(一个中间件),Phase 1 必须加。
|
||
> Bearer token 认证延迟到 Phase 5(REST API 实现时)。MCP 走 stdio transport,不经过 HTTP,不受此影响。
|
||
|
||
---
|
||
|
||
## 八、实现计划
|
||
|
||
### Phase 1: 骨架 + 基础设施
|
||
|
||
1. 初始化项目(pnpm, tsconfig, tsup, .gitignore, CLAUDE.md, .env.example)
|
||
2. `src/config/index.ts` — 环境变量(PORT, HOST=127.0.0.1, HEADLESS, BROWSER_BIN)+ 启动时校验 HOST(拒绝 0.0.0.0 除非显式确认)
|
||
3. `src/utils/logger.ts` — pino + `**` 深层 redact 脱敏 + 自定义错误序列化
|
||
4. `src/utils/errors.ts` — withErrorHandling 错误包装 + 错误分类(ErrorCategory)+ 消息脱敏
|
||
5. `src/browser/manager.ts` — BrowserManager(串行队列 + 分级超时 + timer 清理 + 启动锁 + 背压 + acquirePage 幂等释放 + 崩溃恢复 + drain)
|
||
6. `src/cookie/store.ts` — CookieStore(权限 0600 + 原子写入)
|
||
7. `src/server/app.ts` — AppServer(Express + MCP,不加 CORS)
|
||
8. `src/server/middleware.ts` — DNS rebinding 防御中间件(Host header 校验)+ 优雅关闭 503 中间件
|
||
9. `src/index.ts` — 入口 + SIGINT/SIGTERM 优雅关闭(先 drain 队列 → 关浏览器 → 关 HTTP → flush 日志)+ 全局 `unhandledRejection` / `uncaughtException` 处理
|
||
10. `src/utils/downloader.ts` — 图片下载(发布功能的前置依赖)+ 媒体路径校验
|
||
11. `/health` 端点 — 检查 Browser 存活、队列深度、内存使用(从 Phase 5 提前到 Phase 1)
|
||
12. **测试**:BrowserManager 单元测试(队列串行、背压拒绝、超时、启动锁)+ CookieStore 单元测试(读/写/删/原子写入)+ 错误分类单元测试
|
||
|
||
### Phase 2: 小红书 — 登录
|
||
|
||
13. `selectors.ts` + `types.ts` + `schemas.ts`
|
||
14. `login.ts` — 扫码登录、状态检查(使用 acquirePage,release 幂等)
|
||
15. `actions.ts` 初始化 + `index.ts` 注册 3 个登录工具
|
||
16. **测试**:zod schema 校验测试 + MCP Inspector 端到端验证登录流程
|
||
|
||
### Phase 3: 小红书 — 内容浏览
|
||
|
||
17. `feeds.ts` — 首页 Feed 列表(`__INITIAL_STATE__` 提取)
|
||
18. `search.ts` — 搜索 + 筛选
|
||
19. `feed-detail.ts` — 笔记详情 + 评论加载(scroll_speed 等内部默认配置)
|
||
20. `user-profile.ts` — 用户主页
|
||
21. **测试**:`__INITIAL_STATE__` JSON 解析单元测试 + MCP Inspector 验证 4 个浏览工具
|
||
|
||
### Phase 4: 小红书 — 发布 + 互动
|
||
|
||
22. `publish.ts` — 图文发布(依赖 downloader + 媒体路径校验)
|
||
23. `publish-video.ts` — 视频发布
|
||
24. `comment.ts` — 评论 + 回复
|
||
25. `interaction.ts` — 点赞 / 收藏
|
||
26. **测试**:downloader 单元测试 + 写操作手动验证(使用测试账号)
|
||
|
||
### Phase 5: 工程化
|
||
|
||
27. REST API 路由(调用 actions 层,复用 MCP 的业务逻辑)+ Bearer token 认证 + 限流
|
||
28. Docker(见下方 Docker 配置要求)
|
||
29. README
|
||
|
||
---
|
||
|
||
## 九、测试策略
|
||
|
||
### 9.1 原则
|
||
|
||
这个项目的核心是浏览器自动化,大部分逻辑依赖真实页面交互。
|
||
**不 mock Playwright Page 来测试页面操作**——成本高、收益低、选择器一变全白写。
|
||
只对**可独立测试的纯逻辑**写单元测试,浏览器交互靠端到端手动验证。
|
||
|
||
### 9.2 测试框架
|
||
|
||
| 工具 | 用途 |
|
||
|------|------|
|
||
| **vitest** | 单元测试(快,原生 TS 支持,兼容 Jest API) |
|
||
| **MCP Inspector** | MCP 工具端到端验证(官方调试工具) |
|
||
|
||
devDependencies 补充:`"vitest": "^3.0.0"`
|
||
|
||
### 9.3 单元测试范围
|
||
|
||
| 模块 | 测什么 | 怎么测 |
|
||
|------|--------|--------|
|
||
| **BrowserManager** | 串行队列(并发调用按序执行)、超时(超时后正确抛错 + timer 被清理)、启动锁(并发 ensureBrowser 只启动一次)、背压(超出 MAX_QUEUE_DEPTH 拒绝)、崩溃恢复(isConnected=false 时重建) | mock `chromium.launch` 和 `Browser` 接口 |
|
||
| **CookieStore** | 读/写/删、文件权限 0600、目录自动创建、原子写入(先 tmp 再 rename) | 真实文件系统(临时目录) |
|
||
| **withErrorHandling** | 正常返回透传、异常转 `isError: true`、错误分类正确性、消息脱敏(路径/token 被替换) | 纯函数测试 |
|
||
| **classifyError** | TIMEOUT / AUTH_REQUIRED / SELECTOR_NOT_FOUND / NETWORK / INTERNAL 各类型识别 | 构造不同错误消息测试 |
|
||
| **sanitizeErrorMessage** | 文件路径替换、URL 替换、长 hex 串替换、长度截断 | 纯函数测试 |
|
||
| **downloader** | URL 下载、本地路径校验(无 `..` 穿越)、非图片类型拒绝、文件大小限制 | mock HTTP 请求 |
|
||
| **数据解析** | `__INITIAL_STATE__` JSON → Feed/Comment 类型的转换 | 固定 JSON fixture 输入,断言输出结构 |
|
||
| **zod schema** | 必填字段缺失报错、可选字段默认值、枚举值校验 | 直接调用 `.parse()` / `.safeParse()` |
|
||
| **dnsRebindingGuard** | 合法 Host 放行、非法 Host 返回 403 | 构造 mock request 测试 |
|
||
|
||
### 9.4 端到端验证(手动)
|
||
|
||
每个 Phase 结束后,用 MCP Inspector 连接服务,逐个调用该阶段的 MCP 工具:
|
||
|
||
| Phase | 验证内容 |
|
||
|-------|---------|
|
||
| Phase 2 | `xhs_check_login` → `xhs_get_login_qrcode`(扫码)→ `xhs_check_login`(确认已登录)→ `xhs_delete_cookies` → `xhs_check_login`(确认已登出) |
|
||
| Phase 3 | `xhs_list_feeds`(有数据返回)→ `xhs_search`(关键词+筛选)→ `xhs_get_feed_detail`(取第一条的详情)→ `xhs_get_user_profile` |
|
||
| Phase 4 | `xhs_publish_image`(测试账号发一条)→ `xhs_post_comment` → `xhs_like` → `xhs_favorite`。**写操作用测试账号,不用主力账号** |
|
||
|
||
### 9.5 不做的
|
||
|
||
- **不 mock Page 测页面操作**:选择器绑定真实 DOM,mock 出来的测试没有意义
|
||
- **不做 CI 自动化端到端测试**:依赖真实浏览器 + 真实平台登录态,不适合 CI
|
||
- **不追求覆盖率指标**:只测有价值的纯逻辑,不为凑数写无意义的测试
|
||
|
||
---
|
||
|
||
## 十、关键设计决策
|
||
|
||
### 10.1 为什么不做统一 Platform 接口?
|
||
|
||
各平台业务模型差异大,强行统一 = 大量 `NotSupported` + 失去类型安全。
|
||
**插件模式**:共享基础设施,业务逻辑完全独立。
|
||
|
||
### 10.2 BrowserContext 隔离 + 串行队列
|
||
|
||
一个 Browser 进程 + 每个平台一个 BrowserContext + per-platform 操作串行化。
|
||
- 共享进程,节省资源
|
||
- Cookie/Storage 天然隔离
|
||
- 串行避免多 Page 互相干扰(同一平台同时只有一个操作在执行)
|
||
|
||
> **读写分离队列(暂不实现)**:性能工程师建议读操作(搜索、浏览)可以并发执行,只有写操作需要串行。
|
||
> 理论上正确,但当前 MCP 工具由 AI 顺序调用,极少出现并发场景。保持简单串行队列,
|
||
> 如果未来确实遇到队列积压问题,再升级为读写锁。不过早优化。
|
||
|
||
> **进程级隔离(暂不实现)**:性能工程师建议每个平台一个浏览器进程,隔离崩溃影响。
|
||
> 当前只有一个平台,一个进程足够。每多一个进程 +300MB 内存。等 3+ 平台时再考虑。
|
||
|
||
### 10.3 登录流程为什么不用 withPage?
|
||
|
||
`withPage` 是"用完即关",但扫码登录需要:
|
||
1. 打开页面获取二维码 → 返回给 AI
|
||
2. 用户扫码(页面保持打开)
|
||
3. 扫码成功 → 保存 Cookie → 关闭页面
|
||
|
||
因此登录使用 `acquirePage()` + 手动 release,其他操作用 `withPage()`。
|
||
`release` 函数是幂等的(内部 `released` 标志位),防止 setTimeout 和 finally 双重触发。
|
||
|
||
### 10.4 zod 一鱼两吃
|
||
|
||
- MCP 工具参数 schema(运行时校验)
|
||
- TypeScript 类型推导(`z.infer`)
|
||
- MCP SDK v1.x `server.tool()` 原生支持 zod shape
|
||
|
||
### 10.5 MCP 工具前缀 `xhs_`
|
||
|
||
简短、AI 易识别。后续平台:`xhh_`(小黑盒)、`bili_`(B站)、`wb_`(微博)。
|
||
|
||
### 10.6 为什么不在 Phase 1 加 Cookie 加密?
|
||
|
||
安全审计建议用 AES-256-GCM 加密 cookie 文件并集成 OS keychain。
|
||
当前安全模型(文件权限 0600 + 监听 127.0.0.1 + Host 校验)对本地自动化工具足够。
|
||
加密引入密钥管理复杂度(密钥存哪里?),收益与成本不匹配。
|
||
如果未来需要多用户共享机器或远程部署,再引入加密。
|
||
|
||
### 10.7 为什么不做选择器 fallback?
|
||
|
||
架构师建议为每个 CSS 选择器准备 fallback 备选。
|
||
但选择器失效时通常整个页面结构都变了,fallback 大概率也失效。
|
||
保持简单的 `selectors.ts` 集中管理,发现失效时快速修复即可。
|
||
|
||
---
|
||
|
||
## 十一、风险和注意事项
|
||
|
||
1. **CSS 选择器失效** — 集中到 `selectors.ts` 便于快速修复
|
||
2. **反爬升级** — rebrowser-playwright 修复底层检测点,比 JS 层 stealth 更彻底,但仍需持续跟进
|
||
3. **浏览器资源** — Playwright 进程 ~300MB,Docker 镜像较大
|
||
4. **Node.js 部署** — Docker 是推荐方式
|
||
5. **浏览器崩溃影响** — 所有平台共享一个 Browser 进程,崩溃时全部 context 丢失。`browser.on('disconnected')` 主动清理状态,下次操作自动重建
|
||
6. **Cookie 过期** — 平台可能随时吊销 session。当前无主动检测机制,依赖操作失败时的错误分类(`AUTH_REQUIRED`)提示 AI 重新登录
|
||
7. **DNS rebinding 攻击** — 即使监听 localhost,攻击者网页可通过 DNS rebinding 发送 POST 请求触发写操作。Phase 1 用 Host header 校验中间件防御
|
||
8. **Docker /dev/shm** — 必须配置 `shm_size: 1gb`,否则 Chromium 会因共享内存不足 SIGBUS 崩溃
|
||
9. **长操作队列阻塞** — 视频发布可能耗时 1-5 分钟,期间同平台其他操作全部排队等待。已用分级超时缓解(快操作 15s 超时,不会被慢操作的默认超时卡住)
|
||
|
||
---
|
||
|
||
## 十二、Docker 配置要求
|
||
|
||
```yaml
|
||
# deploy/docker-compose.yml
|
||
services:
|
||
social-auto-hub:
|
||
build: .
|
||
ports:
|
||
- "127.0.0.1:3000:3000" # 必须带 127.0.0.1 前缀,不要省略
|
||
shm_size: '1gb' # 必须:Chromium 需要足够的共享内存,默认 64MB 会导致 SIGBUS 崩溃
|
||
deploy:
|
||
resources:
|
||
limits:
|
||
memory: 2g # 至少 2x 预期峰值
|
||
cpus: '2.0' # Chromium 多进程架构需要至少 2 核
|
||
reservations:
|
||
memory: 1g
|
||
cpus: '1.0'
|
||
security_opt:
|
||
- no-new-privileges:true
|
||
cap_drop:
|
||
- ALL
|
||
read_only: true
|
||
tmpfs:
|
||
- /tmp:size=512m
|
||
volumes:
|
||
- cookie-data:/home/appuser/.social-auto-hub
|
||
environment:
|
||
- NODE_ENV=production
|
||
|
||
volumes:
|
||
cookie-data:
|
||
```
|
||
|
||
```dockerfile
|
||
# Dockerfile 要点
|
||
# - 非 root 用户运行(UID 1001)
|
||
# - 多阶段构建,不把 .env 或密钥文件复制进镜像层
|
||
# - HEALTHCHECK 检查 /health 端点
|
||
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
|
||
CMD node -e "fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1))"
|
||
```
|
||
|
||
---
|
||
|
||
## 十三、依赖清单
|
||
|
||
```json
|
||
{
|
||
"dependencies": {
|
||
"@modelcontextprotocol/sdk": "^1.27.0",
|
||
"rebrowser-playwright": "^1.52.0",
|
||
"express": "^4.21.0",
|
||
"pino": "^9.0.0",
|
||
"zod": "^3.25.0"
|
||
},
|
||
"devDependencies": {
|
||
"typescript": "^5.7.0",
|
||
"tsup": "^8.0.0",
|
||
"vitest": "^3.0.0",
|
||
"pino-pretty": "^13.0.0",
|
||
"@types/express": "^5.0.0",
|
||
"@types/node": "^22.0.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 附录 A:Review 修正清单
|
||
|
||
本 Plan 经过四方专业评审(架构师、安全审计、后端架构师、性能工程师),以下是已采纳的修正:
|
||
|
||
### 已整合进 Plan 的修正(P0/P1)
|
||
|
||
| 优先级 | 问题 | 来源 | 对应章节 |
|
||
|--------|------|------|---------|
|
||
| **P0** | 超时 timer 泄漏 + fn 在 page 关闭后继续运行 | 全员 | 6.1 BrowserManager |
|
||
| **P0** | `ensureBrowser()` 并发竞争可能启动多个浏览器 | 后端+性能 | 6.1 launchPromise |
|
||
| **P0** | `release()` 双重调用竞态 | 后端+架构 | 6.1 acquirePage + 6.2 登录流程 |
|
||
| **P0** | `withErrorHandling` 缺少 async 关键字 | 后端 | 6.4 错误处理 |
|
||
| **P1** | 队列无深度限制(无背压) | 全员 | 6.1 MAX_QUEUE_DEPTH |
|
||
| **P1** | 缺少错误分类体系 | 架构+后端 | 6.4 ErrorCategory |
|
||
| **P1** | 错误消息泄露文件路径 | 安全 | 6.4 sanitizeErrorMessage |
|
||
| **P1** | DNS rebinding 攻击 | 安全 | 7.1 dnsRebindingGuard |
|
||
| **P1** | 日志 redact 只匹配一层 | 安全 | 6.5 `**` 深层通配 |
|
||
| **P1** | Health check 在 Phase 5 太晚 | 架构+性能 | 8 Phase 1 第 11 项 |
|
||
| **P1** | Docker `shm_size` 缺失 | 性能 | 十二 Docker 配置 |
|
||
| **P1** | 浏览器崩溃后 contexts Map 残留 | 后端+性能 | 6.1 disconnected 事件 |
|
||
|
||
### 已整合但标记为延迟实现的
|
||
|
||
| 问题 | 来源 | 决定 | 对应章节 |
|
||
|------|------|------|---------|
|
||
| PlatformPlugin 生命周期钩子 | 架构 | 可选钩子,第二个平台时评估 | 3.2 |
|
||
| REST API Bearer token | 安全 | Phase 5 实现 | 7.2 |
|
||
| 媒体文件路径穿越 | 安全 | Phase 4 publish 时实现 | 7.3 |
|
||
| Cookie 原子写入 | 后端 | Phase 1 CookieStore 实现 | 6.3 |
|
||
| 分级超时 | 性能 | Phase 1 BrowserManager 实现 | 6.1 |
|
||
|
||
### 评审后明确不采纳的
|
||
|
||
| 建议 | 来源 | 不采纳原因 | 对应章节 |
|
||
|------|------|-----------|---------|
|
||
| Cookie 加密 + OS keychain | 安全 | 本地工具,文件权限已足够,密钥管理增加复杂度 | 10.6 |
|
||
| 选择器 fallback 机制 | 架构 | 选择器失效时通常整页变化,fallback 无意义 | 10.7 |
|
||
| 读写分离队列 | 性能 | AI 顺序调用极少并发,过早优化 | 10.2 |
|
||
| 进程级平台隔离 | 性能 | 只有一个平台,+300MB/进程 成本过高 | 10.2 |
|
||
| 内存安全(Buffer 清零) | 安全 | JS 字符串不可变,Node.js 中不实用 | — |
|