Chrome插件Wiz Note Ziw Reader

用途:读取Wiz笔记的ziw文件并展示渲染后的网页内容,兼容windows和安卓

前段时间用了一下思源,很优秀,优雅,漂亮.支持局域网服务和网络同步.

可惜不太习惯它的编辑.所以用回了为知笔记(经典版).

可是为知笔记(经典版)没有移动版.

就用Qwen3-Coder-30B-A3B-Instruct做了个小插件.可以简单阅读ziw文件.

全程是AI出力.

不得不感叹AI太厉害了,完全不懂代码的人(我)也能弄些小东西

分享一下代码,懒得上传文件了.

权当记录一下编写过程(虽然我只是一边BB)

没有截图,h1.appinn.net上传图片失败.

共10个文件,

jszip.min.js可以自己下载

https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js

图标自己随便弄一个吧

background.js

// 后台脚本,用于处理扩展的后台逻辑
chrome.runtime.onInstalled.addListener(() => {
  console.log('Wiz Note Reader 插件已安装');
});

// 处理消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'getFileContent') {
    // 处理文件内容获取
    sendResponse({ status: 'success' });
  }
});

// 处理插件图标点击
chrome.action.onClicked.addListener((tab) => {
  // 打开文件选择页面
  chrome.tabs.create({
    url: chrome.runtime.getURL('file-selector.html'),
    active: true
  });
});

content.js

// 内容脚本,用于在页面中执行
console.log('Wiz Note Reader 内容脚本已加载');

// 监听来自popup的事件
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  if (request.action === 'getFileInfo') {
    sendResponse({
      fileName: document.title,
      url: window.location.href
    });
  }
});

// 检测是否在Android设备上运行
function isAndroid() {
  return /Android/i.test(navigator.userAgent);
}

// 为Android设备添加额外的兼容性处理
if (isAndroid()) {
  console.log('检测到Android设备,启用兼容模式');
  
  // 监听Android特定事件
  document.addEventListener('DOMContentLoaded', function() {
    // 在Android上可能需要额外的处理
    console.log('Android环境初始化完成');
  });
}

