用途:读取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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
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%;
}
}