794 lines
29 KiB
TypeScript
794 lines
29 KiB
TypeScript
/**
|
||
* 文件:api.ts
|
||
* 功能:小红书网页自动化 API 封装(模拟 / 原型阶段版本)。
|
||
*
|
||
* 主要职责:
|
||
* - 提供基于 webview / executeScript 的 DOM 操作能力
|
||
* - 模拟:登录状态检测、内容填写、图片/视频上传触发、发布按钮点击
|
||
* - 统一:错误处理、调试日志、发布流程封装(publishViaAutomation)
|
||
* - 附加:cookies 简易持久化(localStorage 方式,非生产级)
|
||
*
|
||
* 设计理念:
|
||
* - 抽象层:XiaohongshuWebAPI → 提供面向“动作”级别的方法(open / selectTab / fill / publish)
|
||
* - 扩展层:XiaohongshuAPIManager → 单例管理与调试模式开关
|
||
* - 低侵入:不直接耦合业务数据结构,可与适配器/转换器组合
|
||
*
|
||
* 重要限制(当前阶段):
|
||
* - 未接入真实文件上传与后端接口;
|
||
* - 登录凭证恢复仅限非 HttpOnly Cookie;
|
||
* - DOM 选择器依赖页面稳定性,需后续做多策略降级;
|
||
* - 未实现对发布后结果弹窗/状态的二次确认。
|
||
*
|
||
* 后续可改进:
|
||
* - 使用 Electron session.cookies 增强会话持久化;
|
||
* - 引入 MutationObserver 优化上传完成检测;
|
||
* - 抽象行为脚本 DSL,支持可配置流程;
|
||
* - 接入真实 API 进行更稳定的内容发布链路。
|
||
*/
|
||
|
||
import { Notice } from 'obsidian';
|
||
import {
|
||
XiaohongshuAPI,
|
||
XiaohongshuPost,
|
||
XiaohongshuResponse,
|
||
PostStatus,
|
||
XiaohongshuErrorCode,
|
||
XIAOHONGSHU_CONSTANTS
|
||
} from './types';
|
||
import { XHS_SELECTORS } from './selectors';
|
||
|
||
/**
|
||
* XiaohongshuWebAPI
|
||
*
|
||
* 说明(中文注释):
|
||
* 基于模拟网页操作的小红书API实现类。
|
||
* 通过操作网页DOM元素和模拟用户行为来实现小红书内容发布功能。
|
||
*
|
||
* 主要功能:
|
||
* - 自动登录小红书创作者中心
|
||
* - 填写发布表单并提交内容
|
||
* - 上传图片到小红书平台
|
||
* - 查询发布状态和结果
|
||
*
|
||
* 技术方案:
|
||
* 使用Electron的webContents API来操作内嵌的网页视图,
|
||
* 通过JavaScript代码注入的方式模拟用户操作。
|
||
*
|
||
* 注意事项:
|
||
* - 网页结构可能随时变化,需要容错处理
|
||
* - 需要处理反爬虫检测,添加随机延迟
|
||
* - 保持登录状态,处理会话过期
|
||
*/
|
||
export class XiaohongshuWebAPI implements XiaohongshuAPI {
|
||
private isLoggedIn: boolean = false;
|
||
private webview: any | null = null; // Electron webview element
|
||
private debugMode: boolean = false;
|
||
|
||
constructor(debugMode: boolean = false) {
|
||
this.debugMode = debugMode;
|
||
this.initializeWebview();
|
||
}
|
||
|
||
/**
|
||
* 初始化Webview
|
||
* 创建隐藏的webview用于网页操作
|
||
*/
|
||
private initializeWebview(): void {
|
||
// 创建隐藏的webview元素
|
||
this.webview = document.createElement('webview');
|
||
this.webview.addClass('xhs-webview');
|
||
|
||
// 设置webview属性
|
||
this.webview.setAttribute('nodeintegration', 'false');
|
||
this.webview.setAttribute('websecurity', 'false');
|
||
this.webview.setAttribute('partition', 'xiaohongshu');
|
||
|
||
// 添加到DOM
|
||
document.body.appendChild(this.webview);
|
||
|
||
// 监听webview事件
|
||
this.setupWebviewListeners();
|
||
|
||
this.debugLog('Webview initialized');
|
||
}
|
||
|
||
/**
|
||
* 设置webview事件监听器
|
||
*/
|
||
private setupWebviewListeners(): void {
|
||
if (!this.webview) return;
|
||
|
||
this.webview.addEventListener('dom-ready', () => {
|
||
this.debugLog('Webview DOM ready');
|
||
});
|
||
|
||
this.webview.addEventListener('did-fail-load', (event: any) => {
|
||
this.debugLog('Webview load failed:', event.errorDescription);
|
||
});
|
||
|
||
this.webview.addEventListener('console-message', (event: any) => {
|
||
if (this.debugMode) {
|
||
console.log('Webview console:', event.message);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 调试日志输出
|
||
*/
|
||
private debugLog(message: string, ...args: any[]): void {
|
||
if (this.debugMode) {
|
||
console.log(`[XiaohongshuAPI] ${message}`, ...args);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 等待指定时间
|
||
*/
|
||
private async delay(ms: number): Promise<void> {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
|
||
/**
|
||
* 在webview中执行JavaScript代码
|
||
*/
|
||
private async executeScript(script: string): Promise<any> {
|
||
return new Promise((resolve, reject) => {
|
||
if (!this.webview) {
|
||
reject(new Error('Webview not initialized'));
|
||
return;
|
||
}
|
||
|
||
this.webview.executeJavaScript(script)
|
||
.then(resolve)
|
||
.catch(reject);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 导航到指定URL
|
||
*/
|
||
private async navigateToUrl(url: string): Promise<void> {
|
||
return new Promise((resolve, reject) => {
|
||
if (!this.webview) {
|
||
reject(new Error('Webview not initialized'));
|
||
return;
|
||
}
|
||
|
||
const onDidFinishLoad = () => {
|
||
this.webview!.removeEventListener('did-finish-load', onDidFinishLoad);
|
||
resolve();
|
||
};
|
||
|
||
this.webview.addEventListener('did-finish-load', onDidFinishLoad);
|
||
this.webview.src = url;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 检查登录状态
|
||
*/
|
||
async checkLoginStatus(): Promise<boolean> {
|
||
try {
|
||
this.debugLog('Checking login status...');
|
||
|
||
// 导航到小红书创作者中心
|
||
await this.navigateToUrl(XIAOHONGSHU_CONSTANTS.PUBLISH_URL);
|
||
await this.delay(2000);
|
||
|
||
// 检查是否显示登录表单
|
||
const loginFormExists = await this.executeScript(`
|
||
(function() {
|
||
// 查找登录相关的元素
|
||
const loginSelectors = [
|
||
'.login-form',
|
||
'.auth-form',
|
||
'input[type="password"]',
|
||
'input[placeholder*="密码"]',
|
||
'input[placeholder*="手机"]',
|
||
'.login-container'
|
||
];
|
||
|
||
for (const selector of loginSelectors) {
|
||
if (document.querySelector(selector)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
})()
|
||
`);
|
||
|
||
this.isLoggedIn = !loginFormExists;
|
||
this.debugLog('Login status:', this.isLoggedIn);
|
||
|
||
return this.isLoggedIn;
|
||
} catch (error) {
|
||
this.debugLog('Error checking login status:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 使用用户名密码登录
|
||
*/
|
||
async loginWithCredentials(username: string, password: string): Promise<boolean> {
|
||
try {
|
||
this.debugLog('Attempting login with credentials...');
|
||
|
||
// 确保在登录页面
|
||
const isLoggedIn = await this.checkLoginStatus();
|
||
if (isLoggedIn) {
|
||
this.debugLog('Already logged in');
|
||
return true;
|
||
}
|
||
|
||
// 填写登录表单
|
||
const loginSuccess = await this.executeScript(`
|
||
(function() {
|
||
try {
|
||
// 查找用户名/手机号输入框
|
||
const usernameSelectors = [
|
||
'input[type="text"]',
|
||
'input[placeholder*="手机"]',
|
||
'input[placeholder*="用户"]',
|
||
'.username-input',
|
||
'.phone-input'
|
||
];
|
||
|
||
let usernameInput = null;
|
||
for (const selector of usernameSelectors) {
|
||
usernameInput = document.querySelector(selector);
|
||
if (usernameInput) break;
|
||
}
|
||
|
||
if (!usernameInput) {
|
||
console.log('Username input not found');
|
||
return false;
|
||
}
|
||
|
||
// 查找密码输入框
|
||
const passwordInput = document.querySelector('input[type="password"]');
|
||
if (!passwordInput) {
|
||
console.log('Password input not found');
|
||
return false;
|
||
}
|
||
|
||
// 填写表单
|
||
usernameInput.value = '${username}';
|
||
passwordInput.value = '${password}';
|
||
|
||
// 触发输入事件
|
||
usernameInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||
passwordInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||
|
||
// 查找并点击登录按钮
|
||
const loginButtonSelectors = [
|
||
'button[type="submit"]',
|
||
'.login-btn',
|
||
'.submit-btn',
|
||
'button:contains("登录")',
|
||
'button:contains("登陆")'
|
||
];
|
||
|
||
let loginButton = null;
|
||
for (const selector of loginButtonSelectors) {
|
||
loginButton = document.querySelector(selector);
|
||
if (loginButton) break;
|
||
}
|
||
|
||
if (loginButton) {
|
||
loginButton.click();
|
||
return true;
|
||
}
|
||
|
||
console.log('Login button not found');
|
||
return false;
|
||
} catch (error) {
|
||
console.error('Login script error:', error);
|
||
return false;
|
||
}
|
||
})()
|
||
`);
|
||
|
||
if (!loginSuccess) {
|
||
throw new Error('Failed to fill login form');
|
||
}
|
||
|
||
// 等待登录完成
|
||
await this.delay(3000);
|
||
|
||
// 验证登录状态
|
||
const finalLoginStatus = await this.checkLoginStatus();
|
||
this.isLoggedIn = finalLoginStatus;
|
||
|
||
if (this.isLoggedIn) {
|
||
new Notice('小红书登录成功');
|
||
this.debugLog('Login successful');
|
||
} else {
|
||
new Notice('小红书登录失败,请检查用户名和密码');
|
||
this.debugLog('Login failed');
|
||
}
|
||
|
||
return this.isLoggedIn;
|
||
} catch (error) {
|
||
this.debugLog('Login error:', error);
|
||
new Notice('小红书登录失败: ' + error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 上传单张图片
|
||
*/
|
||
async uploadImage(imageBlob: Blob): Promise<string> {
|
||
try {
|
||
this.debugLog('Uploading single image...');
|
||
|
||
if (!this.isLoggedIn) {
|
||
throw new Error('Not logged in');
|
||
}
|
||
|
||
// 导航到发布页面
|
||
await this.navigateToUrl(XIAOHONGSHU_CONSTANTS.PUBLISH_URL);
|
||
await this.delay(2000);
|
||
|
||
// TODO: 实现图片上传逻辑
|
||
// 这里需要将Blob转换为File并通过文件选择器上传
|
||
const imageUrl = await this.executeScript(`
|
||
(function() {
|
||
// 查找图片上传区域
|
||
const uploadSelectors = [
|
||
'.image-upload',
|
||
'.photo-upload',
|
||
'input[type="file"]',
|
||
'.upload-area'
|
||
];
|
||
|
||
let uploadElement = null;
|
||
for (const selector of uploadSelectors) {
|
||
uploadElement = document.querySelector(selector);
|
||
if (uploadElement) break;
|
||
}
|
||
|
||
if (!uploadElement) {
|
||
throw new Error('Upload element not found');
|
||
}
|
||
|
||
// TODO: 实际的图片上传逻辑
|
||
// 暂时返回占位符
|
||
return 'placeholder-image-url';
|
||
})()
|
||
`);
|
||
|
||
this.debugLog('Image uploaded:', imageUrl);
|
||
return imageUrl;
|
||
} catch (error) {
|
||
this.debugLog('Image upload error:', error);
|
||
throw new Error('图片上传失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量上传图片
|
||
*/
|
||
async uploadImages(imageBlobs: Blob[]): Promise<string[]> {
|
||
const results: string[] = [];
|
||
|
||
for (const blob of imageBlobs) {
|
||
const url = await this.uploadImage(blob);
|
||
results.push(url);
|
||
|
||
// 添加延迟避免过快的请求
|
||
await this.delay(1000);
|
||
}
|
||
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* 发布内容到小红书
|
||
*/
|
||
async createPost(content: XiaohongshuPost): Promise<XiaohongshuResponse> {
|
||
try {
|
||
this.debugLog('Creating post...', content);
|
||
|
||
if (!this.isLoggedIn) {
|
||
return {
|
||
success: false,
|
||
message: '未登录,请先登录小红书',
|
||
errorCode: XiaohongshuErrorCode.AUTH_FAILED
|
||
};
|
||
}
|
||
|
||
// 导航到发布页面
|
||
await this.navigateToUrl(XIAOHONGSHU_CONSTANTS.PUBLISH_URL);
|
||
await this.delay(2000);
|
||
|
||
// 填写发布表单
|
||
const publishResult = await this.executeScript(`
|
||
(function() {
|
||
try {
|
||
// 查找标题输入框
|
||
const titleSelectors = [
|
||
'input[placeholder*="标题"]',
|
||
'.title-input',
|
||
'input.title'
|
||
];
|
||
|
||
let titleInput = null;
|
||
for (const selector of titleSelectors) {
|
||
titleInput = document.querySelector(selector);
|
||
if (titleInput) break;
|
||
}
|
||
|
||
if (titleInput) {
|
||
titleInput.value = '${content.title}';
|
||
titleInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||
}
|
||
|
||
// 查找内容输入框
|
||
const contentSelectors = [
|
||
'textarea[placeholder*="内容"]',
|
||
'.content-textarea',
|
||
'textarea.content'
|
||
];
|
||
|
||
let contentTextarea = null;
|
||
for (const selector of contentSelectors) {
|
||
contentTextarea = document.querySelector(selector);
|
||
if (contentTextarea) break;
|
||
}
|
||
|
||
if (contentTextarea) {
|
||
contentTextarea.value = '${content.content}';
|
||
contentTextarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||
}
|
||
|
||
// 查找发布按钮
|
||
const publishButtonSelectors = [
|
||
'button:contains("发布")',
|
||
'.publish-btn',
|
||
'.submit-btn'
|
||
];
|
||
|
||
let publishButton = null;
|
||
for (const selector of publishButtonSelectors) {
|
||
publishButton = document.querySelector(selector);
|
||
if (publishButton) break;
|
||
}
|
||
|
||
if (publishButton) {
|
||
publishButton.click();
|
||
return { success: true, message: '发布请求已提交' };
|
||
} else {
|
||
return { success: false, message: '未找到发布按钮' };
|
||
}
|
||
} catch (error) {
|
||
return { success: false, message: '发布失败: ' + error.message };
|
||
}
|
||
})()
|
||
`);
|
||
|
||
// 等待发布完成
|
||
await this.delay(3000);
|
||
|
||
this.debugLog('Publish result:', publishResult);
|
||
|
||
return {
|
||
success: publishResult.success,
|
||
message: publishResult.message,
|
||
postId: publishResult.success ? 'generated-post-id' : undefined,
|
||
errorCode: publishResult.success ? undefined : XiaohongshuErrorCode.PUBLISH_FAILED
|
||
};
|
||
} catch (error) {
|
||
this.debugLog('Create post error:', error);
|
||
return {
|
||
success: false,
|
||
message: '发布失败: ' + error.message,
|
||
errorCode: XiaohongshuErrorCode.PUBLISH_FAILED,
|
||
errorDetails: error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询发布状态
|
||
*/
|
||
async getPostStatus(postId: string): Promise<PostStatus> {
|
||
try {
|
||
this.debugLog('Getting post status for:', postId);
|
||
|
||
// TODO: 实现状态查询逻辑
|
||
// 暂时返回已发布状态
|
||
return PostStatus.PUBLISHED;
|
||
} catch (error) {
|
||
this.debugLog('Get post status error:', error);
|
||
return PostStatus.FAILED;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 注销登录
|
||
*/
|
||
async logout(): Promise<boolean> {
|
||
try {
|
||
this.debugLog('Logging out...');
|
||
|
||
const logoutSuccess = await this.executeScript(`
|
||
(function() {
|
||
// 查找注销按钮或用户菜单
|
||
const logoutSelectors = [
|
||
'.logout-btn',
|
||
'button:contains("退出")',
|
||
'button:contains("注销")',
|
||
'.user-menu .logout'
|
||
];
|
||
|
||
for (const selector of logoutSelectors) {
|
||
const element = document.querySelector(selector);
|
||
if (element) {
|
||
element.click();
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
})()
|
||
`);
|
||
|
||
if (logoutSuccess) {
|
||
this.isLoggedIn = false;
|
||
new Notice('已退出小红书登录');
|
||
}
|
||
|
||
return logoutSuccess;
|
||
} catch (error) {
|
||
this.debugLog('Logout error:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 销毁webview并清理资源
|
||
*/
|
||
destroy(): void {
|
||
if (this.webview) {
|
||
document.body.removeChild(this.webview);
|
||
this.webview = null;
|
||
}
|
||
this.isLoggedIn = false;
|
||
this.debugLog('XiaohongshuWebAPI destroyed');
|
||
}
|
||
|
||
/**
|
||
* 打开发布入口页面(发布视频/图文)
|
||
*/
|
||
async openPublishEntry(): Promise<void> {
|
||
await this.navigateToUrl(XIAOHONGSHU_CONSTANTS.PUBLISH_URL);
|
||
await this.delay(1500);
|
||
}
|
||
|
||
/**
|
||
* 选择发布 Tab:视频 或 图文
|
||
*/
|
||
async selectPublishTab(type: 'video' | 'image'): Promise<boolean> {
|
||
const selector = type === 'video' ? XHS_SELECTORS.PUBLISH_TAB.TAB_VIDEO : XHS_SELECTORS.PUBLISH_TAB.TAB_IMAGE;
|
||
const ok = await this.executeScript(`(function(){
|
||
const el = document.querySelector('${selector}');
|
||
if (el) { el.click(); return true; }
|
||
return false;
|
||
})()`);
|
||
this.debugLog('Select tab', type, ok);
|
||
return ok;
|
||
}
|
||
|
||
/**
|
||
* 上传媒体:视频或图片(入口点击,不处理文件系统对话框)
|
||
*/
|
||
async triggerMediaUpload(type: 'video' | 'image'): Promise<boolean> {
|
||
const selector = type === 'video' ? XHS_SELECTORS.PUBLISH_TAB.UPLOAD_BUTTON : XHS_SELECTORS.IMAGE.IMAGE_UPLOAD_ENTRY;
|
||
const ok = await this.executeScript(`(function(){
|
||
const el = document.querySelector('${selector}');
|
||
if (el) { el.click(); return true; }
|
||
return false;
|
||
})()`);
|
||
this.debugLog('Trigger upload', type, ok);
|
||
return ok;
|
||
}
|
||
|
||
/**
|
||
* 并行填写标题与内容
|
||
*/
|
||
async fillTitleAndContent(type: 'video' | 'image', title: string, content: string): Promise<void> {
|
||
const titleSelector = type === 'video' ? XHS_SELECTORS.VIDEO.TITLE_INPUT : XHS_SELECTORS.IMAGE.TITLE_INPUT;
|
||
const contentSelector = type === 'video' ? XHS_SELECTORS.VIDEO.CONTENT_EDITOR : XHS_SELECTORS.IMAGE.CONTENT_EDITOR;
|
||
await this.executeScript(`(function(){
|
||
const t = document.querySelector('${titleSelector}');
|
||
if (t) { t.value = ${JSON.stringify(title)}; t.dispatchEvent(new Event('input',{bubbles:true})); }
|
||
const c = document.querySelector('${contentSelector}');
|
||
if (c) { c.innerHTML = ${JSON.stringify(content)}; c.dispatchEvent(new Event('input',{bubbles:true})); }
|
||
})()`);
|
||
}
|
||
|
||
/**
|
||
* 选择立即发布 / 定时发布 (暂仅实现立即发布)
|
||
*/
|
||
async choosePublishMode(immediate: boolean = true, scheduleTime?: string): Promise<void> {
|
||
await this.executeScript(`(function(){
|
||
const radioImmediate = document.querySelector('${XHS_SELECTORS.VIDEO.RADIO_IMMEDIATE}');
|
||
const radioSchedule = document.querySelector('${XHS_SELECTORS.VIDEO.RADIO_SCHEDULE}');
|
||
if (${immediate}) {
|
||
if (radioImmediate) { radioImmediate.click(); }
|
||
} else {
|
||
if (radioSchedule) { radioSchedule.click(); }
|
||
const timeInput = document.querySelector('${XHS_SELECTORS.VIDEO.SCHEDULE_TIME_INPUT}') as HTMLInputElement;
|
||
if (timeInput && ${JSON.stringify(scheduleTime)} ) { timeInput.value = ${JSON.stringify(scheduleTime)}; timeInput.dispatchEvent(new Event('input',{bubbles:true})); }
|
||
}
|
||
})()`);
|
||
}
|
||
|
||
/**
|
||
* 异步等待上传完成(检测文字“上传成功”或元素出现)
|
||
*/
|
||
async waitForUploadSuccess(type: 'video' | 'image', timeoutMs: number = 180000): Promise<boolean> {
|
||
const successSelector = type === 'video' ? XHS_SELECTORS.VIDEO.UPLOAD_SUCCESS_STAGE : XHS_SELECTORS.IMAGE.IMAGE_UPLOAD_ENTRY; // 图文等待入口变化可后续细化
|
||
const start = Date.now();
|
||
while (Date.now() - start < timeoutMs) {
|
||
const ok = await this.executeScript(`(function(){
|
||
const el = document.querySelector('${successSelector}');
|
||
if (!el) return false;
|
||
const text = el.textContent || '';
|
||
if (text.includes('上传成功') || text.includes('完成') ) return true;
|
||
return false;
|
||
})()`);
|
||
if (ok) return true;
|
||
await this.delay(1500);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 点击发布按钮
|
||
*/
|
||
async clickPublishButton(type: 'video' | 'image'): Promise<boolean> {
|
||
const selector = type === 'video' ? XHS_SELECTORS.VIDEO.PUBLISH_BUTTON : XHS_SELECTORS.IMAGE.PUBLISH_BUTTON;
|
||
const ok = await this.executeScript(`(function(){
|
||
const el = document.querySelector('${selector}');
|
||
if (el) { el.click(); return true; }
|
||
return false;
|
||
})()`);
|
||
this.debugLog('Click publish', type, ok);
|
||
return ok;
|
||
}
|
||
|
||
/**
|
||
* 高层封装:发布视频或图文
|
||
*/
|
||
async publishViaAutomation(params: {type: 'video' | 'image'; title: string; content: string; immediate?: boolean; scheduleTime?: string;}): Promise<XiaohongshuResponse> {
|
||
try {
|
||
await this.openPublishEntry();
|
||
await this.selectPublishTab(params.type);
|
||
await this.triggerMediaUpload(params.type);
|
||
// 不阻塞:并行填写标题和内容
|
||
await this.fillTitleAndContent(params.type, params.title, params.content);
|
||
await this.choosePublishMode(params.immediate !== false, params.scheduleTime);
|
||
const success = await this.waitForUploadSuccess(params.type);
|
||
if (!success) {
|
||
return { success: false, message: '媒体上传超时', errorCode: XiaohongshuErrorCode.IMAGE_UPLOAD_FAILED };
|
||
}
|
||
const clicked = await this.clickPublishButton(params.type);
|
||
if (!clicked) {
|
||
return { success: false, message: '未能点击发布按钮', errorCode: XiaohongshuErrorCode.PUBLISH_FAILED };
|
||
}
|
||
// 发布流程点击后尝试保存 cookies(保持会话)
|
||
this.saveCookies().catch(()=>{});
|
||
return { success: true, message: '发布流程已触发' };
|
||
} catch (e:any) {
|
||
return { success: false, message: e?.message || '发布异常', errorCode: XiaohongshuErrorCode.PUBLISH_FAILED };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 保存当前页面 cookies 到 localStorage(在浏览器上下文内执行)
|
||
*/
|
||
async saveCookies(): Promise<boolean> {
|
||
try {
|
||
const result = await this.executeScript(`(async function(){
|
||
try {
|
||
const all = document.cookie; // 简单方式:获取所有 cookie 串
|
||
if (!all) return false;
|
||
localStorage.setItem('__xhs_cookies_backup__', all);
|
||
return true;
|
||
} catch(e){ return false; }
|
||
})()`);
|
||
this.debugLog('saveCookies result', result);
|
||
return !!result;
|
||
} catch (e) {
|
||
this.debugLog('saveCookies error', e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 恢复 cookies:将 localStorage 中保存的 cookie 串重新写回 document.cookie
|
||
* 注意:有些带 HttpOnly/Domain/Path/Expires 的 cookie 无法直接还原,此方式只适合临时会话维持。
|
||
*/
|
||
async restoreCookies(): Promise<boolean> {
|
||
try {
|
||
const result = await this.executeScript(`(function(){
|
||
try {
|
||
const data = localStorage.getItem('__xhs_cookies_backup__');
|
||
if (!data) return false;
|
||
const parts = data.split(';');
|
||
for (const p of parts) {
|
||
// 仅还原简单 key=value
|
||
const kv = p.trim();
|
||
if (!kv) continue;
|
||
if (kv.includes('=')) {
|
||
document.cookie = kv; // 可能丢失附加属性
|
||
}
|
||
}
|
||
return true;
|
||
} catch(e){ return false; }
|
||
})()`);
|
||
this.debugLog('restoreCookies result', result);
|
||
return !!result;
|
||
} catch (e) {
|
||
this.debugLog('restoreCookies error', e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 确保会话:尝试恢复 cookies,再检测登录;若失败则返回 false
|
||
*/
|
||
async ensureSession(): Promise<boolean> {
|
||
// 先尝试恢复
|
||
await this.navigateToUrl(XIAOHONGSHU_CONSTANTS.PUBLISH_URL);
|
||
await this.restoreCookies();
|
||
await this.delay(1200);
|
||
const ok = await this.checkLoginStatus();
|
||
return ok;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 小红书API实例管理器
|
||
*
|
||
* 提供单例模式的API实例管理
|
||
*/
|
||
export class XiaohongshuAPIManager {
|
||
private static instance: XiaohongshuWebAPI | null = null;
|
||
private static debugMode: boolean = false;
|
||
|
||
/**
|
||
* 获取API实例
|
||
*/
|
||
static getInstance(debugMode: boolean = false): XiaohongshuWebAPI {
|
||
if (!this.instance) {
|
||
this.debugMode = debugMode;
|
||
this.instance = new XiaohongshuWebAPI(debugMode);
|
||
}
|
||
return this.instance;
|
||
}
|
||
|
||
/**
|
||
* 销毁API实例
|
||
*/
|
||
static destroyInstance(): void {
|
||
if (this.instance) {
|
||
this.instance.destroy();
|
||
this.instance = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置调试模式
|
||
*/
|
||
static setDebugMode(enabled: boolean): void {
|
||
this.debugMode = enabled;
|
||
if (this.instance) {
|
||
this.destroyInstance();
|
||
// 下次获取时会用新的调试设置创建实例
|
||
}
|
||
}
|
||
} |