Conversation
- 添加save_draft工具 - 由于小红书的暂存功能是保存在浏览器本地而不在云端,save_draft实际上是发布“仅个人可见”笔记
- 私密发布(通过“仅个人可见”发布,保存在云端) - 暂存(通过调用页面“暂时离开”,存在浏览器本地)
|
There was a problem hiding this comment.
PR #602 代码审查
总体评价
本 PR 为项目新增了草稿管理功能(save_draft / list_local_drafts / get_local_draft_detail),补齐了草稿工作流。功能设计上 cloud/local 两种模式的划分合理,IndexedDB 读写和轮询等待机制的实现也比较完整。
但在代码质量方面发现了一些需要修复的问题,主要集中在:缩进破坏、资源泄漏风险、重复代码、以及可能导致 panic 的调用方式。
🔴 严重问题(需修复)
1. publish_video.go — submitPublishVideo 原有代码缩进被破坏
diff 显示原有的 submitPublishVideo 函数末尾被多加了一层缩进(tab),这属于非预期的格式改动,会影响代码可读性和后续 blame 追溯:
- // 等待发布按钮可点击
- btn, err := waitForPublishButtonClickable(page)
+ // 等待发布按钮可点击
+ btn, err := waitForPublishButtonClickable(page)
+ if err != nil {
+ return err
+ }
+
+ // 点击发布
+ if err := btn.Click(proto.InputMouseButtonLeft, 1); err != nil {
+ return errors.Wrap(err, "点击发布按钮失败")
}建议:还原 submitPublishVideo 函数中已有代码的缩进,只增加新代码。请运行 gofmt 或 goimports 确认格式正确。
2. publish.go — submitLocalDraft 闭合括号缩进异常
if err := waitAndClickTitleInput(titleElem); err != nil {
return err
} // <-- 此处多了一个 tab这里的 } 应与 if 对齐。虽然 Go 编译器能通过,但不符合 gofmt 规范,说明该文件可能没有经过 gofmt 格式化。
3. drafts.go — 使用 MustNavigate / MustWaitLoad 会导致 panic
page.MustNavigate("https://creator.xiaohongshu.com/").MustWaitLoad()Must* 系列方法在失败时会 panic,不适合在生产代码中使用。如果网络超时或页面加载失败,整个进程会崩溃。建议改为:
err := page.Navigate("https://creator.xiaohongshu.com/")
if err != nil {
return nil, errors.Wrap(err, "导航到创作中心失败")
}
if err := page.WaitLoad(); err != nil {
return nil, errors.Wrap(err, "等待页面加载失败")
}ListLocalDrafts 和 GetLocalDraftDetail 中各有一处,需要都修改。
4. service.go — draftBrowser 缺少生命周期管理
var (
draftBrowserOnce sync.Once
draftBrowser *headless_browser.Browser
draftBrowserMu sync.Mutex
)
func getDraftBrowser() *headless_browser.Browser {
draftBrowserOnce.Do(func() {
draftBrowser = newBrowser()
})
return draftBrowser
}draftBrowser 通过 sync.Once 创建后 永远不会被 Close,这意味着:
- Chrome 子进程在程序整个生命周期中持续占用内存
- 程序退出时可能残留僵尸进程
- 与项目中其他浏览器使用模式(
defer b.Close())不一致
建议增加一个 closeDraftBrowser() 函数,在程序 shutdown 时调用,或者注册到 signal 处理中:
func closeDraftBrowser() {
draftBrowserMu.Lock()
defer draftBrowserMu.Unlock()
if draftBrowser != nil {
draftBrowser.Close()
draftBrowser = nil
}
}🟡 中等问题(建议修复)
5. draftBrowserMu 互斥锁粒度过大
当前所有草稿操作(save/list/get)都在函数开始时获取 draftBrowserMu,直到函数结束才释放。这意味着:
SaveLocalImageDraft内部有最多 60 秒的轮询等待(waitForLocalDraftWritten)- 在此期间
ListLocalDrafts和GetLocalDraftDetail将被阻塞 - 多个并发的草稿查询也会被串行化
建议考虑:
- 将锁的范围缩小到仅保护
getDraftBrowser()和NewPage()的调用 - 或者使用
sync.RWMutex,读操作(list/get)使用读锁
6. submitLocalDraft 和 submitLocalVideoDraft 与已有代码高度重复
submitLocalDraft 与 submitPublish 大量逻辑重复(标题输入、正文输入、标签处理、设置可见范围等),仅最后一步不同(点击发布 vs 点击暂存离开)。submitLocalVideoDraft 与 submitPublishVideo 同理。
建议提取公共的"填充内容"逻辑为一个内部函数,减少维护负担:
func fillPublishForm(page *rod.Page, title, content string, tags []string, ...) error {
// 标题、正文、标签、定时发布、可见范围、商品等通用逻辑
}然后 submitPublish、submitLocalDraft、submitPublishVideo、submitLocalVideoDraft 调用 fillPublishForm 后执行各自的最终操作。
7. handleGetLocalDraftDetail 中重复定义了两个几乎相同的结构体
type localDraftDetailResp struct {
Found bool `json:"found"`
Store string `json:"store"`
DraftID string `json:"draft_id"`
Type string `json:"type"`
Value json.RawMessage `json:"value"`
}
// ... 几行后 ...
type outResp struct {
Found bool `json:"found"`
Store string `json:"store"`
DraftID string `json:"draft_id"`
Type string `json:"type"`
Value json.RawMessage `json:"value,omitempty"`
}两个结构体唯一区别是 Value 字段的 omitempty tag,实际上可以只保留一个(使用带 omitempty 的版本即可)。
8. browser.go — 猜测性设置多个环境变量
_ = os.Setenv("ROD_BROWSER_USER_DATA_DIR", userDataDir)
_ = os.Setenv("ROD_USER_DATA_DIR", userDataDir)
_ = os.Setenv("CHROME_USER_DATA_DIR", userDataDir)注释说"兼容多种可能的环境变量命名(以 headless_browser 实现为准)",这意味着开发者并不确定 headless_browser 库实际读取哪个变量。这种"猜测式兼容"有风险:
- 如果库不读任何一个,该功能完全无效
- 全局
os.Setenv会影响所有浏览器实例(不仅仅是 draftBrowser) - 未来库升级可能导致静默失效
建议:确认 headless_browser 库的具体实现,或在 headless_browser.Option 中直接传递 user-data-dir(如支持的话)。
9. SaveDraftArgs.Mode — jsonschema 标注与实际行为矛盾
Mode string `json:"mode" jsonschema:"【必填】暂存模式:cloud(...);local(...)"`jsonschema 描述标注为"必填",但代码中有默认值处理:
if args.Mode == "" {
args.Mode = "cloud"
}二者矛盾。建议统一:要么去掉 jsonschema 中的"必填"字样,要么移除默认值并在 Mode 为空时返回错误。
🟢 轻微建议
10. 冗余的 nil 赋值
var scheduleTime *time.Time = nilGo 中指针类型零值即为 nil,= nil 是多余的,直接 var scheduleTime *time.Time 即可。出现在 SaveLocalImageDraft 和 SaveLocalVideoDraft 两处。
11. 大段内嵌 JavaScript
drafts.go 和 publish.go 中内嵌了数百行 JavaScript 代码(IndexedDB 操作、轮询逻辑等)。建议:
- 考虑抽取为
const常量或使用embed嵌入独立的.js文件 - 便于单独测试和维护 JS 逻辑
12. 工具计数硬编码
logrus.Infof("Registered %d MCP tools", 16)每次增减工具都需要手动更新此数字,容易遗忘。建议使用动态计数或直接移除该日志。
总结
| 类别 | 数量 |
|---|---|
| 🔴 严重 | 4 |
| 🟡 中等 | 5 |
| 🟢 轻微 | 3 |
主要需要解决的是:代码格式问题(#1, #2)、MustNavigate panic 风险(#3)、以及 draftBrowser 资源泄漏(#4)。修复这些问题后,这个 PR 的质量会好很多。
感谢 @xiangjiali123 的贡献,草稿管理是一个很实用的功能 👍
|
探索仓库 /tmp/kimi-agent-work/xpzouying/xiaohongshu-mcp-pull-request-602 的代码结构。我需要了解:
请读取这些文件的关键部分。 审查摘要🔴 严重问题(4项)
🟡 中等问题(5项)
🟢 轻微建议(3项)
评审结论:功能设计合理,但需要修复上述严重问题后才能合并。建议 @xiangjiali123 优先解决 #1-#4 的代码格式和资源管理问题。 |
Summary
新增小红书草稿能力,提供 3 个 MCP 工具:
save_draft:保存草稿(cloud/local两种模式)list_local_drafts:读取本地草稿列表get_local_draft_detail:按draft_id读取本地草稿详情动机
补齐草稿完整工作流(保存 -> 列表 -> 详情),支持通过 MCP 对本地草稿进行管理与读取。
改动
README的草稿工具说明测试
save_draft(local)可执行list_local_drafts与get_local_draft_detail可调用并返回结果fixes #348