/** * 文件:main.ts * 入口:Obsidian 插件主类,负责: * - 视图注册 / 右键菜单扩展 * - 微信公众号与小红书发布入口调度 * - 设置加载与保存 * - 与 NotePreview / 批量发布 / 小红书登录流程衔接 */ import { Plugin, WorkspaceLeaf, App, PluginManifest, Menu, Notice, TAbstractFile, TFile, TFolder } from 'obsidian'; import { PreviewView, VIEW_TYPE_NOTE_PREVIEW } from './preview-view'; import { NMPSettings } from './settings'; import { NoteToMpSettingTab } from './setting-tab'; import AssetsManager from './assets'; import { setVersion, uevent } from './utils'; import { WidgetsModal } from './widgets-modal'; import { BatchPublishModal } from './batch-publish-modal'; import { XiaohongshuLoginModal } from './xiaohongshu/login-modal'; import { XiaohongshuContentAdapter } from './xiaohongshu/adapter'; import { XiaohongshuAPIManager } from './xiaohongshu/api'; /** * NoteToMpPlugin * * 中文说明: * 这是插件的入口类,负责: * - 插件生命周期管理(onload/onunload) * - 注册自定义视图 NotePreview 用于渲染与发布文章 * - 提供多种命令:单篇发布、批量发布、插入样式组件等 * - 提供文件右键菜单扩展,支持对单文件或文件夹进行发布操作 * * 设计决策(简要): * - 将批量发布的 UI 放在 `BatchPublishModal` 中,命令 `note-to-mp-batch-publish` 会打开该模态框 * - 单篇发布/文件夹批量发布仍复用 `NotePreview` 的发布逻辑,避免重复实现上传流程 */ export default class NoteToMpPlugin extends Plugin { settings: NMPSettings; assetsManager: AssetsManager; ribbonIconEl: HTMLElement | null = null; constructor(app: App, manifest: PluginManifest) { super(app, manifest); AssetsManager.setup(app, manifest); this.assetsManager = AssetsManager.getInstance(); } async loadResource() { await this.loadSettings(); await this.assetsManager.loadAssets(); } async onload() { console.log('Loading NoteToMP (plugin onload start)'); setVersion(this.manifest.version); uevent('load'); console.log('[NoteToMpPlugin] workspace.layoutReady at onload =', this.app.workspace.layoutReady); // 先注册 view 之前,防止旧 snapshot 立即恢复创建大量视图:先临时卸载残留叶子(如果类型匹配) try { const legacyLeaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW); if (legacyLeaves.length > 0) { console.log('[NoteToMpPlugin] detach legacy leaves early count=', legacyLeaves.length); this.app.workspace.detachLeavesOfType(VIEW_TYPE_NOTE_PREVIEW); } } catch (e) { console.warn('[NoteToMpPlugin] early detach failed', e); } this.app.workspace.onLayoutReady(async () => { console.log('[NoteToMpPlugin] onLayoutReady callback entered'); console.time('[NoteToMpPlugin] startup:onLayoutReady→loadResource'); try { await this.loadResource(); // 确保资源完全加载完再继续,避免后续视图初始化反复等待 } catch (e) { console.error('[NoteToMpPlugin] loadResource 失败', e); } finally { console.timeEnd('[NoteToMpPlugin] startup:onLayoutReady→loadResource'); } // 清理旧视图 this.cleanupLegacyViews(); // 取消自动打开预览视图(用于排查启动卡顿)。用户可通过图标或命令手动打开。 // console.log('[NoteToMpPlugin] 已跳过自动打开预览视图调试模式'); }); this.registerView( VIEW_TYPE_NOTE_PREVIEW, (leaf) => new PreviewView(leaf, this) ); this.ribbonIconEl = this.addRibbonIcon('clipboard-paste', '复制到公众号', (evt: MouseEvent) => { this.activateView(); }); this.ribbonIconEl.addClass('note-to-mp-plugin-ribbon-class'); this.addCommand({ id: 'note-to-mp-preview', name: '复制到公众号', callback: () => { this.activateView(); } }); this.addSettingTab(new NoteToMpSettingTab(this.app, this)); this.addCommand({ id: 'note-to-mp-widget', name: '插入样式小部件', callback: () => { new WidgetsModal(this.app).open(); } }); this.addCommand({ id: 'note-to-mp-batch-publish', name: '批量发布文章', callback: () => { new BatchPublishModal(this.app, this).open(); } }); // TODO: 重构后需要重新实现批量发布功能 // this.addCommand({ // id: 'note-to-mp-pub', // name: '发布公众号文章', // callback: async () => { // await this.activateView(); // this.getNotePreview()?.postArticle(); // } // }); // 命令:当前文件发布到微信草稿 this.addCommand({ id: 'note-to-mp-post-current', name: '发布当前文件到公众号草稿', callback: async () => { const file = this.app.workspace.getActiveFile(); if (!file) { new Notice('没有活动文件'); return; } if (file.extension.toLowerCase() !== 'md') { new Notice('只能发布 Markdown 文件'); return; } await this.activateView(); await this.getNotePreview()?.postWechatDraft(file); } }); // 监听右键菜单(文件浏览器) this.registerEvent( this.app.workspace.on('file-menu', (menu, file) => { // 发布到公众号草稿 menu.addItem((item) => { item .setTitle('发布公众号') .setIcon('lucide-send') .onClick(async () => { if (file instanceof TFile) { if (file.extension.toLowerCase() !== 'md') { new Notice('只能发布 Markdown 文件'); return; } await this.activateView(); await this.getNotePreview()?.postWechatDraft(file); } }); }); // 发布到小红书 menu.addItem((item) => { item .setTitle('发布小红书') .setIcon('lucide-heart') .onClick(async () => { if (file instanceof TFile) { if (file.extension.toLowerCase() !== 'md') { new Notice('只能发布 Markdown 文件'); return; } await this.publishToXiaohongshu(file); } }); }); }) ); } onunload() { console.log('Unloading NoteToMP'); // 移除 ribbon icon,避免重载插件时重复创建 if (this.ribbonIconEl) { this.ribbonIconEl.remove(); this.ribbonIconEl = null; } } /** * 清理历史失效视图: * 某些用户可能曾使用过旧插件构建(例如 note-mp-preview-manager),升级后残留的标签页会提示“插件不再活动”。 * 这里做一次性清理,避免用户手动关标签造成困扰。 */ private cleanupLegacyViews() { try { const legacyIds = ['note-mp-preview-manager']; // 可扩展 const { workspace } = this.app; // 遍历所有叶子,关闭可能的失效 view(无法直接匹配 id 时,仅检测报错视图类型) workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW).forEach(l => { // 如果 view 的 plugin 不存在或 manifest id 不匹配我们当前的 id,则关闭 const anyView: any = l.view; const vid = (anyView?.plugin?.manifest?.id) || ''; if (vid && vid !== this.manifest.id && legacyIds.includes(vid)) { workspace.detachLeavesOfType(VIEW_TYPE_NOTE_PREVIEW); } }); } catch (e) { console.warn('[NoteToMp] cleanupLegacyViews 失败', e); } } async loadSettings() { NMPSettings.loadSettings(await this.loadData()); } async saveSettings() { await this.saveData(NMPSettings.allSettings()); } async activateView() { const { workspace } = this.app; let leaf: WorkspaceLeaf | null = null; const leaves = workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW); if (leaves.length > 0) { leaf = leaves[0]; } else { leaf = workspace.getRightLeaf(false); await leaf?.setViewState({ type: VIEW_TYPE_NOTE_PREVIEW, active: false }); } if (leaf) workspace.revealLeaf(leaf); } getNotePreview(): PreviewView | null { const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_NOTE_PREVIEW); if (leaves.length > 0) { const leaf = leaves[0]; return leaf.view as PreviewView; } return null; } /** * 发布到小红书 */ async publishToXiaohongshu(file: TFile) { try { console.log('开始发布到小红书...', file.name); new Notice('开始发布到小红书...'); // 获取API实例 const api = XiaohongshuAPIManager.getInstance(true); // 检查登录状态,如果未登录则显示登录对话框 console.log('检查登录状态...'); // 暂时总是显示登录对话框进行测试 const isLoggedIn = false; // await api.checkLoginStatus(); console.log('登录状态:', isLoggedIn); if (!isLoggedIn) { console.log('用户未登录,显示登录对话框...'); new Notice('需要登录小红书账户'); let loginSuccess = false; const loginModal = new XiaohongshuLoginModal(this.app, () => { console.log('登录成功回调被调用'); loginSuccess = true; }); console.log('打开登录模态窗口...'); await new Promise((resolve) => { const originalClose = loginModal.close; loginModal.close = () => { console.log('登录窗口关闭'); originalClose.call(loginModal); resolve(); }; loginModal.open(); }); console.log('登录结果:', loginSuccess); if (!loginSuccess) { new Notice('登录失败,无法发布到小红书'); return; } } // 读取文件内容 const content = await this.app.vault.read(file); // 转换内容格式 const adapter = new XiaohongshuContentAdapter(); const xiaohongshuPost = adapter.adaptMarkdownToXiaohongshu(content, { generateTitle: true, addStyle: true }); // 发布文章 const result = await api.createPost(xiaohongshuPost); if (result.success) { new Notice('文章已成功发布到小红书!'); } else { new Notice('发布失败: ' + result.message); } } catch (error) { console.error('发布到小红书失败:', error); new Notice('发布失败: ' + (error instanceof Error ? error.message : String(error))); } } }