update at 2025-10-09 12:39:24
This commit is contained in:
@@ -1,72 +0,0 @@
|
||||
# 小红书自动化发布机制说明
|
||||
|
||||
## 1. 结构化 CSS 选择器
|
||||
集中存放于 `selectors.ts`,按功能分类:
|
||||
- ENTRY:入口区域(视频/图文选择)
|
||||
- PUBLISH_TAB:主发布 Tab(视频 or 图片)
|
||||
- VIDEO:视频发布流程元素
|
||||
- IMAGE:图文发布流程元素
|
||||
|
||||
修改页面结构时,仅需维护该文件。
|
||||
|
||||
## 2. 发布流程自动化方法(api.ts)
|
||||
| 方法 | 作用 |
|
||||
|------|------|
|
||||
| openPublishEntry | 打开发布入口页面 |
|
||||
| selectPublishTab | 切换到视频 or 图文 Tab |
|
||||
| triggerMediaUpload | 触发上传入口(不处理系统文件对话框)|
|
||||
| fillTitleAndContent | 并行填写标题与正文(不阻塞上传)|
|
||||
| choosePublishMode | 选择立即发布或定时(暂实现立即)|
|
||||
| waitForUploadSuccess | 轮询等待“上传成功”文案出现 |
|
||||
| clickPublishButton | 点击发布按钮 |
|
||||
| publishViaAutomation | 高层封装:一键执行完整流程 |
|
||||
| saveCookies | 将 document.cookie 简单保存到 localStorage |
|
||||
| restoreCookies | 从 localStorage 写回 cookie(仅适合简单会话)|
|
||||
| ensureSession | 恢复并检测是否仍已登录 |
|
||||
|
||||
## 3. 异步上传策略
|
||||
- 上传触发后立即并行执行:填写标题 + 填写正文 + 设置发布模式
|
||||
- 独立等待“上传成功”文案出现(最大 180s)
|
||||
- 提供扩展点:可替换为 MutationObserver
|
||||
|
||||
## 4. Cookies 会话保持策略
|
||||
当前采用简化方案:
|
||||
1. 登录后或发布点击后调用 `saveCookies()` 将 `document.cookie` 原始串写入 localStorage。
|
||||
2. 下次调用 `ensureSession()` 时:
|
||||
- 打开发布页
|
||||
- `restoreCookies()` 将简单 key=value 还原
|
||||
- 检查是否仍已登录(调用 `checkLoginStatus()`)
|
||||
|
||||
局限:
|
||||
- 无法还原 HttpOnly / 过期属性 / 域等
|
||||
- 真实长期稳定需使用:
|
||||
- Electron session APIs(如 webContents.session.cookies.get/set)
|
||||
- 或在本地插件存储中序列化 cookie 条目
|
||||
|
||||
## 5. 待优化建议
|
||||
- 增加前端 Hook:上传完成事件触发后立即发布
|
||||
- 增加失败重试,比如发布按钮未出现时二次尝试选择 Tab
|
||||
- 图文上传成功 DOM 精细化判断
|
||||
- 支持定时发布(scheduleTime 入参)
|
||||
- 支持话题 / 地址选择自动化
|
||||
|
||||
## 6. 示例调用
|
||||
```ts
|
||||
await api.publishViaAutomation({
|
||||
type: 'video',
|
||||
title: '测试标题',
|
||||
content: '正文内容...',
|
||||
immediate: true
|
||||
});
|
||||
```
|
||||
|
||||
## 7. 风险提示
|
||||
| 风险 | 描述 | 处理建议 |
|
||||
|------|------|----------|
|
||||
| DOM 变动 | 页面结构变化导致选择器失效 | 增加多选择器冗余 + 容错 |
|
||||
| 登录失效 | Cookies 方式失效 | 使用 Electron cookies API |
|
||||
| 上传超时 | 网络抖动导致等待失败 | 暴露重试机制 |
|
||||
| 发布失败未捕获 | 发布后提示弹窗变化 | 增加结果轮询与提示解析 |
|
||||
|
||||
---
|
||||
更新时间:2025-09-27
|
||||
@@ -1,107 +0,0 @@
|
||||
# 小红书发布功能完成总结
|
||||
|
||||
## 📋 功能概述
|
||||
|
||||
✅ **已完成**: 为 Note2MP 插件成功添加了完整的小红书发布功能。
|
||||
|
||||
## 🚀 新增功能
|
||||
|
||||
### 1. 右键菜单集成
|
||||
- ✅ 在文件右键菜单中添加了"发布到小红书"选项
|
||||
- ✅ 仅对 Markdown 文件显示该选项
|
||||
- ✅ 使用心形图标(lucide-heart)作为菜单图标
|
||||
|
||||
### 2. 登录系统
|
||||
- ✅ 智能登录检查:首次使用时自动检测登录状态
|
||||
- ✅ 登录弹窗:未登录时自动弹出登录对话框
|
||||
- ✅ 手机验证码登录:默认手机号 13357108011
|
||||
- ✅ 验证码发送功能:60秒倒计时防重复发送
|
||||
- ✅ 登录状态管理:记录用户登录状态
|
||||
|
||||
### 3. 内容适配系统
|
||||
- ✅ Markdown 转小红书格式
|
||||
- ✅ 标题自动生成和长度控制(20字符以内)
|
||||
- ✅ 内容长度限制(1000字符以内)
|
||||
- ✅ 小红书风格样式添加(表情符号等)
|
||||
- ✅ 标签自动提取和格式化
|
||||
|
||||
### 4. 图片处理
|
||||
- ✅ 自动图片格式转换(统一转为PNG)
|
||||
- ✅ EXIF 信息处理和图片方向校正
|
||||
- ✅ 图片尺寸优化(适应平台要求)
|
||||
|
||||
### 5. Web 自动化发布
|
||||
- ✅ 基于 Electron webview 的网页操作
|
||||
- ✅ 自动填写发布表单
|
||||
- ✅ 模拟用户操作发布流程
|
||||
- ✅ 发布状态检查和结果反馈
|
||||
|
||||
## 📁 文件结构
|
||||
|
||||
```
|
||||
src/xiaohongshu/
|
||||
├── types.ts # 类型定义和常量
|
||||
├── api.ts # Web API 和自动化逻辑
|
||||
├── adapter.ts # 内容格式转换
|
||||
├── image.ts # 图片处理工具
|
||||
└── login-modal.ts # 登录界面组件
|
||||
```
|
||||
|
||||
## 🔧 技术特点
|
||||
|
||||
### 架构设计
|
||||
- **模块化设计**: 独立的小红书模块,不影响现有微信公众号功能
|
||||
- **单例模式**: API 管理器使用单例模式,确保资源有效利用
|
||||
- **类型安全**: 完整的 TypeScript 类型定义
|
||||
|
||||
### 用户体验
|
||||
- **一键发布**: 右键选择文件即可发布
|
||||
- **智能检查**: 自动检测登录状态和文件类型
|
||||
- **实时反馈**: 详细的状态提示和错误信息
|
||||
- **无缝集成**: 与现有预览界面完美集成
|
||||
|
||||
### 错误处理
|
||||
- **完善的异常捕获**: 各层级都有相应的错误处理
|
||||
- **用户友好提示**: 清晰的错误信息和解决建议
|
||||
- **日志记录**: 调试模式下的详细操作日志
|
||||
|
||||
## 📱 使用流程
|
||||
|
||||
1. **选择文件**: 在文件资源管理器中右键选择 Markdown 文件
|
||||
2. **点击发布**: 选择"发布到小红书"菜单项
|
||||
3. **登录验证**: 首次使用时输入手机号和验证码登录
|
||||
4. **内容处理**: 系统自动转换内容格式并优化
|
||||
5. **发布完成**: 获得发布结果反馈
|
||||
|
||||
## ✨ 用户需求满足度
|
||||
|
||||
✅ **核心需求**: "新增小红书发布功能" - 完全实现
|
||||
✅ **技术方案**: "模拟网页操作(类似Playwright自动化)" - 通过 Electron webview 实现
|
||||
✅ **UI集成**: "文章右键增加'发布小红书'" - 已完成
|
||||
✅ **登录流程**: "如果没有登陆,弹出登陆对话框。默认用户名:13357108011。点击发送验证码。填入验证码验证登陆" - 完全按要求实现
|
||||
|
||||
## 🎯 完成状态
|
||||
|
||||
- [x] 架构设计和技术方案
|
||||
- [x] 核心模块开发(4个模块)
|
||||
- [x] 内容适配和图片处理
|
||||
- [x] 登录界面和验证流程
|
||||
- [x] 右键菜单集成
|
||||
- [x] 完整功能测试和构建验证
|
||||
|
||||
**总计**: 1800+ 行代码,功能完整,可以投入使用!
|
||||
|
||||
## 🔮 后续扩展
|
||||
|
||||
该架构为后续功能扩展预留了空间:
|
||||
- 批量发布小红书内容
|
||||
- 发布状态追踪和管理
|
||||
- 更多平台支持
|
||||
- 高级内容编辑功能
|
||||
|
||||
---
|
||||
|
||||
*Created: 2024-12-31*
|
||||
*Status: ✅ 完成*
|
||||
*Code Lines: ~1800*
|
||||
*Files Modified: 5 files created, 1 file modified*
|
||||
@@ -1,112 +0,0 @@
|
||||
# 小红书发布功能使用指南
|
||||
|
||||
## 📋 问题修复情况
|
||||
|
||||
### ✅ 问题1: 右键菜单无法弹出登录窗口
|
||||
**原因**: 登录状态检查方法在主线程调用时可能失败
|
||||
**修复**:
|
||||
- 添加了详细的调试日志
|
||||
- 临时设置为总是显示登录对话框(便于测试)
|
||||
- 在 main.ts 中添加了状态提示
|
||||
|
||||
### ✅ 问题2: 验证码发送后手机收不到
|
||||
**原因**: 当前为开发模式,使用模拟验证码服务
|
||||
**修复**:
|
||||
- 明确标注为开发模式
|
||||
- 提供测试验证码:`123456`
|
||||
- 在界面中显示测试提示
|
||||
|
||||
## 🚀 测试步骤
|
||||
|
||||
### 1. 基本测试流程
|
||||
1. **右键发布**:
|
||||
- 在文件资源管理器中选择任意 `.md` 文件
|
||||
- 右键选择"发布到小红书"
|
||||
- 应该看到提示:"开始发布到小红书..."
|
||||
|
||||
2. **登录对话框**:
|
||||
- 会自动弹出登录对话框
|
||||
- 默认手机号:`13357108011`
|
||||
- 标题显示为:"登录小红书"
|
||||
|
||||
3. **验证码测试**:
|
||||
- 点击"发送验证码"按钮
|
||||
- 看到提示:"验证码已发送 [开发模式: 请使用 123456]"
|
||||
- 在验证码输入框中输入:`123456`
|
||||
- 点击"登录"按钮
|
||||
|
||||
4. **登录成功**:
|
||||
- 显示"登录成功!"
|
||||
- 1.5秒后自动关闭对话框
|
||||
- 继续发布流程
|
||||
|
||||
### 2. 开发者控制台日志
|
||||
打开开发者控制台(F12),可以看到详细日志:
|
||||
```
|
||||
开始发布到小红书... filename.md
|
||||
检查登录状态...
|
||||
登录状态: false
|
||||
用户未登录,显示登录对话框...
|
||||
打开登录模态窗口...
|
||||
[模拟] 向 13357108011 发送验证码
|
||||
[开发模式] 请使用测试验证码: 123456
|
||||
[模拟] 使用手机号 13357108011 和验证码 123456 登录
|
||||
登录成功回调被调用
|
||||
登录窗口关闭
|
||||
登录结果: true
|
||||
```
|
||||
|
||||
## 🔧 调试信息
|
||||
|
||||
### 当前模拟状态
|
||||
- **登录检查**: 总是返回未登录状态(便于测试登录流程)
|
||||
- **验证码发送**: 模拟发送,不会真正发送短信
|
||||
- **验证码验证**: 接受测试验证码 `123456`, `000000`, `888888`
|
||||
- **内容发布**: 会执行内容转换,但实际发布为模拟状态
|
||||
|
||||
### 预期的用户交互
|
||||
1. ✅ 右键菜单显示"发布到小红书"
|
||||
2. ✅ 点击后显示加载提示
|
||||
3. ✅ 自动弹出登录对话框
|
||||
4. ✅ 默认手机号已填写
|
||||
5. ✅ 发送验证码功能正常
|
||||
6. ✅ 使用测试验证码可以成功登录
|
||||
7. ✅ 登录成功后会关闭对话框
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 如果登录对话框没有弹出
|
||||
1. 检查开发者控制台是否有错误信息
|
||||
2. 确认是否安装了最新版本的插件
|
||||
3. 检查是否选择的是 `.md` 文件
|
||||
|
||||
### 如果验证码验证失败
|
||||
1. 确认输入的是测试验证码:`123456`
|
||||
2. 检查是否先点击了"发送验证码"
|
||||
3. 确认倒计时已开始(60秒)
|
||||
|
||||
### 如果发布流程中断
|
||||
1. 查看开发者控制台的详细错误信息
|
||||
2. 确认文件格式为有效的 Markdown
|
||||
3. 检查插件是否正确加载了所有小红书模块
|
||||
|
||||
## 💡 下一步工作
|
||||
|
||||
### 生产环境集成
|
||||
1. **真实验证码服务**: 集成小红书官方验证码API
|
||||
2. **登录状态持久化**: 保存登录状态,避免重复登录
|
||||
3. **实际发布接口**: 连接小红书创作者平台API
|
||||
4. **错误处理优化**: 添加更详细的错误提示和恢复机制
|
||||
|
||||
### 功能增强
|
||||
1. **批量发布**: 支持选择多个文件批量发布
|
||||
2. **发布历史**: 记录发布历史和状态
|
||||
3. **内容预览**: 发布前预览小红书格式效果
|
||||
4. **高级设置**: 允许用户自定义发布参数
|
||||
|
||||
---
|
||||
|
||||
**开发状态**: ✅ 功能调试完成,可以进行UI测试
|
||||
**测试验证码**: `123456`
|
||||
**当前版本**: v1.3.0-dev
|
||||
**最后更新**: 2024-12-31
|
||||
@@ -15,6 +15,9 @@ import AssetsManager from '../assets';
|
||||
import { paginateArticle, renderPage, PageInfo } from './paginator';
|
||||
import { sliceCurrentPage, sliceAllPages } from './slice';
|
||||
|
||||
const XHS_PREVIEW_DEFAULT_WIDTH = 540;
|
||||
const XHS_PREVIEW_WIDTH_OPTIONS = [1080, 720, 540, 360];
|
||||
|
||||
/**
|
||||
* 小红书预览视图类
|
||||
*/
|
||||
@@ -29,6 +32,7 @@ export class XiaohongshuPreview {
|
||||
topToolbar!: HTMLDivElement;
|
||||
templateSelect!: HTMLSelectElement;
|
||||
fontSizeInput!: HTMLInputElement;
|
||||
previewWidthSelect!: HTMLSelectElement;
|
||||
|
||||
pageContainer!: HTMLDivElement;
|
||||
bottomToolbar!: HTMLDivElement;
|
||||
@@ -110,6 +114,30 @@ export class XiaohongshuPreview {
|
||||
option.text = name;
|
||||
});
|
||||
|
||||
const previewWidthLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
|
||||
previewWidthLabel.innerText = '预览宽度';
|
||||
this.previewWidthSelect = this.topToolbar.createEl('select', { cls: 'xhs-select' });
|
||||
const currentPreviewWidth = this.settings.xhsPreviewWidth || XHS_PREVIEW_DEFAULT_WIDTH;
|
||||
XHS_PREVIEW_WIDTH_OPTIONS.forEach(value => {
|
||||
const option = this.previewWidthSelect.createEl('option');
|
||||
option.value = String(value);
|
||||
option.text = `${value}px`;
|
||||
});
|
||||
if (!XHS_PREVIEW_WIDTH_OPTIONS.includes(currentPreviewWidth)) {
|
||||
const customOption = this.previewWidthSelect.createEl('option');
|
||||
customOption.value = String(currentPreviewWidth);
|
||||
customOption.text = `${currentPreviewWidth}px`;
|
||||
}
|
||||
this.previewWidthSelect.value = String(currentPreviewWidth);
|
||||
this.previewWidthSelect.onchange = async () => {
|
||||
const value = parseInt(this.previewWidthSelect.value, 10);
|
||||
if (Number.isFinite(value) && value > 0) {
|
||||
await this.onPreviewWidthChanged(value);
|
||||
} else {
|
||||
this.previewWidthSelect.value = String(this.settings.xhsPreviewWidth || XHS_PREVIEW_DEFAULT_WIDTH);
|
||||
}
|
||||
};
|
||||
|
||||
// 字号控制(可直接编辑)
|
||||
const fontSizeLabel = this.topToolbar.createDiv({ cls: 'toolbar-label' });
|
||||
fontSizeLabel.innerText = '字号';
|
||||
@@ -205,6 +233,7 @@ export class XiaohongshuPreview {
|
||||
if (this.currentThemeClass) classes.push('note-to-mp');
|
||||
const pageElement = wrapper.createDiv({ cls: classes.join(' ') });
|
||||
renderPage(pageElement, page.content, this.settings);
|
||||
this.applyPreviewSizing(wrapper, pageElement);
|
||||
|
||||
// 应用字体设置
|
||||
this.applyFontSettings(pageElement);
|
||||
@@ -213,6 +242,49 @@ export class XiaohongshuPreview {
|
||||
this.pageNumberDisplay.innerText = `${this.currentPageIndex + 1}/${this.pages.length}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据设置的宽度和横竖比应用预览尺寸与缩放
|
||||
*/
|
||||
private applyPreviewSizing(wrapper: HTMLElement, pageElement: HTMLElement): void {
|
||||
const configuredWidth = this.settings.sliceImageWidth || 1080;
|
||||
const actualWidth = Math.max(1, configuredWidth);
|
||||
const ratio = this.parseAspectRatio(this.settings.sliceImageAspectRatio);
|
||||
const actualHeight = Math.round((actualWidth * ratio.height) / ratio.width);
|
||||
const previewWidthSetting = this.settings.xhsPreviewWidth || XHS_PREVIEW_DEFAULT_WIDTH;
|
||||
const previewWidth = Math.max(1, previewWidthSetting);
|
||||
const scale = Math.max(previewWidth / actualWidth, 0.01);
|
||||
const previewHeight = Math.max(1, Math.round(actualHeight * scale));
|
||||
|
||||
wrapper.style.width = `${previewWidth}px`;
|
||||
wrapper.style.height = `${previewHeight}px`;
|
||||
|
||||
pageElement.style.width = `${actualWidth}px`;
|
||||
pageElement.style.height = `${actualHeight}px`;
|
||||
pageElement.style.transform = `scale(${scale})`;
|
||||
pageElement.style.position = 'absolute';
|
||||
pageElement.style.top = '0';
|
||||
pageElement.style.left = '0';
|
||||
}
|
||||
|
||||
private async onPreviewWidthChanged(newWidth: number): Promise<void> {
|
||||
if (newWidth <= 0) return;
|
||||
if (this.settings.xhsPreviewWidth === newWidth) return;
|
||||
this.settings.xhsPreviewWidth = newWidth;
|
||||
await this.persistSettings();
|
||||
this.renderCurrentPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析横竖比例字符串
|
||||
*/
|
||||
private parseAspectRatio(ratio: string | undefined): { width: number; height: number } {
|
||||
const parts = (ratio ?? '').split(':').map(part => parseFloat(part.trim()));
|
||||
if (parts.length === 2 && isFinite(parts[0]) && isFinite(parts[1]) && parts[0] > 0 && parts[1] > 0) {
|
||||
return { width: parts[0], height: parts[1] };
|
||||
}
|
||||
return { width: 3, height: 4 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用字体设置(仅字号,字体从主题读取)
|
||||
*/
|
||||
@@ -341,6 +413,17 @@ export class XiaohongshuPreview {
|
||||
}
|
||||
}
|
||||
|
||||
private async persistSettings(): Promise<void> {
|
||||
try {
|
||||
const plugin = (this.app as any)?.plugins?.getPlugin?.('note-to-mp');
|
||||
if (plugin?.saveSettings) {
|
||||
await plugin.saveSettings();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[XiaohongshuPreview] 保存设置失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示小红书预览视图
|
||||
*/
|
||||
@@ -365,6 +448,7 @@ export class XiaohongshuPreview {
|
||||
destroy(): void {
|
||||
this.topToolbar = null as any;
|
||||
this.templateSelect = null as any;
|
||||
this.previewWidthSelect = null as any;
|
||||
this.fontSizeInput = null as any;
|
||||
this.pageContainer = null as any;
|
||||
this.bottomToolbar = null as any;
|
||||
|
||||
Reference in New Issue
Block a user