Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Wandao Release Notes

## 1.5.2

这是一次面向 Markdown 阅读稳定性和长任务恢复体验的修复版本。建议升级,尤其是经常阅读带图片的导出文档、教程公告或需要在导出过程中查看文件的用户。

### Markdown 图片与教程阅读

- Markdown 阅读器、实时预览、教程公告和飞书教程统一持久化图片资源,重新打开时优先使用本地缓存,减少重复下载。
- 图片尚未读取完成前显示明确的加载状态和进度,不再先展示不完整正文,避免误以为文章没有图片。
- 图片加载失败或超时后保留可重试占位,不会让页面一直卡在等待状态。

### 导出中阅读与长任务稳定性

- 导出过程可以打开 Markdown 阅读器查看输出目录;正文、图片和附件没有完成前不会展示不完整文章。
- 增加导出阅读看门狗:无进展超过 2 分钟或总等待超过 30 分钟时停止等待并提示到任务中心排查。
- 知识星球增量任务会在 checkpoint 后继续补读历史页面,避免只检查最新内容后误报完成。
- 飞书知识库和云空间递归目录读取继续使用心跳续期机制,减少大型目录被固定超时提前截断。

### 下载与安装

- **Windows x64**:下载并运行 `Wandao-1.5.2-win-x64-setup.exe`。
- **macOS Apple Silicon**:下载 `Wandao-1.5.2-macOS-arm64.zip`,解压后将 `Wandao.app` 拖入“应用程序”。
- Windows 系统显示名称为“万能导”;更新标识、可执行文件名和现有用户数据目录保持不变,可直接覆盖升级。

程序内更新所需的签名资产会随正式 Release 一起发布,普通用户无需单独下载 `.sig`、`latest.json` 或校验文件。Windows 正式 Release 只保留一份安装包,下载和程序内更新共用同一份资产。

当前 Windows 和 macOS 安装包尚未配置商业代码签名,系统可能显示未知发布者或 Gatekeeper 提示。

### 插件更新

- 飞书插件升级至 v1.0.21,知识星球插件升级至 v1.0.17;两个插件兼容尚未包含心跳看门狗的旧版主程序,避免插件更新后在启动阶段导入失败。

## 如果项目帮到你了,还请点个star支持一下吧 !

## 1.5.1

这是一次面向飞书导入可靠性和 Markdown 阅读体验的修复版本。建议升级,尤其是需要批量导入、继续历史任务或在本地整理 Markdown 文档的用户。
Expand Down
50 changes: 46 additions & 4 deletions plugins/feishu/backend/export_feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@

from wandao_core.browser import (
CDPClient,
console_heartbeat_watchdog,
DEFAULT_PORT,
ExportError,
ExportStopped,
Expand All @@ -69,6 +68,26 @@
throttle_request,
wait_for_debug_port,
)

try:
from wandao_core.browser import console_heartbeat_watchdog
except ImportError:
# Plugins can be updated before the desktop core. Keep long-running
# exports usable on older installations without heartbeat support.
_CDP_WATCHDOG_SUPPORTED = False

def console_heartbeat_watchdog(
prefix: str,
on_heartbeat: Callable[[dict[str, Any], int], None] | None = None,
) -> Callable[[dict[str, Any]], bool]:
del prefix, on_heartbeat

def observe(_message: dict[str, Any]) -> bool:
return False

