|
|
@@ -0,0 +1,572 @@
|
|
|
+const { ipcMain, BrowserWindow } = require('electron');
|
|
|
+const path = require('path');
|
|
|
+const fs = require('fs');
|
|
|
+
|
|
|
+let winax;
|
|
|
+try {
|
|
|
+ winax = require('winax');
|
|
|
+ console.log('✅ Winax loaded successfully');
|
|
|
+} catch (error) {
|
|
|
+ console.warn('❌ Winax not available:', error.message);
|
|
|
+}
|
|
|
+
|
|
|
+class WPSHelper {
|
|
|
+ constructor() {
|
|
|
+ this.wpsApp = null;
|
|
|
+ this.activeDocument = null;
|
|
|
+ this.isConnected = false;
|
|
|
+ this.documents = new Map();
|
|
|
+ this.checkInterval = null;
|
|
|
+ this.lastParagraphContent = '';
|
|
|
+
|
|
|
+ this.startMonitoring();
|
|
|
+ console.log('🔄 WPSHelper initialized');
|
|
|
+ }
|
|
|
+
|
|
|
+ connectToWPS() {
|
|
|
+ if (!winax) {
|
|
|
+ console.warn('⚠️ Winax not available, using fallback mode');
|
|
|
+ this.setupFallbackMode();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.wpsApp = new winax.Object('KWPS.Application');
|
|
|
+ this.wpsApp.Visible = true;
|
|
|
+ this.isConnected = true;
|
|
|
+
|
|
|
+ console.log('✅ Connected to WPS');
|
|
|
+ this.setupEventListeners();
|
|
|
+ this.startMonitoring();
|
|
|
+
|
|
|
+ this.sendWPSStatus();
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ Failed to connect to WPS:', error);
|
|
|
+ this.isConnected = false;
|
|
|
+ this.setupFallbackMode();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setupEventListeners() {
|
|
|
+ this.lastActiveDocument = null;
|
|
|
+ this.lastDocumentCount = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ startMonitoring() {
|
|
|
+ if (this.checkInterval) {
|
|
|
+ clearInterval(this.checkInterval);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.checkInterval = setInterval(() => {
|
|
|
+ this.checkWPSStatus();
|
|
|
+ this.updateDocumentsList();
|
|
|
+ }, 2000);
|
|
|
+ }
|
|
|
+
|
|
|
+ checkWPSStatus() {
|
|
|
+ try {
|
|
|
+ if (this.wpsApp && this.isConnected) {
|
|
|
+ try {
|
|
|
+ const name = this.wpsApp.Name;
|
|
|
+ this.isConnected = true;
|
|
|
+ } catch (error) {
|
|
|
+ this.isConnected = false;
|
|
|
+ this.wpsApp = null;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.isConnected = false;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ this.isConnected = false;
|
|
|
+ this.wpsApp = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ updateDocumentsList() {
|
|
|
+ if (!this.isConnected || !this.wpsApp) {
|
|
|
+ if (!this.isConnected) {
|
|
|
+ this.simulateDocumentsList();
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const newDocs = new Map();
|
|
|
+ let documents;
|
|
|
+
|
|
|
+ try {
|
|
|
+ documents = this.wpsApp.Documents;
|
|
|
+ } catch (error) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let i = 1; i <= documents.Count; i++) {
|
|
|
+ try {
|
|
|
+ const doc = documents.Item(i);
|
|
|
+ const fullName = doc.FullName;
|
|
|
+ const name = doc.Name;
|
|
|
+
|
|
|
+ newDocs.set(fullName, {
|
|
|
+ name: name,
|
|
|
+ path: fullName,
|
|
|
+ document: doc,
|
|
|
+ isActive: doc === this.activeDocument
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Error accessing document:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.documents = newDocs;
|
|
|
+ this.sendDocumentsList();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error updating documents list:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getFullParagraphContent() {
|
|
|
+ if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
|
|
|
+ return this.getFallbackParagraphContent();
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const selection = this.wpsApp.Selection;
|
|
|
+ if (!selection) {
|
|
|
+ return this.getFallbackParagraphContent();
|
|
|
+ }
|
|
|
+
|
|
|
+ const originalStart = selection.Start;
|
|
|
+ const originalEnd = selection.End;
|
|
|
+
|
|
|
+ try {
|
|
|
+ selection.Expand(5); // wdParagraph
|
|
|
+
|
|
|
+ const text = selection.Text;
|
|
|
+ const revisions = this.getRevisionsInSelection(selection);
|
|
|
+ const comments = this.getCommentsInSelection(selection);
|
|
|
+
|
|
|
+ selection.SetRange(originalStart, originalEnd);
|
|
|
+
|
|
|
+ return {
|
|
|
+ text: text.trim(),
|
|
|
+ html: text.trim(),
|
|
|
+ revisions: revisions,
|
|
|
+ comments: comments,
|
|
|
+ hasRevisions: revisions.length > 0,
|
|
|
+ hasComments: comments.length > 0
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ selection.SetRange(originalStart, originalEnd);
|
|
|
+ return this.getFallbackParagraphContent();
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ return this.getFallbackParagraphContent();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getRevisionsInSelection(selection) {
|
|
|
+ const revisions = [];
|
|
|
+ if (!selection || !selection.Revisions) return revisions;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const revisionsCount = selection.Revisions.Count;
|
|
|
+ for (let i = 1; i <= revisionsCount; i++) {
|
|
|
+ try {
|
|
|
+ const revision = selection.Revisions.Item(i);
|
|
|
+ revisions.push({
|
|
|
+ type: 'insert',
|
|
|
+ author: revision.Author || '未知作者',
|
|
|
+ date: revision.Date || new Date(),
|
|
|
+ text: revision.Range.Text || '',
|
|
|
+ index: i
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Error getting revision:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Could not access revisions:', error.message);
|
|
|
+ }
|
|
|
+
|
|
|
+ return revisions;
|
|
|
+ }
|
|
|
+
|
|
|
+ getCommentsInSelection(selection) {
|
|
|
+ const comments = [];
|
|
|
+ if (!selection || !selection.Comments) return comments;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const commentsCount = selection.Comments.Count;
|
|
|
+ for (let i = 1; i <= commentsCount; i++) {
|
|
|
+ try {
|
|
|
+ const comment = selection.Comments.Item(i);
|
|
|
+ comments.push({
|
|
|
+ author: comment.Author || '未知作者',
|
|
|
+ date: comment.Date || new Date(),
|
|
|
+ text: comment.Range.Text || '',
|
|
|
+ index: i
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Error getting comment:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Could not access comments:', error.message);
|
|
|
+ }
|
|
|
+
|
|
|
+ return comments;
|
|
|
+ }
|
|
|
+
|
|
|
+ getFallbackParagraphContent() {
|
|
|
+ return {
|
|
|
+ text: this.lastParagraphContent || '',
|
|
|
+ html: this.lastParagraphContent || '',
|
|
|
+ revisions: [],
|
|
|
+ comments: [],
|
|
|
+ hasRevisions: false,
|
|
|
+ hasComments: false
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ updateParagraphWithRevisions(newContent) {
|
|
|
+ if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
|
|
|
+ return { success: false, error: 'WPS not connected' };
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const selection = this.wpsApp.Selection;
|
|
|
+ if (!selection) {
|
|
|
+ return { success: false, error: 'No selection available' };
|
|
|
+ }
|
|
|
+
|
|
|
+ const originalStart = selection.Start;
|
|
|
+ const originalEnd = selection.End;
|
|
|
+
|
|
|
+ try {
|
|
|
+ selection.Expand(5); // wdParagraph
|
|
|
+ const originalText = selection.Text.trim();
|
|
|
+
|
|
|
+ if (originalText === newContent.trim()) {
|
|
|
+ return { success: true, unchanged: true };
|
|
|
+ }
|
|
|
+
|
|
|
+ selection.Text = newContent;
|
|
|
+ this.lastParagraphContent = newContent;
|
|
|
+
|
|
|
+ return {
|
|
|
+ success: true,
|
|
|
+ originalContent: originalText,
|
|
|
+ newContent: newContent,
|
|
|
+ hasChanges: true
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ } finally {
|
|
|
+ try {
|
|
|
+ selection.SetRange(originalStart, originalStart);
|
|
|
+ } catch (e) {}
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ navigateParagraph(direction) {
|
|
|
+ if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const selection = this.wpsApp.Selection;
|
|
|
+ if (!selection) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (direction === 'prev') {
|
|
|
+ selection.MoveUp(5, 1); // wdParagraph
|
|
|
+ } else if (direction === 'next') {
|
|
|
+ selection.MoveDown(5, 1); // wdParagraph
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error navigating paragraphs:', error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ openDocument(filePath) {
|
|
|
+ if (!this.isConnected || !this.wpsApp) {
|
|
|
+ console.log('WPS not connected, using fallback');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (!fs.existsSync(filePath)) {
|
|
|
+ throw new Error('File does not exist: ' + filePath);
|
|
|
+ }
|
|
|
+
|
|
|
+ const doc = this.wpsApp.Documents.Open(filePath);
|
|
|
+ this.activeDocument = doc;
|
|
|
+
|
|
|
+ this.documents.set(filePath, {
|
|
|
+ name: doc.Name,
|
|
|
+ path: filePath,
|
|
|
+ document: doc,
|
|
|
+ isActive: true
|
|
|
+ });
|
|
|
+
|
|
|
+ this.sendDocumentsList();
|
|
|
+ this.sendActiveDocumentChange();
|
|
|
+
|
|
|
+ console.log('Document opened successfully:', path.basename(filePath));
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error opening document:', error);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ switchToDocument(filePath) {
|
|
|
+ if (!this.isConnected || !this.wpsApp) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const docInfo = this.documents.get(filePath);
|
|
|
+ if (docInfo && docInfo.document) {
|
|
|
+ try {
|
|
|
+ docInfo.document.Activate();
|
|
|
+ this.activeDocument = docInfo.document;
|
|
|
+ this.sendActiveDocumentChange();
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error switching document:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ handleRevision(action, revisionIndex) {
|
|
|
+ if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
|
|
|
+ return { success: false, error: 'WPS not connected' };
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (revisionIndex !== null) {
|
|
|
+ const revision = this.activeDocument.Revisions.Item(revisionIndex);
|
|
|
+ if (action === 'accept') {
|
|
|
+ revision.Accept();
|
|
|
+ } else {
|
|
|
+ revision.Reject();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const revisions = this.activeDocument.Revisions;
|
|
|
+ if (action === 'accept') {
|
|
|
+ revisions.AcceptAll();
|
|
|
+ } else {
|
|
|
+ revisions.RejectAll();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { success: true };
|
|
|
+ } catch (error) {
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ addComment(commentText) {
|
|
|
+ if (!this.isConnected || !this.wpsApp || !this.activeDocument) {
|
|
|
+ return { success: false, error: 'WPS not connected' };
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const selection = this.wpsApp.Selection;
|
|
|
+ if (!selection) {
|
|
|
+ return { success: false, error: 'No selection available' };
|
|
|
+ }
|
|
|
+
|
|
|
+ const comment = selection.Comments.Add(selection.Range);
|
|
|
+ comment.Range.Text = commentText;
|
|
|
+
|
|
|
+ return { success: true, comment: commentText };
|
|
|
+ } catch (error) {
|
|
|
+ return { success: false, error: error.message };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setTrackRevisions(track) {
|
|
|
+ if (!this.isConnected || !this.wpsApp) return false;
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.wpsApp.ActiveDocument.TrackRevisions = track;
|
|
|
+ return true;
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Could not set track revisions:', error.message);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setupFallbackMode() {
|
|
|
+ console.log('Setting up fallback mode');
|
|
|
+ this.isConnected = false;
|
|
|
+ this.startMonitoring();
|
|
|
+ setTimeout(() => {
|
|
|
+ this.simulateDocumentsList();
|
|
|
+ }, 2000);
|
|
|
+ }
|
|
|
+
|
|
|
+ simulateDocumentsList() {
|
|
|
+ const mockDocuments = [
|
|
|
+ {
|
|
|
+ name: '示例文档.docx',
|
|
|
+ path: 'C:\\Documents\\示例文档.docx',
|
|
|
+ isActive: true
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ this.sendDocumentsList(mockDocuments);
|
|
|
+
|
|
|
+ const sampleParagraph = `这是一个示例段落内容。在回退模式下,您可以查看界面功能,但实际的WPS文档操作需要安装WPS并确保Winax正常工作。
|
|
|
+
|
|
|
+当前功能包括:
|
|
|
+• 文档列表显示
|
|
|
+• 段落内容显示
|
|
|
+• 基本的导航操作
|
|
|
+• 文件拖拽处理
|
|
|
+
|
|
|
+要启用完整的WPS集成功能,请确保:
|
|
|
+1. 已安装WPS Office
|
|
|
+2. Winax模块正确安装
|
|
|
+3. 应用程序有足够的权限访问COM组件`;
|
|
|
+
|
|
|
+ this.sendFullParagraphContent({
|
|
|
+ text: sampleParagraph,
|
|
|
+ html: sampleParagraph,
|
|
|
+ revisions: [],
|
|
|
+ comments: [],
|
|
|
+ hasRevisions: false,
|
|
|
+ hasComments: false
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ sendWPSStatus() {
|
|
|
+ if (ipcMain) {
|
|
|
+ const windows = BrowserWindow.getAllWindows();
|
|
|
+ windows.forEach(window => {
|
|
|
+ if (window && !window.isDestroyed()) {
|
|
|
+ try {
|
|
|
+ window.webContents.send('wps-status-changed', {
|
|
|
+ connected: this.isConnected,
|
|
|
+ hasActiveDocument: !!this.activeDocument,
|
|
|
+ documentCount: this.documents.size,
|
|
|
+ timestamp: new Date().toISOString()
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Error sending WPS status:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ sendDocumentsList(documents = null) {
|
|
|
+ if (ipcMain) {
|
|
|
+ const docs = documents || Array.from(this.documents.values()).map(doc => ({
|
|
|
+ name: doc.name,
|
|
|
+ path: doc.path,
|
|
|
+ isActive: doc.document === this.activeDocument
|
|
|
+ }));
|
|
|
+
|
|
|
+ const windows = BrowserWindow.getAllWindows();
|
|
|
+ windows.forEach(window => {
|
|
|
+ if (window && !window.isDestroyed()) {
|
|
|
+ try {
|
|
|
+ window.webContents.send('documents-list-changed', docs);
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Error sending documents list:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ sendActiveDocumentChange() {
|
|
|
+ if (ipcMain && this.activeDocument) {
|
|
|
+ const windows = BrowserWindow.getAllWindows();
|
|
|
+ windows.forEach(window => {
|
|
|
+ if (window && !window.isDestroyed()) {
|
|
|
+ try {
|
|
|
+ window.webContents.send('active-document-changed', {
|
|
|
+ name: this.activeDocument.Name,
|
|
|
+ path: this.activeDocument.FullName
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Error sending active document change:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ sendFullParagraphContent(content) {
|
|
|
+ if (ipcMain) {
|
|
|
+ const windows = BrowserWindow.getAllWindows();
|
|
|
+ windows.forEach(window => {
|
|
|
+ if (window && !window.isDestroyed()) {
|
|
|
+ try {
|
|
|
+ window.webContents.send('full-paragraph-content-changed', content);
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Error sending full paragraph content:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ getStatus() {
|
|
|
+ if (!this.isConnected) this.connectToWPS();
|
|
|
+ return {
|
|
|
+ connected: this.isConnected,
|
|
|
+ activeDocument: this.activeDocument ? {
|
|
|
+ name: this.activeDocument.Name,
|
|
|
+ path: this.activeDocument.FullName
|
|
|
+ } : null,
|
|
|
+ documents: Array.from(this.documents.values()).map(doc => ({
|
|
|
+ name: doc.name,
|
|
|
+ path: doc.path,
|
|
|
+ isActive: doc.document === this.activeDocument
|
|
|
+ })),
|
|
|
+ currentParagraph: this.lastParagraphContent,
|
|
|
+ timestamp: new Date().toISOString()
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ cleanup() {
|
|
|
+ console.log('Cleaning up WPSHelper resources...');
|
|
|
+
|
|
|
+ if (this.checkInterval) {
|
|
|
+ clearInterval(this.checkInterval);
|
|
|
+ this.checkInterval = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.wpsApp) {
|
|
|
+ try {
|
|
|
+ console.log('WPS application reference cleaned');
|
|
|
+ } catch (error) {
|
|
|
+ console.warn('Error cleaning WPS application:', error);
|
|
|
+ }
|
|
|
+ this.wpsApp = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.activeDocument = null;
|
|
|
+ this.isConnected = false;
|
|
|
+ this.documents.clear();
|
|
|
+
|
|
|
+ console.log('WPSHelper cleanup completed');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const wpsHelper = new WPSHelper();
|
|
|
+module.exports = { wpsHelper };
|