file-selector.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>选择 Wiz 笔记文件</title>
  <style>
    body {
      margin: 0;
      padding: 20px;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background-color: #f5f5f5;
      min-width: 300px;
      max-width: 600px;
      display: flex;
      flex-direction: column;
      align-items: center;
      min-height: 100vh;
    }
    
    .container {
      background: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
      width: 100%;
      max-width: 500px;
      margin-top: 20px;
    }
    
    h1 {
      font-size: 22px;
      margin-bottom: 20px;
      color: #333;
      text-align: center;
    }
    
    .file-input-wrapper {
      margin-bottom: 20px;
      width: 100%;
    }
    
    #fileInput {
      width: 100%;
      padding: 12px;
      border: 2px dashed #ccc;
      border-radius: 4px;
      cursor: pointer;
      text-align: center;
      transition: border-color 0.3s;
      background-color: #f8f9fa;
    }
    
    #fileInput:hover {
      border-color: #007bff;
      background-color: #e9ecef;
    }
    
    #fileInput:focus {
      outline: none;
      border-color: #007bff;
    }
    
    .file-info {
      margin-top: 10px;
      padding: 10px;
      background-color: #e9ecef;
      border-radius: 4px;
      font-size: 12px;
      word-break: break-all;
    }
    
    .loading {
      text-align: center;
      padding: 20px;
    }
    
    .loading-spinner {
      border: 4px solid #f3f3f3;
      border-top: 4px solid #007bff;
      border-radius: 50%;
      width: 30px;
      height: 30px;
      animation: spin 1s linear infinite;
      margin: 0 auto 10px;
    }
    
    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
    
    .error {
      color: #dc3545;
      background-color: #f8d7da;
      border-color: #f5c6cb;
      padding: 10px;
      border-radius: 4px;
      margin-top: 10px;
    }
    
    .success {
      color: #155724;
      background-color: #d4edda;
      border-color: #c3e6cb;
      padding: 10px;
      border-radius: 4px;
      margin-top: 10px;
    }
    
    button {
      width: 100%;
      padding: 12px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 14px;
      margin-top: 10px;
      transition: background-color 0.2s;
    }
    
    button:hover {
      background-color: #0056b3;
    }
    
    button:disabled {
      background-color: #6c757d;
      cursor: not-allowed;
    }
    
    .instructions {
      margin-top: 20px;
      padding: 10px;
      background-color: #e7f3ff;
      border-radius: 4px;
      font-size: 14px;
      line-height: 1.5;
    }
    
    .instructions h3 {
      margin-top: 0;
      color: #007bff;
    }
    
    .instructions ul {
      padding-left: 20px;
    }
    
    .instructions li {
      margin-bottom: 8px;
    }
    
    /* 响应式设计 */
    @media (max-width: 480px) {
      body {
        padding: 10px;
      }
      
      .container {
        padding: 15px;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>Wiz Note Ziw Reader</h1>
    <div class="file-input-wrapper">
      <input type="file" id="fileInput" accept=".ziw" />
      <div id="fileInfo" class="file-info" style="display: none;"></div>
    </div>
    <div id="status"></div>
    <button id="viewBtn" disabled>查看笔记</button>
    
    <div class="instructions">
      <h3>使用说明</h3>
      <ul>
        <li>点击"选择文件"按钮选择您的 .ziw 笔记文件</li>
        <li>支持 Windows 和 Android 平台</li>
        <li>Android 10 及以上版本支持</li>
        <li>选择文件后点击"查看笔记"按钮</li>
      </ul>
    </div>
  </div>
  
  <script src="file-selector.js"></script>
</body>
</html>

file-selector.js

// 全局变量
let selectedFile = null;

document.addEventListener('DOMContentLoaded', function() {
  const fileInput = document.getElementById('fileInput');
  const viewBtn = document.getElementById('viewBtn');
  const fileInfo = document.getElementById('fileInfo');
  const statusDiv = document.getElementById('status');

  // 文件选择事件
  fileInput.addEventListener('change', function(event) {
    const file = event.target.files[0];
    if (file && file.name.endsWith('.ziw')) {
      selectedFile = file;
      fileInfo.textContent = `文件: ${file.name}\n大小: ${formatFileSize(file.size)}`;
      fileInfo.style.display = 'block';
      viewBtn.disabled = false;
      statusDiv.innerHTML = '';
    } else {
      selectedFile = null;
      fileInfo.style.display = 'none';
      viewBtn.disabled = true;
      statusDiv.innerHTML = '<div class="error">请选择有效的.ziw文件</div>';
    }
  });

  // 查看笔记按钮事件
  viewBtn.addEventListener('click', function() {
    if (!selectedFile) {
      statusDiv.innerHTML = '<div class="error">请选择文件</div>';
      return;
    }

    readZiwFile(selectedFile);
  });
});

// 读取ziw文件
function readZiwFile(file) {
  const statusDiv = document.getElementById('status');
  statusDiv.innerHTML = '<div class="loading"><div class="loading-spinner"></div>正在读取文件...</div>';
  
  // 确保JSZip库已加载
  if (typeof JSZip === 'undefined') {
    loadJSZip().then(() => {
      processZiwFile(file);
    }).catch(error => {
      console.error('加载JSZip失败:', error);
      statusDiv.innerHTML = `<div class="error">加载JSZip库失败: ${error.message}</div>`;
    });
  } else {
    processZiwFile(file);
  }
}

// 加载JSZip库
function loadJSZip() {
  return new Promise((resolve, reject) => {
    // 检查是否已经加载
    if (typeof JSZip !== 'undefined') {
      resolve();
      return;
    }
    
    // 创建script标签加载JSZip
    const script = document.createElement('script');
    script.src = chrome.runtime.getURL('jszip.min.js');
    script.onload = function() {
      console.log('JSZip加载成功');
      resolve();
    };
    script.onerror = function() {
      reject(new Error('JSZip加载失败'));
    };
    document.head.appendChild(script);
  });
}

// 处理ziw文件
function processZiwFile(file) {
  const statusDiv = document.getElementById('status');
  
  const reader = new FileReader();
  
  reader.onload = function(e) {
    try {
      const arrayBuffer = e.target.result;
      const uint8Array = new Uint8Array(arrayBuffer);
      
      // 使用JSZip读取文件
      const zip = new JSZip();
      zip.loadAsync(uint8Array)
        .then(function(zip) {
          console.log('ZIP文件加载成功');
          // 找到主HTML文件
          const mainFile = findMainHtmlFile(zip);
          if (!mainFile) {
            throw new Error('未找到主HTML文件');
          }
          
          console.log('找到主文件:', mainFile);
          // 读取主HTML内容并正确解码
          return zip.file(mainFile).async('arraybuffer');
        })
        .then(function(arrayBuffer) {
          console.log('原始数据大小:', arrayBuffer.byteLength);
          // 解码UTF-16-LE内容
          const decodedContent = decodeUtf16LE(arrayBuffer);
          console.log('解码后内容:', decodedContent.substring(0, 200) + '...');
          // 处理HTML内容,替换图片引用和CSS
          return processHtmlContent(decodedContent, zip);
        })
        .then(function(processedContent) {
          console.log('处理完成,准备显示');
          // 创建临时URL显示内容
          const url = createViewerUrl(processedContent);
          chrome.tabs.create({ url: url, active: true });
          statusDiv.innerHTML = '<div class="success">文件读取成功,正在打开查看器</div>';
        })
        .catch(function(error) {
          console.error('读取文件错误:', error);
          statusDiv.innerHTML = `<div class="error">读取错误: ${error.message}</div>`;
        });
    } catch (error) {
      console.error('文件读取错误:', error);
      statusDiv.innerHTML = `<div class="error">文件读取错误: ${error.message}</div>`;
    }
  };
  
  reader.onerror = function() {
    statusDiv.innerHTML = '<div class="error">文件读取失败</div>';
  };
  
  reader.readAsArrayBuffer(file);
}

// 解码UTF-16-LE内容
function decodeUtf16LE(arrayBuffer) {
  try {
    const uint8Array = new Uint8Array(arrayBuffer);
    
    // 检查BOM
    const hasBOM = uint8Array[0] === 0xFF && uint8Array[1] === 0xFE;
    
    // 如果有BOM,跳过前两个字节
    const startOffset = hasBOM ? 2 : 0;
    
    // 创建UTF-16字符串
    let result = '';
    for (let i = startOffset; i < uint8Array.length; i += 2) {
      const byte1 = uint8Array[i];
      const byte2 = uint8Array[i + 1];
      if (byte1 !== 0 || byte2 !== 0) {
        const code = (byte2 << 8) | byte1;
        result += String.fromCharCode(code);
      }
    }
    
    return result;
  } catch (error) {
    console.error('UTF-16-LE解码错误:', error);
    // 如果解码失败,尝试用其他方法
    return new TextDecoder('utf-16le').decode(arrayBuffer);
  }
}

// 查找主HTML文件
function findMainHtmlFile(zip) {
  // 按照Wiz笔记的常见结构查找
  const htmlFiles = Object.keys(zip.files).filter(name => 
    name.toLowerCase().endsWith('.html') || name.toLowerCase().endsWith('.htm')
  );
  
  console.log('找到的HTML文件:', htmlFiles);
  
  // 优先查找index.html或main.html
  const mainFiles = htmlFiles.filter(name => 
    name.toLowerCase().includes('index') || name.toLowerCase().includes('main')
  );
  
  if (mainFiles.length > 0) {
    return mainFiles[0];
  }
  
  // 如果没有特定命名的文件,返回第一个HTML文件
  return htmlFiles.length > 0 ? htmlFiles[0] : null;
}

// 处理HTML内容,替换图片引用和CSS
function processHtmlContent(htmlContent, zip) {
  return new Promise((resolve, reject) => {
    try {
      // 创建一个临时的DOM来处理HTML
      const tempDiv = document.createElement('div');
      tempDiv.innerHTML = htmlContent;

      // 查找所有CSS相关的内容
      const cssFiles = findCssFiles(zip);
      console.log('找到的CSS文件:', cssFiles);

      // 处理CSS文件 - 修改为处理多个CSS文件
      const cssPromises = cssFiles.map(cssFile => {
        return zip.file(cssFile).async('arraybuffer')
          .then(function(arrayBuffer) {
            console.log('成功读取CSS文件:', cssFile);
            // 使用UTF-16LE解码CSS内容
            const cssContent = decodeUtf16LE(arrayBuffer);
            return { fileName: cssFile, content: cssContent };
          })
          .catch(error => {
            console.error('读取CSS文件失败:', cssFile, error);
            return null;
          });
      });

      Promise.all(cssPromises).then(cssResults => {
        // 过滤掉失败的CSS文件
        const validCss = cssResults.filter(result => result !== null);
        console.log('有效的CSS文件数量:', validCss.length);
        
        // 为每个CSS文件创建style标签
        let cssStyleTags = '';
        validCss.forEach(css => {
          // 转义CSS内容中的特殊字符,防止HTML解析错误
          const escapedCss = css.content
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
          cssStyleTags += `<style>${escapedCss}</style>`;
        });
        console.log('CSS样式标签内容(已转义):', cssStyleTags.substring(0, 100) + '...');

        // 处理图片元素
        const imgElements = tempDiv.querySelectorAll('img');
        console.log('找到图片元素数量:', imgElements.length);

        // 如果没有图片和CSS,直接返回
        if (imgElements.length === 0 && cssStyleTags.length === 0) {
          console.log('没有图片和CSS,直接返回');
          resolve(tempDiv.innerHTML);
          return;
        }

        // 为每个图片创建独立的处理任务
        const promises = [];
        
        imgElements.forEach((imgElement, index) => {
          const src = imgElement.getAttribute('src') || '';
          if (!src) {
            promises.push(Promise.resolve());
            return;
          }

          console.log(`处理第 ${index + 1} 张图片:`, src);
          
          const promise = processSingleImageWithIndex(src, zip, imgElement, index);
          promises.push(promise);
        });

        Promise.all(promises).then(() => {
          // 检查原始HTML是否包含head标签
          let htmlWithStyles = tempDiv.innerHTML;
          console.log('原始HTML长度:', htmlWithStyles.length);
          
          // 如果HTML中没有head标签,需要手动添加
          if (!htmlWithStyles.includes('</head>')) {
            console.log('HTML中没有head标签,需要添加');
            // 确保HTML结构完整
            if (htmlWithStyles.includes('<head>')) {
              // 如果有head标签但没有闭合标签
              htmlWithStyles = htmlWithStyles.replace('</head>', `${cssStyleTags}</head>`);
            } else if (htmlWithStyles.includes('<html')) {
              // 如果有html标签但没有head标签,插入head标签
              htmlWithStyles = htmlWithStyles.replace('<html', '<html><head>' + cssStyleTags + '</head>');
            } else {
              // 如果完全没有任何结构,创建完整的HTML
              htmlWithStyles = `<html><head>${cssStyleTags}</head><body>${htmlWithStyles}</body></html>`;
            }
          } else {
            // 如果有head标签,正常替换
            htmlWithStyles = htmlWithStyles.replace('</head>', `${cssStyleTags}</head>`);
          }
          
          console.log('处理后的HTML长度:', htmlWithStyles.length);
          resolve(htmlWithStyles);
        }).catch(reject);
      }).catch(reject);

    } catch (error) {
      console.error('处理HTML内容时出错:', error);
      reject(error);
    }
  });
}

// 查找CSS文件 - 改进版本,更可靠地查找CSS文件
function findCssFiles(zip) {
  const allFiles = Object.keys(zip.files);
  console.log('所有文件:', allFiles);
  
  // 方法1: 直接查找所有.css文件
  const cssFiles = allFiles.filter(name => 
    name.toLowerCase().endsWith('.css')
  );
  
  console.log('直接匹配的CSS文件:', cssFiles);
  
  // 如果没有找到,尝试通过HTML内容分析
  if (cssFiles.length === 0) {
    console.log('未找到CSS文件,尝试通过HTML内容分析');
    // 这里可以添加从HTML中提取CSS引用的逻辑
  }
  
  return cssFiles;
}

// 处理单个图片元素(带索引确保独立性)
function processSingleImageWithIndex(src, zip, imgElement, index) {
  return new Promise((resolve, reject) => {
    try {
      // 检查是否是相对路径
      let imagePath = src;
      if (src.startsWith('./') || src.startsWith('../')) {
        imagePath = src.substring(2);
      }

      // 精确查找图片文件
      const allFiles = Object.keys(zip.files);
      let foundFile = null;
      
      // 优先完全匹配
      foundFile = allFiles.find(name => 
        name.toLowerCase() === imagePath.toLowerCase()
      );
      
      // 如果没有完全匹配,尝试模糊匹配
      if (!foundFile) {
        foundFile = allFiles.find(name => {
          const lowerName = name.toLowerCase();
          const lowerSrc = imagePath.toLowerCase();
          return lowerName.includes(lowerSrc) && 
                 (lowerName.endsWith('.jpg') || 
                  lowerName.endsWith('.jpeg') || 
                  lowerName.endsWith('.png') || 
                  lowerName.endsWith('.gif') || 
                  lowerName.endsWith('.bmp') || 
                  lowerName.endsWith('.webp'));
        });
      }
      
      // 如果还是没找到,尝试基于文件名匹配
      if (!foundFile) {
        foundFile = allFiles.find(name => {
          const fileName = name.split('/').pop().toLowerCase();
          const srcName = imagePath.split('/').pop().toLowerCase();
          return fileName === srcName;
        });
      }

      console.log(`图片 ${index + 1} 查找结果:`, foundFile);

      if (!foundFile) {
        console.log('未找到图片文件:', src);
        resolve();
        return;
      }

      console.log('使用图片文件:', foundFile);

      // 读取图片文件并转换为base64
      zip.file(foundFile).async('base64')
        .then(function(base64Data) {
          const mimeType = getImageMimeType(foundFile);
          const dataUrl = `data:${mimeType};base64,${base64Data}`;
          // 确保只修改当前图片元素
          imgElement.setAttribute('src', dataUrl);
          console.log(`成功替换第 ${index + 1} 张图片:`, src, '->', foundFile);
          resolve();
        })
        .catch(function(error) {
          console.error('读取图片失败:', error);
          resolve(); // 即使图片读取失败也继续
        });

    } catch (error) {
      console.error('处理图片失败:', error);
      resolve();
    }
  });
}

// 获取图片MIME类型
function getImageMimeType(filename) {
  const lowerName = filename.toLowerCase();
  if (lowerName.endsWith('.jpg') || lowerName.endsWith('.jpeg')) {
    return 'image/jpeg';
  } else if (lowerName.endsWith('.png')) {
    return 'image/png';
  } else if (lowerName.endsWith('.gif')) {
    return 'image/gif';
  } else if (lowerName.endsWith('.bmp')) {
    return 'image/bmp';
  } else if (lowerName.endsWith('.webp')) {
    return 'image/webp';
  }
  return 'image/png'; // 默认
}


// 创建查看器URL
function createViewerUrl(content) {
  // 创建一个完整的HTML页面
  const viewerContent = `
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
</head>
<body>
${content}
</body>
</html>
  `;
  
  // 使用blob URL
  const blob = new Blob([viewerContent], { type: 'text/html;charset=utf-8' });
  return URL.createObjectURL(blob);
}

// 格式化文件大小
function formatFileSize(bytes) {
  if (bytes === 0) return '0 Bytes';
  
  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

icon16.png

icon48.png

icon128.png

jszip.min.js

manifest.json

{
  "manifest_version": 3,
  "name": "Wiz Note Ziw Reader",
  "version": "1.0.0",
  "description": "读取Wiz笔记的ziw文件并展示渲染后的网页内容",
  "icons": {
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  },
  "permissions": [
    "activeTab",
    "storage",
    "tabs"
  ],
  "action": {
    "default_popup": "",
    "default_title": "Wiz Note Reader"
  },
  "background": {
    "service_worker": "background.js"
  },
  "web_accessible_resources": [
    {
      "resources": ["jszip.min.js", "file-selector.html"],
      "matches": ["<all_urls>"]
    }
  ],
  "host_permissions": [
    "<all_urls>"
  ]
}

styles.css

/* 通用样式 */
body {
  margin: 0;
  padding: 10px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background-color: #f5f5f5;
  min-width: 300px;
  max-width: 600px;
}

.container {
  background: white;
  border-radius: 8px;
  padding: 20px;
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

h2 {
  margin-top: 0;
  color: #333;
  font-size: 18px;
}

.file-input-container {
  margin-bottom: 15px;
}

.btn {
  padding: 10px 15px;
  background-color: #4285f4;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  transition: background-color 0.2s;
  text-align: center;
}

.btn:hover {
  background-color: #3367d6;
}

.btn:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.btn-secondary {
  background-color: #f1f1f1;
  color: #333;
}

.btn-secondary:hover {
  background-color: #e0e0e0;
}

.controls {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 15px;
}

.file-name {
  font-size: 12px;
  color: #666;
  word-break: break-all;
  margin-top: 5px;
}

.status {
  padding: 10px;
  border-radius: 4px;
  font-size: 13px;
  margin-bottom: 15px;
}

.status.success {
  background-color: #d4edda;
  color: #155724;
  border: 1px solid #c3e6cb;
}

.status.error {
  background-color: #f8d7da;
  color: #721c24;
  border: 1px solid #f5c6cb;
}

.status.info {
  background-color: #d1ecf1;
  color: #0c5460;
  border: 1px solid #bee5eb;
}

.preview-container {
  border: 1px solid #ddd;
  border-radius: 4px;
  padding: 10px;
  margin-top: 15px;
}

.preview-container h3 {
  margin-top: 0;
  margin-bottom: 10px;
  color: #333;
}

iframe {
  width: 100%;
  height: 400px;
  border: none;
  background: white;
}

/* 响应式设计 */
@media (max-width: 480px) {
  .container {
    padding: 15px;
  }
  
  .controls {
    flex-direction: column;
  }
  
  .btn {
    width: 100%;
  }
}

曾经官方上架过Google Play,最终版本号8.2.1

那个移动版,不太好用

我之前手机上只是用来阅读保存的文章,所以没啥问题。
为知作为一款过去的软件,感觉继续投入意义不大,我已被 @tiger 忽悠去了Zotero :face_savoring_food:

对于md类笔记软件和编辑器实在用不习惯.

包括论坛

在emeditor打了些文字,一复制到论坛.换行全部变成空格了.有必要这么别扭吗?

我也一直没找到趁手的md软件,我看坛子里也有别人在找。
md比Word好的地方是纯文本。

emeditor是我最喜欢的文本编辑器,没有之一。
你的问题,粘贴或发布回复前在格式栏最左边把md编辑器切换成富文本编辑器试试。