return observe
else:
_CDP_WATCHDOG_SUPPORTED = True
from wandao_core.checkpoint import add_checkpoint_args, open_checkpoint_from_args
from wandao_core.output_layout import add_output_layout_args, resolve_output_directory
from wandao_cli import extend_arg_list_from_file
Expand Down Expand Up @@ -1433,7 +1452,8 @@ def load_wiki_tree(
throttle_request(args)
wait_for_wiki_ready(cdp, timeout=35, args=args, expected_url=wiki_url)
try:
value = cdp.evaluate(
value = evaluate_with_watchdog(
cdp,
f"({FEISHU_TREE_LOADER_JS})({js_string(start_token)})",
timeout=FEISHU_TREE_WATCHDOG_IDLE_SECONDS,
max_timeout=FEISHU_TREE_WATCHDOG_MAX_SECONDS,
Expand All @@ -1460,7 +1480,8 @@ def load_drive_folder_tree(
throttle_request(args)
wait_for_wiki_ready(cdp, timeout=35, args=args, expected_url=folder_url)
try:
value = cdp.evaluate(
value = evaluate_with_watchdog(
cdp,
f"({FEISHU_DRIVE_FOLDER_LOADER_JS})({js_string(start_token)})",
timeout=FEISHU_TREE_WATCHDOG_IDLE_SECONDS,
max_timeout=FEISHU_TREE_WATCHDOG_MAX_SECONDS,
Expand Down Expand Up @@ -2661,6 +2682,26 @@ def report(payload: dict[str, Any], elapsed: int) -> None:
return console_heartbeat_watchdog(FEISHU_TREE_WATCHDOG_PREFIX, report)


def evaluate_with_watchdog(
cdp: CDPClient,
expression: str,
*,
timeout: int,
max_timeout: int,
watchdog: Callable[[dict[str, Any]], bool],
) -> Any:
"""Use heartbeat support when available, while retaining old-core compatibility."""

if _CDP_WATCHDOG_SUPPORTED:
return cdp.evaluate(
expression,
timeout=timeout,
max_timeout=max_timeout,
watchdog=watchdog,
)
return cdp.evaluate(expression, timeout=max_timeout)


def extract_doc_markdown_current(
cdp: CDPClient,
node: dict[str, Any],
Expand Down Expand Up @@ -2706,7 +2747,8 @@ def evaluate_markdown_file(allow_preview_fallback: bool) -> dict[str, Any]:
value = evaluate_markdown_file(True)
else:
wait_for_doc_ready(cdp, timeout=35, args=args, node=node)
value = cdp.evaluate(
value = evaluate_with_watchdog(
cdp,
f"({FEISHU_CONVERTER_JS})({js_string(node.get('title') or '未命名')})",
timeout=FEISHU_DOCUMENT_WATCHDOG_IDLE_SECONDS,
max_timeout=FEISHU_DOCUMENT_WATCHDOG_MAX_SECONDS,
Expand Down
2 changes: 1 addition & 1 deletion plugins/feishu/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"id": "feishu",
"name": "飞书",
"description": "飞书 Wiki Markdown 导出与导入,包含目录、图片、权限检测和断点续跑。",
"version": "1.0.20",
"version": "1.0.21",
"publisher": "Wandao Official",
"homepage": "https://www.feishu.cn/",
"license": "AGPL-3.0-only",
Expand Down
45 changes: 43 additions & 2 deletions plugins/zsxq/backend/export_zsxq.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
ExportStopped,
check_stopped,
chrome_debug_available,
console_heartbeat_watchdog,
default_data_dir,
default_state_path,
emit,
Expand All @@ -68,6 +67,27 @@
stop_requested,
wait_for_debug_port,
)

try:
from wandao_core.browser import console_heartbeat_watchdog
except ImportError:
# Plugins can be updated before the desktop core. Keep the long-running
# export usable on older installations and let the core's fixed timeout
# handle the evaluation when heartbeat support is unavailable.
_CDP_WATCHDOG_SUPPORTED = False

def console_heartbeat_watchdog(
prefix: str,
on_heartbeat: Callable[[dict[str, Any], int], None] | None = None,
) -> Callable[[dict[str, Any]], bool]:
del prefix, on_heartbeat

def observe(_message: dict[str, Any]) -> bool:
return False

return observe
else:
_CDP_WATCHDOG_SUPPORTED = True
from wandao_core.report import finalize_report
from wandao_core.checkpoint import CheckpointLeaseLostError, WandaoCheckpoint
from wandao_core.output_layout import add_output_layout_args, resolve_output_directory
Expand Down Expand Up @@ -391,6 +411,26 @@ def report(payload: dict[str, Any], elapsed: int) -> None:
return console_heartbeat_watchdog(ZSXQ_TOC_WATCHDOG_PREFIX, report)


def evaluate_with_watchdog(
cdp: CDPClient,
expression: str,
*,
timeout: int,
max_timeout: int,
watchdog: Callable[[dict[str, Any]], bool],
) -> Any:
"""Use heartbeat support when available, while retaining old-core compatibility."""

if _CDP_WATCHDOG_SUPPORTED:
return cdp.evaluate(
expression,
timeout=timeout,
max_timeout=max_timeout,
watchdog=watchdog,
)
return cdp.evaluate(expression, timeout=max_timeout)


def wait_with_stop(args: argparse.Namespace | None, seconds: float) -> None:
"""Sleep responsively and keep a claimed checkpoint alive during long pauses."""
deadline = time.monotonic() + max(0, seconds)
Expand Down Expand Up @@ -2299,7 +2339,8 @@ def collect_toc(
timeout=35,
args=args,
)
toc = cdp.evaluate(
toc = evaluate_with_watchdog(
cdp,
f"({ZSXQ_TOC_JS})()",
timeout=ZSXQ_TOC_WATCHDOG_IDLE_SECONDS,
max_timeout=ZSXQ_TOC_WATCHDOG_MAX_SECONDS,
Expand Down
2 changes: 1 addition & 1 deletion plugins/zsxq/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"id": "zsxq",
"name": "知识星球",
"description": "知识星球 Group、专栏、帖子与文章 Markdown 导出。",
"version": "1.0.16",
"version": "1.0.17",
"publisher": "Wandao Official",
"homepage": "https://wx.zsxq.com/",
"license": "AGPL-3.0-only",
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "wandao"
version = "1.5.1"
version = "1.5.2"
description = "万能导:多平台知识库 Markdown 导出与互转工具"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion scripts/build_updater_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def build_manifest(directory: Path, tag: str) -> dict[str, object]:
windows_payload = target_payload(windows, tag)
return {
"version": tag.removeprefix("v").removeprefix("V"),
"notes": "包含飞书导入配置预检、Markdown 空目录提示、Vditor 阅读编辑体验和长文导出稳定性优化。",
"notes": "包含 Markdown 图片持久化、教程图片加载进度、导出中阅读保护、知识星球增量恢复和长任务看门狗优化。",
"pub_date": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
"platforms": {
"windows-x86_64": windows_payload,
Expand Down
5 changes: 4 additions & 1 deletion tests_js/guide_ipc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ test('Tauri exposes and registers the restricted provider guide image command',
assert.match(commandsSource, /MAX_REMOTE_IMAGE_BYTES/);
assert.match(commandsSource, /RedirectUrlPolicy::RemoteDocsImage/);
assert.match(commandsSource, /is_allowed_remote_image_target/);
assert.match(commandsSource, /fetch_remote_guide_image\(&provider_id, remote_url, &spec\)\.await/);
assert.match(commandsSource, /cached_remote_guide_image\(&state, &provider_id, remote_url, &spec\)\.await/);
assert.match(commandsSource, /RedirectUrlPolicy::RemoteGuideImage/);
assert.match(commandsSource, /CONTENT_TYPE/);
assert.match(commandsSource, /starts_with\(b"\\x89PNG\\r\\n\\x1a\\n"\)/);
Expand All @@ -42,9 +42,12 @@ test('provider guide rendering hydrates image placeholders after inserting Markd

test('notice center hydrates safe GitHub images through Tauri instead of remote img URLs', () => {
assert.match(appSource, /async function requestNoticeImage\(imageUrl\)/);
assert.match(appSource, /window\.electronAPI\.cacheMarkdownImage\(safeUrl\)/);
assert.match(appSource, /window\.electronAPI\.fetchRemoteImage\(safeUrl\)/);
assert.match(appSource, /async function hydrateNoticeImages\(container\)/);
assert.match(appSource, /hydrateNoticeImages\(contentArea\)/);
assert.match(appSource, /NOTICE_IMAGE_WATCHDOG_MS = 120000/);
assert.match(appSource, /markdown-image-loading-indicator/);
assert.match(appSource, /resolveImageSource: \(imageSource\) => resolveNoticeImageSource\(imageSource, selected\)/);
assert.match(appSource, /remoteImageAttribute: 'data-notice-image'/);
});
2 changes: 1 addition & 1 deletion tests_js/guide_markdown.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ test('Feishu import tutorial pins all screenshots remotely and excludes them fro
assert.equal(assets.reduce((total, name) => total + fs.statSync(path.join(remoteAssetRoot, name)).size, 0), 17317358);
assert.deepEqual(new Set(Object.keys(provider.guideAssets)), new Set(imageReferences.map((match) => match[1])));
assert.equal(Object.values(provider.guideAssets).every((asset) => asset.mime === 'image/png' && asset.bytes <= 3 * 1024 * 1024 && /^[a-f0-9]{64}$/.test(asset.sha256)), true);
assert.equal(plugin.version, '1.0.20');
assert.equal(plugin.version, '1.0.21');
});

test('guide hydration limits remote IPC concurrency and renders an offline fallback', () => {
Expand Down
32 changes: 32 additions & 0 deletions tests_js/markdown_dock.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');

const source = fs.readFileSync(
path.join(__dirname, '..', 'wandao_electron', 'renderer', 'markdown_dock.js'),
'utf8',
);

test('Markdown reader reuses local image data URLs across re-renders', () => {
assert.match(source, /const markdownAssetCache = new Map\(\)/);
assert.match(source, /readCachedMarkdownAsset\(markdownPath, assetPath\)/);
assert.match(source, /cacheMarkdownAsset\(markdownPath, assetPath, result\.dataUrl\)/);
assert.match(source, /MAX_MARKDOWN_ASSET_CACHE_ENTRIES = 48/);
assert.match(source, /clearMarkdownAssetCache\(reader\.path\)/);
assert.match(source, /cacheMarkdownImage\(value\)/);
assert.match(source, /resolveMarkdownImage\(markdownPath, source\)/);
assert.match(source, /is-images-loading/);
assert.match(source, /正在加载文档图片/);
});

test('live Markdown reader waits for export completion before showing a document', () => {
assert.match(source, /LIVE_READER_IDLE_TIMEOUT_MS = 120000/);
assert.match(source, /LIVE_READER_HARD_TIMEOUT_MS = 1800000/);
assert.match(source, /pendingContent/);
assert.match(source, /state\.reader\.status = 'loading'/);
assert.match(source, /refreshLiveReader\(true,/);
assert.match(source, /导出中的正文、图片和附件尚未完成/);
assert.match(source, /导出内容长时间没有完成/);
assert.match(source, /state\.liveExport\.mode === 'running'/);
});
4 changes: 2 additions & 2 deletions tests_js/tauri_bridge.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ test('Tauri bridge exposes all Electron-compatible commands', () => {
}
});
try {
assert.equal(loaded.bridge.COMMAND_NAMES.length, 39);
assert.equal(loaded.bridge.COMMAND_NAMES.length, 40);
for (const command of loaded.bridge.COMMAND_NAMES) {
assert.match(command, /^[a-z][a-z0-9_]*$/);
}
assert.equal(Object.keys(loaded.window.electronAPI).length, 46);
assert.equal(Object.keys(loaded.window.electronAPI).length, 47);
} finally {
loaded.restore();
}
Expand Down
4 changes: 2 additions & 2 deletions wandao_electron/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion wandao_electron/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wandao",
"version": "1.5.1",
"version": "1.5.2",
"description": "Wandao multi-platform document conversion tool",
"scripts": {
"start": "tauri dev",
Expand Down
Loading
Loading