@agentrhq/webcmd 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +317 -24
- package/dist/src/adapter-shadow.d.ts +11 -0
- package/dist/src/adapter-shadow.js +72 -0
- package/dist/src/adapter-shadow.test.d.ts +1 -0
- package/dist/src/adapter-shadow.test.js +49 -0
- package/dist/src/adapter-source.d.ts +11 -0
- package/dist/src/adapter-source.js +24 -0
- package/dist/src/adapter-source.test.d.ts +1 -0
- package/dist/src/adapter-source.test.js +29 -0
- package/dist/src/brand.d.ts +9 -0
- package/dist/src/brand.js +9 -0
- package/dist/src/brand.test.d.ts +1 -0
- package/dist/src/brand.test.js +15 -0
- package/dist/src/browser/analyze.d.ts +118 -0
- package/dist/src/browser/analyze.js +405 -0
- package/dist/src/browser/analyze.test.d.ts +1 -0
- package/dist/src/browser/analyze.test.js +209 -0
- package/dist/src/browser/article-extract.d.ts +57 -0
- package/dist/src/browser/article-extract.e2e.test.d.ts +1 -0
- package/dist/src/browser/article-extract.e2e.test.js +105 -0
- package/dist/src/browser/article-extract.js +169 -0
- package/dist/src/browser/article-extract.test.d.ts +1 -0
- package/dist/src/browser/article-extract.test.js +94 -0
- package/dist/src/browser/ax-snapshot.d.ts +37 -0
- package/dist/src/browser/ax-snapshot.js +217 -0
- package/dist/src/browser/ax-snapshot.test.d.ts +1 -0
- package/dist/src/browser/ax-snapshot.test.js +91 -0
- package/dist/src/browser/base-page.d.ts +151 -0
- package/dist/src/browser/base-page.js +1060 -0
- package/dist/src/browser/base-page.test.d.ts +1 -0
- package/dist/src/browser/base-page.test.js +803 -0
- package/dist/src/browser/bridge-readiness.d.ts +14 -0
- package/dist/src/browser/bridge-readiness.js +24 -0
- package/dist/src/browser/bridge-readiness.test.d.ts +1 -0
- package/dist/src/browser/bridge-readiness.test.js +58 -0
- package/dist/src/browser/bridge.d.ts +26 -0
- package/dist/src/browser/bridge.js +58 -0
- package/dist/src/browser/cdp-click-fixture.test.d.ts +1 -0
- package/dist/src/browser/cdp-click-fixture.test.js +87 -0
- package/dist/src/browser/cdp.d.ts +46 -0
- package/dist/src/browser/cdp.js +536 -0
- package/dist/src/browser/cdp.test.d.ts +1 -0
- package/dist/src/browser/cdp.test.js +79 -0
- package/dist/src/browser/compound.d.ts +59 -0
- package/dist/src/browser/compound.js +112 -0
- package/dist/src/browser/compound.test.d.ts +1 -0
- package/dist/src/browser/compound.test.js +175 -0
- package/dist/src/browser/config.d.ts +2 -0
- package/dist/src/browser/config.js +14 -0
- package/dist/src/browser/daemon-client.d.ts +32 -0
- package/dist/src/browser/daemon-client.js +135 -0
- package/dist/src/browser/daemon-client.test.d.ts +1 -0
- package/dist/src/browser/daemon-client.test.js +326 -0
- package/dist/src/browser/daemon-lifecycle.d.ts +37 -0
- package/dist/src/browser/daemon-lifecycle.js +147 -0
- package/dist/src/browser/daemon-transport.d.ts +54 -0
- package/dist/src/browser/daemon-transport.js +68 -0
- package/dist/src/browser/daemon-version.d.ts +4 -0
- package/dist/src/browser/daemon-version.js +12 -0
- package/dist/src/browser/dom-helpers.d.ts +41 -0
- package/dist/src/browser/dom-helpers.js +239 -0
- package/dist/src/browser/dom-helpers.test.d.ts +1 -0
- package/dist/src/browser/dom-helpers.test.js +106 -0
- package/dist/src/browser/dom-snapshot.d.ts +93 -0
- package/dist/src/browser/dom-snapshot.js +894 -0
- package/dist/src/browser/dom-snapshot.test.d.ts +11 -0
- package/dist/src/browser/dom-snapshot.test.js +313 -0
- package/dist/src/browser/errors.d.ts +38 -0
- package/dist/src/browser/errors.js +79 -0
- package/dist/src/browser/errors.test.d.ts +1 -0
- package/dist/src/browser/errors.test.js +60 -0
- package/dist/src/browser/extract.d.ts +69 -0
- package/dist/src/browser/extract.js +132 -0
- package/dist/src/browser/extract.test.d.ts +1 -0
- package/dist/src/browser/extract.test.js +129 -0
- package/dist/src/browser/find.d.ts +84 -0
- package/dist/src/browser/find.js +398 -0
- package/dist/src/browser/find.test.d.ts +1 -0
- package/dist/src/browser/find.test.js +180 -0
- package/dist/src/browser/html-tree.d.ts +75 -0
- package/dist/src/browser/html-tree.js +112 -0
- package/dist/src/browser/html-tree.test.d.ts +1 -0
- package/dist/src/browser/html-tree.test.js +181 -0
- package/dist/src/browser/index.d.ts +14 -0
- package/dist/src/browser/index.js +12 -0
- package/dist/src/browser/network-cache.d.ts +49 -0
- package/dist/src/browser/network-cache.js +78 -0
- package/dist/src/browser/network-cache.test.d.ts +1 -0
- package/dist/src/browser/network-cache.test.js +75 -0
- package/dist/src/browser/network-interceptor.d.ts +11 -0
- package/dist/src/browser/network-interceptor.js +11 -0
- package/dist/src/browser/network-key.d.ts +22 -0
- package/dist/src/browser/network-key.js +66 -0
- package/dist/src/browser/network-key.test.d.ts +1 -0
- package/dist/src/browser/network-key.test.js +49 -0
- package/dist/src/browser/page.d.ts +83 -0
- package/dist/src/browser/page.js +453 -0
- package/dist/src/browser/page.test.d.ts +1 -0
- package/dist/src/browser/page.test.js +406 -0
- package/dist/src/browser/profile.d.ts +14 -0
- package/dist/src/browser/profile.js +86 -0
- package/dist/src/browser/protocol.d.ts +63 -0
- package/dist/src/browser/protocol.js +1 -0
- package/dist/src/browser/runtime/local-cloak/actions.d.ts +3 -0
- package/dist/src/browser/runtime/local-cloak/actions.js +240 -0
- package/dist/src/browser/runtime/local-cloak/downloads.d.ts +3 -0
- package/dist/src/browser/runtime/local-cloak/downloads.js +28 -0
- package/dist/src/browser/runtime/local-cloak/network.d.ts +28 -0
- package/dist/src/browser/runtime/local-cloak/network.js +120 -0
- package/dist/src/browser/runtime/local-cloak/network.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/network.test.js +124 -0
- package/dist/src/browser/runtime/local-cloak/profiles.d.ts +6 -0
- package/dist/src/browser/runtime/local-cloak/profiles.js +17 -0
- package/dist/src/browser/runtime/local-cloak/profiles.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/profiles.test.js +19 -0
- package/dist/src/browser/runtime/local-cloak/provider.d.ts +15 -0
- package/dist/src/browser/runtime/local-cloak/provider.js +27 -0
- package/dist/src/browser/runtime/local-cloak/provider.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/provider.test.js +244 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +77 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +280 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +92 -0
- package/dist/src/browser/runtime/provider.d.ts +9 -0
- package/dist/src/browser/runtime/provider.js +1 -0
- package/dist/src/browser/runtime/provider.test.d.ts +2 -0
- package/dist/src/browser/runtime/provider.test.js +49 -0
- package/dist/src/browser/shape-filter.d.ts +52 -0
- package/dist/src/browser/shape-filter.js +101 -0
- package/dist/src/browser/shape-filter.test.d.ts +1 -0
- package/dist/src/browser/shape-filter.test.js +101 -0
- package/dist/src/browser/shape.d.ts +23 -0
- package/dist/src/browser/shape.js +95 -0
- package/dist/src/browser/shape.test.d.ts +1 -0
- package/dist/src/browser/shape.test.js +82 -0
- package/dist/src/browser/stealth.d.ts +11 -0
- package/dist/src/browser/stealth.js +359 -0
- package/dist/src/browser/stealth.test.d.ts +1 -0
- package/dist/src/browser/stealth.test.js +134 -0
- package/dist/src/browser/tabs.d.ts +13 -0
- package/dist/src/browser/tabs.js +70 -0
- package/dist/src/browser/target-errors.d.ts +39 -0
- package/dist/src/browser/target-errors.js +45 -0
- package/dist/src/browser/target-errors.test.d.ts +1 -0
- package/dist/src/browser/target-errors.test.js +94 -0
- package/dist/src/browser/target-resolver.d.ts +158 -0
- package/dist/src/browser/target-resolver.js +570 -0
- package/dist/src/browser/target-resolver.test.d.ts +1 -0
- package/dist/src/browser/target-resolver.test.js +118 -0
- package/dist/src/browser/utils.d.ts +20 -0
- package/dist/src/browser/utils.js +64 -0
- package/dist/src/browser/utils.test.d.ts +1 -0
- package/dist/src/browser/utils.test.js +29 -0
- package/dist/src/browser/verify-fixture.d.ts +65 -0
- package/dist/src/browser/verify-fixture.js +318 -0
- package/dist/src/browser/verify-fixture.test.d.ts +1 -0
- package/dist/src/browser/verify-fixture.test.js +219 -0
- package/dist/src/browser/visual-refs.d.ts +11 -0
- package/dist/src/browser/visual-refs.js +108 -0
- package/dist/src/browser.test.d.ts +1 -0
- package/dist/src/browser.test.js +297 -0
- package/dist/src/build-manifest.d.ts +103 -0
- package/dist/src/build-manifest.js +363 -0
- package/dist/src/build-manifest.test.d.ts +1 -0
- package/dist/src/build-manifest.test.js +341 -0
- package/dist/src/capabilityRouting.d.ts +19 -0
- package/dist/src/capabilityRouting.js +56 -0
- package/dist/src/capabilityRouting.test.d.ts +1 -0
- package/dist/src/capabilityRouting.test.js +63 -0
- package/dist/src/cli-argv-preprocess.d.ts +63 -0
- package/dist/src/cli-argv-preprocess.js +303 -0
- package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
- package/dist/src/cli-argv-preprocess.test.js +304 -0
- package/dist/src/cli.d.ts +79 -0
- package/dist/src/cli.js +3350 -0
- package/dist/src/cli.test.d.ts +1 -0
- package/dist/src/cli.test.js +3041 -0
- package/dist/src/commanderAdapter.d.ts +21 -0
- package/dist/src/commanderAdapter.js +208 -0
- package/dist/src/commanderAdapter.test.d.ts +1 -0
- package/dist/src/commanderAdapter.test.js +352 -0
- package/dist/src/commands/auth.d.ts +40 -0
- package/dist/src/commands/auth.js +434 -0
- package/dist/src/commands/auth.test.d.ts +1 -0
- package/dist/src/commands/auth.test.js +252 -0
- package/dist/src/commands/daemon.d.ts +9 -0
- package/dist/src/commands/daemon.js +91 -0
- package/dist/src/commands/daemon.test.d.ts +1 -0
- package/dist/src/commands/daemon.test.js +293 -0
- package/dist/src/completion-fast.d.ts +22 -0
- package/dist/src/completion-fast.js +88 -0
- package/dist/src/completion-shared.d.ts +13 -0
- package/dist/src/completion-shared.js +61 -0
- package/dist/src/completion.d.ts +21 -0
- package/dist/src/completion.js +68 -0
- package/dist/src/completion.test.d.ts +1 -0
- package/dist/src/completion.test.js +24 -0
- package/dist/src/constants.d.ts +18 -0
- package/dist/src/constants.js +41 -0
- package/dist/src/convention-audit.d.ts +50 -0
- package/dist/src/convention-audit.js +553 -0
- package/dist/src/convention-audit.test.d.ts +1 -0
- package/dist/src/convention-audit.test.js +255 -0
- package/dist/src/daemon/server.d.ts +13 -0
- package/dist/src/daemon/server.js +185 -0
- package/dist/src/daemon/server.test.d.ts +1 -0
- package/dist/src/daemon/server.test.js +132 -0
- package/dist/src/daemon-utils.d.ts +18 -0
- package/dist/src/daemon-utils.js +37 -0
- package/dist/src/daemon.d.ts +1 -0
- package/dist/src/daemon.js +27 -0
- package/dist/src/daemon.test.d.ts +1 -0
- package/dist/src/daemon.test.js +60 -0
- package/dist/src/discovery.d.ts +46 -0
- package/dist/src/discovery.js +341 -0
- package/dist/src/doctor.d.ts +39 -0
- package/dist/src/doctor.js +168 -0
- package/dist/src/doctor.test.d.ts +1 -0
- package/dist/src/doctor.test.js +302 -0
- package/dist/src/download/article-download.d.ts +72 -0
- package/dist/src/download/article-download.js +305 -0
- package/dist/src/download/article-download.test.d.ts +1 -0
- package/dist/src/download/article-download.test.js +235 -0
- package/dist/src/download/index.d.ts +71 -0
- package/dist/src/download/index.js +363 -0
- package/dist/src/download/index.test.d.ts +1 -0
- package/dist/src/download/index.test.js +174 -0
- package/dist/src/download/media-download.d.ts +49 -0
- package/dist/src/download/media-download.js +127 -0
- package/dist/src/download/media-download.test.d.ts +1 -0
- package/dist/src/download/media-download.test.js +112 -0
- package/dist/src/download/progress.d.ts +36 -0
- package/dist/src/download/progress.js +120 -0
- package/dist/src/download/progress.test.d.ts +1 -0
- package/dist/src/download/progress.test.js +36 -0
- package/dist/src/electron-apps.d.ts +31 -0
- package/dist/src/electron-apps.js +91 -0
- package/dist/src/electron-apps.test.d.ts +1 -0
- package/dist/src/electron-apps.test.js +76 -0
- package/dist/src/engine.test.d.ts +1 -0
- package/dist/src/engine.test.js +312 -0
- package/dist/src/errors.d.ts +114 -0
- package/dist/src/errors.js +174 -0
- package/dist/src/errors.test.d.ts +1 -0
- package/dist/src/errors.test.js +109 -0
- package/dist/src/execution.d.ts +24 -0
- package/dist/src/execution.js +509 -0
- package/dist/src/execution.test.d.ts +1 -0
- package/dist/src/execution.test.js +645 -0
- package/dist/src/external.d.ts +49 -0
- package/dist/src/external.js +211 -0
- package/dist/src/external.test.d.ts +1 -0
- package/dist/src/external.test.js +110 -0
- package/dist/src/help.d.ts +88 -0
- package/dist/src/help.js +556 -0
- package/dist/src/help.test.d.ts +1 -0
- package/dist/src/help.test.js +58 -0
- package/dist/src/hooks.d.ts +46 -0
- package/dist/src/hooks.js +58 -0
- package/dist/src/hooks.test.d.ts +4 -0
- package/dist/src/hooks.test.js +92 -0
- package/dist/src/interceptor.d.ts +44 -0
- package/dist/src/interceptor.js +183 -0
- package/dist/src/interceptor.test.d.ts +4 -0
- package/dist/src/interceptor.test.js +81 -0
- package/dist/src/launcher.d.ts +41 -0
- package/dist/src/launcher.js +226 -0
- package/dist/src/launcher.test.d.ts +1 -0
- package/dist/src/launcher.test.js +153 -0
- package/dist/src/logger.d.ts +26 -0
- package/dist/src/logger.js +49 -0
- package/dist/src/main.d.ts +5 -0
- package/dist/src/main.js +177 -0
- package/dist/src/manifest-types.d.ts +43 -0
- package/dist/src/manifest-types.js +9 -0
- package/dist/src/node-network.d.ts +10 -0
- package/dist/src/node-network.js +174 -0
- package/dist/src/node-network.test.d.ts +1 -0
- package/dist/src/node-network.test.js +55 -0
- package/dist/src/observation/artifact.d.ts +16 -0
- package/dist/src/observation/artifact.js +260 -0
- package/dist/src/observation/artifact.test.d.ts +1 -0
- package/dist/src/observation/artifact.test.js +121 -0
- package/dist/src/observation/events.d.ts +89 -0
- package/dist/src/observation/events.js +1 -0
- package/dist/src/observation/index.d.ts +7 -0
- package/dist/src/observation/index.js +7 -0
- package/dist/src/observation/manager.d.ts +9 -0
- package/dist/src/observation/manager.js +27 -0
- package/dist/src/observation/manager.test.d.ts +1 -0
- package/dist/src/observation/manager.test.js +13 -0
- package/dist/src/observation/redaction.d.ts +11 -0
- package/dist/src/observation/redaction.js +81 -0
- package/dist/src/observation/redaction.test.d.ts +1 -0
- package/dist/src/observation/redaction.test.js +32 -0
- package/dist/src/observation/retention.d.ts +32 -0
- package/dist/src/observation/retention.js +160 -0
- package/dist/src/observation/retention.test.d.ts +1 -0
- package/dist/src/observation/retention.test.js +118 -0
- package/dist/src/observation/ring-buffer.d.ts +22 -0
- package/dist/src/observation/ring-buffer.js +45 -0
- package/dist/src/observation/ring-buffer.test.d.ts +1 -0
- package/dist/src/observation/ring-buffer.test.js +22 -0
- package/dist/src/observation/session.d.ts +25 -0
- package/dist/src/observation/session.js +50 -0
- package/dist/src/output.d.ts +14 -0
- package/dist/src/output.js +152 -0
- package/dist/src/output.test.d.ts +1 -0
- package/dist/src/output.test.js +53 -0
- package/dist/src/package-exports.test.d.ts +1 -0
- package/dist/src/package-exports.test.js +112 -0
- package/dist/src/package-paths.d.ts +8 -0
- package/dist/src/package-paths.js +41 -0
- package/dist/src/pipeline/executor.d.ts +11 -0
- package/dist/src/pipeline/executor.js +93 -0
- package/dist/src/pipeline/executor.test.d.ts +4 -0
- package/dist/src/pipeline/executor.test.js +182 -0
- package/dist/src/pipeline/index.d.ts +5 -0
- package/dist/src/pipeline/index.js +5 -0
- package/dist/src/pipeline/registry.d.ts +28 -0
- package/dist/src/pipeline/registry.js +55 -0
- package/dist/src/pipeline/steps/browser.d.ts +13 -0
- package/dist/src/pipeline/steps/browser.js +86 -0
- package/dist/src/pipeline/steps/download.d.ts +18 -0
- package/dist/src/pipeline/steps/download.js +252 -0
- package/dist/src/pipeline/steps/download.test.d.ts +1 -0
- package/dist/src/pipeline/steps/download.test.js +102 -0
- package/dist/src/pipeline/steps/fetch.d.ts +5 -0
- package/dist/src/pipeline/steps/fetch.js +122 -0
- package/dist/src/pipeline/steps/fetch.test.d.ts +1 -0
- package/dist/src/pipeline/steps/fetch.test.js +117 -0
- package/dist/src/pipeline/steps/intercept.d.ts +5 -0
- package/dist/src/pipeline/steps/intercept.js +50 -0
- package/dist/src/pipeline/steps/tap.d.ts +12 -0
- package/dist/src/pipeline/steps/tap.js +93 -0
- package/dist/src/pipeline/steps/transform.d.ts +9 -0
- package/dist/src/pipeline/steps/transform.js +70 -0
- package/dist/src/pipeline/template.d.ts +17 -0
- package/dist/src/pipeline/template.js +344 -0
- package/dist/src/pipeline/template.test.d.ts +4 -0
- package/dist/src/pipeline/template.test.js +197 -0
- package/dist/src/pipeline/transform.test.d.ts +4 -0
- package/dist/src/pipeline/transform.test.js +133 -0
- package/dist/src/plugin-manifest.d.ts +70 -0
- package/dist/src/plugin-manifest.js +160 -0
- package/dist/src/plugin-manifest.test.d.ts +4 -0
- package/dist/src/plugin-manifest.test.js +179 -0
- package/dist/src/plugin-scaffold.d.ts +28 -0
- package/dist/src/plugin-scaffold.js +143 -0
- package/dist/src/plugin-scaffold.test.d.ts +4 -0
- package/dist/src/plugin-scaffold.test.js +83 -0
- package/dist/src/plugin.d.ts +146 -0
- package/dist/src/plugin.js +1303 -0
- package/dist/src/plugin.test.d.ts +4 -0
- package/dist/src/plugin.test.js +1363 -0
- package/dist/src/registry-api.d.ts +13 -0
- package/dist/src/registry-api.js +10 -0
- package/dist/src/registry.d.ts +123 -0
- package/dist/src/registry.js +125 -0
- package/dist/src/registry.test.d.ts +4 -0
- package/dist/src/registry.test.js +209 -0
- package/dist/src/runtime-copy.test.d.ts +1 -0
- package/dist/src/runtime-copy.test.js +29 -0
- package/dist/src/runtime-detect.d.ts +31 -0
- package/dist/src/runtime-detect.js +47 -0
- package/dist/src/runtime-detect.test.d.ts +1 -0
- package/dist/src/runtime-detect.test.js +39 -0
- package/dist/src/runtime-identity.test.d.ts +1 -0
- package/dist/src/runtime-identity.test.js +18 -0
- package/dist/src/runtime.d.ts +47 -0
- package/dist/src/runtime.js +54 -0
- package/dist/src/scripts/framework.d.ts +7 -0
- package/dist/src/scripts/framework.js +25 -0
- package/dist/src/scripts/interact.d.ts +4 -0
- package/dist/src/scripts/interact.js +20 -0
- package/dist/src/scripts/store.d.ts +13 -0
- package/dist/src/scripts/store.js +48 -0
- package/dist/src/serialization.d.ts +42 -0
- package/dist/src/serialization.js +103 -0
- package/dist/src/serialization.test.d.ts +1 -0
- package/dist/src/serialization.test.js +78 -0
- package/dist/src/skills.d.ts +14 -0
- package/dist/src/skills.js +127 -0
- package/dist/src/skills.test.d.ts +1 -0
- package/dist/src/skills.test.js +71 -0
- package/dist/src/snapshotFormatter.d.ts +11 -0
- package/dist/src/snapshotFormatter.js +338 -0
- package/dist/src/snapshotFormatter.test.d.ts +7 -0
- package/dist/src/snapshotFormatter.test.js +521 -0
- package/dist/src/tui.d.ts +28 -0
- package/dist/src/tui.js +179 -0
- package/dist/src/tui.test.d.ts +1 -0
- package/dist/src/tui.test.js +19 -0
- package/dist/src/types.d.ts +236 -0
- package/dist/src/types.js +7 -0
- package/dist/src/update-check.d.ts +66 -0
- package/dist/src/update-check.js +206 -0
- package/dist/src/update-check.test.d.ts +1 -0
- package/dist/src/update-check.test.js +117 -0
- package/dist/src/utils.d.ts +57 -0
- package/dist/src/utils.js +152 -0
- package/dist/src/utils.test.d.ts +1 -0
- package/dist/src/utils.test.js +155 -0
- package/dist/src/validate.d.ts +21 -0
- package/dist/src/validate.js +130 -0
- package/dist/src/validate.test.d.ts +9 -0
- package/dist/src/validate.test.js +90 -0
- package/dist/src/verify.d.ts +26 -0
- package/dist/src/verify.js +67 -0
- package/dist/src/version.d.ts +4 -0
- package/dist/src/version.js +22 -0
- package/dist/src/weixin-download.test.d.ts +1 -0
- package/dist/src/weixin-download.test.js +45 -0
- package/package.json +4 -3
- package/scripts/check-package-bin.mjs +95 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const { mockGetDaemonHealth, mockConnect, mockClose, mockFindShadowedUserAdapters } = vi.hoisted(() => ({
|
|
3
|
+
mockGetDaemonHealth: vi.fn(),
|
|
4
|
+
mockConnect: vi.fn(),
|
|
5
|
+
mockClose: vi.fn(),
|
|
6
|
+
mockFindShadowedUserAdapters: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock('./browser/daemon-transport.js', () => ({
|
|
9
|
+
getDaemonHealth: mockGetDaemonHealth,
|
|
10
|
+
}));
|
|
11
|
+
vi.mock('./browser/index.js', () => ({
|
|
12
|
+
BrowserBridge: class {
|
|
13
|
+
connect = mockConnect;
|
|
14
|
+
close = mockClose;
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
vi.mock('./adapter-shadow.js', async () => {
|
|
18
|
+
const actual = await vi.importActual('./adapter-shadow.js');
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
findShadowedUserAdapters: mockFindShadowedUserAdapters,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
import { renderBrowserDoctorReport, runBrowserDoctor } from './doctor.js';
|
|
25
|
+
describe('doctor report rendering', () => {
|
|
26
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
mockFindShadowedUserAdapters.mockReturnValue([]);
|
|
30
|
+
// Doctor always runs live connectivity. Tests that want connect to fail override.
|
|
31
|
+
mockConnect.mockResolvedValue({
|
|
32
|
+
evaluate: vi.fn().mockResolvedValue(2),
|
|
33
|
+
closeWindow: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
});
|
|
35
|
+
mockClose.mockResolvedValue(undefined);
|
|
36
|
+
});
|
|
37
|
+
it('renders OK-style report when daemon and runtime connected', () => {
|
|
38
|
+
const text = strip(renderBrowserDoctorReport({
|
|
39
|
+
cliVersion: '1.7.9',
|
|
40
|
+
daemonRunning: true,
|
|
41
|
+
daemonVersion: '1.7.9',
|
|
42
|
+
runtimeConnected: true,
|
|
43
|
+
runtimeName: 'Cloak',
|
|
44
|
+
runtimeVersion: '1.6.8',
|
|
45
|
+
issues: [],
|
|
46
|
+
}));
|
|
47
|
+
expect(text).toContain('[OK] Daemon: running on port 19825');
|
|
48
|
+
expect(text).toContain('(v1.7.9)');
|
|
49
|
+
expect(text).toContain('[OK] Runtime: Cloak connected (v1.6.8)');
|
|
50
|
+
expect(text).toContain('Everything looks good!');
|
|
51
|
+
expect(text).not.toContain('webcmd browser analyze <url>');
|
|
52
|
+
});
|
|
53
|
+
it('renders a warning when daemon version is stale', () => {
|
|
54
|
+
const text = strip(renderBrowserDoctorReport({
|
|
55
|
+
cliVersion: '1.7.9',
|
|
56
|
+
daemonRunning: true,
|
|
57
|
+
daemonVersion: '1.7.6',
|
|
58
|
+
daemonStale: true,
|
|
59
|
+
runtimeConnected: true,
|
|
60
|
+
runtimeName: 'Cloak',
|
|
61
|
+
runtimeVersion: '1.0.3',
|
|
62
|
+
issues: ['Stale daemon detected: daemon v1.7.6 != CLI v1.7.9.\n Run: webcmd daemon restart'],
|
|
63
|
+
}));
|
|
64
|
+
expect(text).toContain('[WARN] Daemon: running on port 19825 (v1.7.6, stale; CLI v1.7.9)');
|
|
65
|
+
expect(text).toContain('Run: webcmd daemon restart');
|
|
66
|
+
expect(text).not.toContain('Everything looks good!');
|
|
67
|
+
});
|
|
68
|
+
it('renders MISSING when daemon not running', () => {
|
|
69
|
+
const text = strip(renderBrowserDoctorReport({
|
|
70
|
+
daemonRunning: false,
|
|
71
|
+
runtimeConnected: false,
|
|
72
|
+
issues: ['Daemon is not running.'],
|
|
73
|
+
}));
|
|
74
|
+
expect(text).toContain('[MISSING] Daemon: not running');
|
|
75
|
+
expect(text).toContain('[MISSING] Runtime: Cloak not connected');
|
|
76
|
+
expect(text).toContain('Daemon is not running.');
|
|
77
|
+
});
|
|
78
|
+
it('renders runtime not connected when daemon is running', () => {
|
|
79
|
+
const text = strip(renderBrowserDoctorReport({
|
|
80
|
+
daemonRunning: true,
|
|
81
|
+
runtimeConnected: false,
|
|
82
|
+
issues: ['Daemon is running but the Cloak runtime is not connected.'],
|
|
83
|
+
}));
|
|
84
|
+
expect(text).toContain('[OK] Daemon: running on port 19825');
|
|
85
|
+
expect(text).toContain('[MISSING] Runtime: Cloak not connected');
|
|
86
|
+
});
|
|
87
|
+
it('renders OK when the connected Cloak runtime version is unknown', () => {
|
|
88
|
+
const text = strip(renderBrowserDoctorReport({
|
|
89
|
+
daemonRunning: true,
|
|
90
|
+
runtimeConnected: true,
|
|
91
|
+
runtimeName: 'Cloak',
|
|
92
|
+
issues: [],
|
|
93
|
+
}));
|
|
94
|
+
expect(text).toContain('[OK] Runtime: Cloak connected (version unknown)');
|
|
95
|
+
expect(text).not.toContain('Cloak runtime is connected but did not report a version.');
|
|
96
|
+
expect(text).toContain('Everything looks good!');
|
|
97
|
+
});
|
|
98
|
+
it('renders connectivity OK when live test succeeds', () => {
|
|
99
|
+
const text = strip(renderBrowserDoctorReport({
|
|
100
|
+
daemonRunning: true,
|
|
101
|
+
runtimeConnected: true,
|
|
102
|
+
connectivity: { ok: true, durationMs: 1234 },
|
|
103
|
+
issues: [],
|
|
104
|
+
}));
|
|
105
|
+
expect(text).toContain('[OK] Connectivity: connected in 1.2s');
|
|
106
|
+
});
|
|
107
|
+
it('renders connected profiles when multiple are present', () => {
|
|
108
|
+
const text = strip(renderBrowserDoctorReport({
|
|
109
|
+
daemonRunning: true,
|
|
110
|
+
runtimeConnected: false,
|
|
111
|
+
profiles: [
|
|
112
|
+
{ contextId: 'work', runtimeConnected: true, runtimeVersion: '1.2.3', pending: 0 },
|
|
113
|
+
{ contextId: 'personal', runtimeConnected: true, runtimeVersion: '1.2.3', pending: 0 },
|
|
114
|
+
],
|
|
115
|
+
issues: [],
|
|
116
|
+
}));
|
|
117
|
+
expect(text).toContain('Profiles:');
|
|
118
|
+
expect(text).toContain('work: connected v1.2.3');
|
|
119
|
+
expect(text).toContain('personal: connected v1.2.3');
|
|
120
|
+
});
|
|
121
|
+
it('renders unstable runtime state when live connectivity and status disagree', () => {
|
|
122
|
+
const text = strip(renderBrowserDoctorReport({
|
|
123
|
+
daemonRunning: true,
|
|
124
|
+
runtimeConnected: true,
|
|
125
|
+
runtimeFlaky: true,
|
|
126
|
+
runtimeName: 'Cloak',
|
|
127
|
+
connectivity: { ok: true, durationMs: 1234 },
|
|
128
|
+
issues: ['Cloak runtime connection is unstable.'],
|
|
129
|
+
}));
|
|
130
|
+
expect(text).toContain('[WARN] Runtime: Cloak unstable');
|
|
131
|
+
expect(text).toContain('Cloak runtime connection is unstable.');
|
|
132
|
+
});
|
|
133
|
+
it('renders unstable daemon state when live connectivity and status disagree', () => {
|
|
134
|
+
const text = strip(renderBrowserDoctorReport({
|
|
135
|
+
daemonRunning: false,
|
|
136
|
+
daemonFlaky: true,
|
|
137
|
+
runtimeConnected: false,
|
|
138
|
+
connectivity: { ok: true, durationMs: 1234 },
|
|
139
|
+
issues: ['Daemon connectivity is unstable.'],
|
|
140
|
+
}));
|
|
141
|
+
expect(text).toContain('[WARN] Daemon: unstable');
|
|
142
|
+
expect(text).toContain('Daemon connectivity is unstable.');
|
|
143
|
+
});
|
|
144
|
+
it('reports daemon not running when connectivity fails and daemon stays stopped', async () => {
|
|
145
|
+
mockConnect.mockRejectedValueOnce(new Error('Could not start daemon'));
|
|
146
|
+
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
|
|
147
|
+
const report = await runBrowserDoctor();
|
|
148
|
+
expect(report.daemonRunning).toBe(false);
|
|
149
|
+
expect(report.runtimeConnected).toBe(false);
|
|
150
|
+
expect(report.connectivity?.ok).toBe(false);
|
|
151
|
+
expect(report.issues).toEqual(expect.arrayContaining([
|
|
152
|
+
expect.stringContaining('Daemon is not running'),
|
|
153
|
+
]));
|
|
154
|
+
});
|
|
155
|
+
it('reports flapping when live check succeeds but final status shows runtime disconnected', async () => {
|
|
156
|
+
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-runtime', status: { runtimeConnected: false, runtimeName: 'Cloak' } });
|
|
157
|
+
const report = await runBrowserDoctor();
|
|
158
|
+
expect(report.daemonRunning).toBe(true);
|
|
159
|
+
expect(report.runtimeConnected).toBe(false);
|
|
160
|
+
expect(report.runtimeFlaky).toBe(true);
|
|
161
|
+
expect(report.issues).toEqual(expect.arrayContaining([
|
|
162
|
+
expect.stringContaining('Cloak runtime connection is unstable'),
|
|
163
|
+
]));
|
|
164
|
+
});
|
|
165
|
+
it('uses runtime-neutral readiness hints when the runtime is disconnected', async () => {
|
|
166
|
+
mockConnect.mockRejectedValueOnce(new Error('runtime unavailable'));
|
|
167
|
+
mockGetDaemonHealth.mockResolvedValueOnce({
|
|
168
|
+
state: 'no-runtime',
|
|
169
|
+
status: { runtimeConnected: false, runtimeName: 'Cloak' },
|
|
170
|
+
});
|
|
171
|
+
const report = await runBrowserDoctor();
|
|
172
|
+
const issues = report.issues.join('\n');
|
|
173
|
+
expect(issues).toContain('Cloak runtime is not connected');
|
|
174
|
+
expect(issues).toContain('Make sure Chrome/Chromium is open and Cloak is enabled');
|
|
175
|
+
expect(issues).not.toContain(`Webcmd Browser ${'Bridge'}`);
|
|
176
|
+
expect(issues).not.toContain(`Load ${'unpacked'}`);
|
|
177
|
+
expect(issues).not.toContain('Download the latest extension');
|
|
178
|
+
});
|
|
179
|
+
it('reports daemon flapping when live check succeeds but daemon disappears afterward', async () => {
|
|
180
|
+
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
|
|
181
|
+
const report = await runBrowserDoctor();
|
|
182
|
+
expect(report.daemonRunning).toBe(false);
|
|
183
|
+
expect(report.daemonFlaky).toBe(true);
|
|
184
|
+
expect(report.runtimeConnected).toBe(false);
|
|
185
|
+
expect(report.issues).toEqual(expect.arrayContaining([
|
|
186
|
+
expect.stringContaining('Daemon connectivity is unstable'),
|
|
187
|
+
]));
|
|
188
|
+
});
|
|
189
|
+
it('uses the fast default timeout for live connectivity checks', async () => {
|
|
190
|
+
let timeoutSeen;
|
|
191
|
+
const closeWindow = vi.fn().mockResolvedValue(undefined);
|
|
192
|
+
mockConnect.mockImplementationOnce(async (opts) => {
|
|
193
|
+
timeoutSeen = opts?.timeout;
|
|
194
|
+
expect(opts?.session).toBe('__doctor__');
|
|
195
|
+
expect(opts?.surface).toBe('browser');
|
|
196
|
+
return {
|
|
197
|
+
evaluate: vi.fn().mockResolvedValue(2),
|
|
198
|
+
closeWindow,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'ready', status: { runtimeConnected: true, runtimeName: 'Cloak' } });
|
|
202
|
+
await runBrowserDoctor();
|
|
203
|
+
expect(timeoutSeen).toBe(8);
|
|
204
|
+
expect(closeWindow).toHaveBeenCalledTimes(1);
|
|
205
|
+
});
|
|
206
|
+
it('does not report an issue when the connected Cloak runtime does not report a version', async () => {
|
|
207
|
+
const status = {
|
|
208
|
+
state: 'ready',
|
|
209
|
+
status: {
|
|
210
|
+
runtimeConnected: true,
|
|
211
|
+
runtimeName: 'Cloak',
|
|
212
|
+
runtimeVersion: undefined,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
mockGetDaemonHealth.mockResolvedValue(status);
|
|
216
|
+
const report = await runBrowserDoctor();
|
|
217
|
+
expect(report.runtimeConnected).toBe(true);
|
|
218
|
+
expect(report.runtimeVersion).toBeUndefined();
|
|
219
|
+
expect(report.issues.join('\n')).not.toContain('did not report a version');
|
|
220
|
+
});
|
|
221
|
+
it('does not compare runtime version to CLI version or cached extension updates', async () => {
|
|
222
|
+
const status = {
|
|
223
|
+
state: 'ready',
|
|
224
|
+
status: {
|
|
225
|
+
daemonVersion: '1.7.9',
|
|
226
|
+
runtimeConnected: true,
|
|
227
|
+
runtimeName: 'Cloak',
|
|
228
|
+
runtimeVersion: '99.0.0',
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
mockGetDaemonHealth.mockResolvedValue(status);
|
|
232
|
+
const report = await runBrowserDoctor({ cliVersion: '1.7.9' });
|
|
233
|
+
expect(report.runtimeVersion).toBe('99.0.0');
|
|
234
|
+
expect(report.issues.join('\n')).not.toContain('Extension major version mismatch');
|
|
235
|
+
expect(report.issues.join('\n')).not.toContain('Extension update available');
|
|
236
|
+
expect(report.issues.join('\n')).not.toContain('Download the latest extension');
|
|
237
|
+
});
|
|
238
|
+
it('reports an issue when daemon version differs from CLI version', async () => {
|
|
239
|
+
const status = {
|
|
240
|
+
state: 'ready',
|
|
241
|
+
status: {
|
|
242
|
+
daemonVersion: '1.7.6',
|
|
243
|
+
runtimeConnected: true,
|
|
244
|
+
runtimeName: 'Cloak',
|
|
245
|
+
runtimeVersion: '1.0.3',
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
mockGetDaemonHealth.mockResolvedValue(status);
|
|
249
|
+
const report = await runBrowserDoctor({ cliVersion: '1.7.9' });
|
|
250
|
+
expect(report.daemonStale).toBe(true);
|
|
251
|
+
expect(report.issues).toEqual(expect.arrayContaining([
|
|
252
|
+
expect.stringContaining('Stale daemon detected: daemon v1.7.6 != CLI v1.7.9'),
|
|
253
|
+
]));
|
|
254
|
+
});
|
|
255
|
+
it('reports local adapter shadows as a warning issue', async () => {
|
|
256
|
+
const status = {
|
|
257
|
+
state: 'ready',
|
|
258
|
+
status: {
|
|
259
|
+
daemonVersion: '1.7.9',
|
|
260
|
+
runtimeConnected: true,
|
|
261
|
+
runtimeName: 'Cloak',
|
|
262
|
+
runtimeVersion: '1.0.3',
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
mockGetDaemonHealth.mockResolvedValue(status);
|
|
266
|
+
mockFindShadowedUserAdapters.mockReturnValueOnce([
|
|
267
|
+
{
|
|
268
|
+
name: 'instagram/saved',
|
|
269
|
+
userPath: '/home/me/.webcmd/clis/instagram/saved.js',
|
|
270
|
+
builtinPath: '/pkg/clis/instagram/saved.js',
|
|
271
|
+
},
|
|
272
|
+
]);
|
|
273
|
+
const report = await runBrowserDoctor({ cliVersion: '1.7.9' });
|
|
274
|
+
expect(report.adapterShadows).toHaveLength(1);
|
|
275
|
+
expect(report.issues).toEqual(expect.arrayContaining([
|
|
276
|
+
expect.stringContaining('Local adapter overrides shadow packaged adapters'),
|
|
277
|
+
]));
|
|
278
|
+
});
|
|
279
|
+
it('reports profile-required when multiple profiles are connected without a selection', async () => {
|
|
280
|
+
const status = {
|
|
281
|
+
state: 'profile-required',
|
|
282
|
+
status: {
|
|
283
|
+
runtimeConnected: false,
|
|
284
|
+
runtimeName: 'Cloak',
|
|
285
|
+
profileRequired: true,
|
|
286
|
+
profiles: [
|
|
287
|
+
{ contextId: 'work', runtimeConnected: true, pending: 0 },
|
|
288
|
+
{ contextId: 'personal', runtimeConnected: true, pending: 0 },
|
|
289
|
+
],
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
mockGetDaemonHealth.mockResolvedValue(status);
|
|
293
|
+
// Real connectivity would fail in profile-required state; force it here so
|
|
294
|
+
// the test exercises the profile-required issue path, not the flaky path.
|
|
295
|
+
mockConnect.mockRejectedValueOnce(new Error('profile required'));
|
|
296
|
+
const report = await runBrowserDoctor();
|
|
297
|
+
expect(report.profiles).toHaveLength(2);
|
|
298
|
+
expect(report.issues).toEqual(expect.arrayContaining([
|
|
299
|
+
expect.stringContaining('Multiple Chrome profiles are connected'),
|
|
300
|
+
]));
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Article download helper — shared logic for downloading articles as Markdown.
|
|
3
|
+
*
|
|
4
|
+
* Used by: zhihu/download, weixin/download, and future article adapters.
|
|
5
|
+
*
|
|
6
|
+
* Flow: ArticleData → TurndownService → image download → frontmatter → .md file
|
|
7
|
+
*/
|
|
8
|
+
import TurndownService from 'turndown';
|
|
9
|
+
export interface ArticleData {
|
|
10
|
+
title: string;
|
|
11
|
+
author?: string;
|
|
12
|
+
publishTime?: string;
|
|
13
|
+
sourceUrl?: string;
|
|
14
|
+
contentHtml: string;
|
|
15
|
+
/** Pre-extracted code blocks to restore after Markdown conversion */
|
|
16
|
+
codeBlocks?: Array<{
|
|
17
|
+
lang: string;
|
|
18
|
+
code: string;
|
|
19
|
+
}>;
|
|
20
|
+
/** Image URLs found in the article (pre-collected from DOM) */
|
|
21
|
+
imageUrls?: string[];
|
|
22
|
+
}
|
|
23
|
+
export interface FrontmatterLabels {
|
|
24
|
+
author?: string;
|
|
25
|
+
publishTime?: string;
|
|
26
|
+
sourceUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface ArticleDownloadOptions {
|
|
29
|
+
output: string;
|
|
30
|
+
downloadImages?: boolean;
|
|
31
|
+
/** Extra headers for image downloads (e.g. { Referer: '...' }) */
|
|
32
|
+
imageHeaders?: Record<string, string>;
|
|
33
|
+
maxTitleLength?: number;
|
|
34
|
+
/** Custom TurndownService configuration callback */
|
|
35
|
+
configureTurndown?: (td: TurndownService) => void;
|
|
36
|
+
/** Custom image extension detector (default: infer from URL extension) */
|
|
37
|
+
detectImageExt?: (url: string) => string;
|
|
38
|
+
/** Custom frontmatter labels (default: Chinese labels) */
|
|
39
|
+
frontmatterLabels?: FrontmatterLabels;
|
|
40
|
+
/**
|
|
41
|
+
* Extra CSS selectors removed from the article before Turndown conversion.
|
|
42
|
+
* Use this to drop site-specific noise the adapter can't always trim upstream
|
|
43
|
+
* (e.g. zhihu 折叠卡, weixin 赞赏栏, wiki infobox).
|
|
44
|
+
*/
|
|
45
|
+
cleanSelectors?: string[];
|
|
46
|
+
/**
|
|
47
|
+
* Write the markdown to `process.stdout` instead of a file on disk. Image
|
|
48
|
+
* download and directory creation are skipped — remote image URLs are kept
|
|
49
|
+
* as-is so the output is self-contained when piped.
|
|
50
|
+
*/
|
|
51
|
+
stdout?: boolean;
|
|
52
|
+
}
|
|
53
|
+
export interface ArticleDownloadResult {
|
|
54
|
+
title: string;
|
|
55
|
+
author: string;
|
|
56
|
+
publish_time: string;
|
|
57
|
+
status: string;
|
|
58
|
+
size: string;
|
|
59
|
+
saved: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Download an article to Markdown with optional image localization.
|
|
63
|
+
*
|
|
64
|
+
* Handles the full pipeline:
|
|
65
|
+
* 1. HTML → Markdown (via TurndownService)
|
|
66
|
+
* 2. Code block placeholder restoration
|
|
67
|
+
* 3. Batch image downloading with concurrency + deduplication
|
|
68
|
+
* 4. Image URL replacement in Markdown
|
|
69
|
+
* 5. Frontmatter generation (customizable labels)
|
|
70
|
+
* 6. File write
|
|
71
|
+
*/
|
|
72
|
+
export declare function downloadArticle(data: ArticleData, options: ArticleDownloadOptions): Promise<ArticleDownloadResult[]>;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Article download helper — shared logic for downloading articles as Markdown.
|
|
3
|
+
*
|
|
4
|
+
* Used by: zhihu/download, weixin/download, and future article adapters.
|
|
5
|
+
*
|
|
6
|
+
* Flow: ArticleData → TurndownService → image download → frontmatter → .md file
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
import TurndownService from 'turndown';
|
|
11
|
+
import { gfm } from 'turndown-plugin-gfm';
|
|
12
|
+
import { httpDownload, sanitizeFilename } from './index.js';
|
|
13
|
+
import { formatBytes } from './progress.js';
|
|
14
|
+
const IMAGE_CONCURRENCY = 5;
|
|
15
|
+
const DEFAULT_LABELS = {
|
|
16
|
+
author: '作者',
|
|
17
|
+
publishTime: '发布时间',
|
|
18
|
+
sourceUrl: '原文链接',
|
|
19
|
+
};
|
|
20
|
+
// ============================================================
|
|
21
|
+
// Markdown Conversion
|
|
22
|
+
// ============================================================
|
|
23
|
+
// Nodes that never carry article content. Turndown keeps them by default — if an
|
|
24
|
+
// adapter's contentHtml extraction misses one, CSS / scripts / widget markup
|
|
25
|
+
// ends up inline in the .md. Strip them unconditionally at the converter level.
|
|
26
|
+
// `svg` is not in HTMLElementTagNameMap, so we type-narrow manually.
|
|
27
|
+
// `header/footer/nav/aside` cover page chrome that adapters occasionally
|
|
28
|
+
// forget to trim — the article's own title/author/publishTime are supplied
|
|
29
|
+
// as separate fields on ArticleData, so duplicated nodes are redundant.
|
|
30
|
+
// `iframe` is NOT in this set — it's handled by a dedicated rule below that
|
|
31
|
+
// degrades to a link so embedded content (YouTube, Twitter, CodePen …) keeps
|
|
32
|
+
// a reachable URL in the exported markdown.
|
|
33
|
+
const STRIPPED_TAGS = [
|
|
34
|
+
'script', 'style', 'noscript',
|
|
35
|
+
'canvas',
|
|
36
|
+
'form', 'button', 'dialog',
|
|
37
|
+
'header', 'footer', 'nav', 'aside',
|
|
38
|
+
];
|
|
39
|
+
function createTurndown(configure, cleanSelectors) {
|
|
40
|
+
const td = new TurndownService({
|
|
41
|
+
headingStyle: 'atx',
|
|
42
|
+
codeBlockStyle: 'fenced',
|
|
43
|
+
bulletListMarker: '-',
|
|
44
|
+
});
|
|
45
|
+
td.use(gfm);
|
|
46
|
+
td.remove(STRIPPED_TAGS);
|
|
47
|
+
// turndown-plugin-gfm@1.0.2 emits single-tilde strikethrough (`~x~`), which
|
|
48
|
+
// is not the canonical GFM form. Override it so exported markdown is
|
|
49
|
+
// portable across common renderers.
|
|
50
|
+
td.addRule('canonicalStrikethrough', {
|
|
51
|
+
filter: (node) => ['DEL', 'S', 'STRIKE'].includes(node.nodeName),
|
|
52
|
+
replacement: (content) => `~~${content}~~`,
|
|
53
|
+
});
|
|
54
|
+
// SVG isn't in the static HTML tag map; match by name with a custom filter.
|
|
55
|
+
td.addRule('stripSvg', {
|
|
56
|
+
filter: (node) => node.nodeName === 'svg' || node.nodeName === 'SVG',
|
|
57
|
+
replacement: () => '',
|
|
58
|
+
});
|
|
59
|
+
td.addRule('linebreak', {
|
|
60
|
+
filter: 'br',
|
|
61
|
+
replacement: () => '\n',
|
|
62
|
+
});
|
|
63
|
+
// Inline base64 images would land as huge ``
|
|
64
|
+
// strings that the image downloader can't localize. Drop them.
|
|
65
|
+
td.addRule('ignoreBase64Images', {
|
|
66
|
+
filter: (node) => {
|
|
67
|
+
if (node.nodeName !== 'IMG')
|
|
68
|
+
return false;
|
|
69
|
+
const src = node.getAttribute?.('src') ?? '';
|
|
70
|
+
return src.startsWith('data:');
|
|
71
|
+
},
|
|
72
|
+
replacement: () => '',
|
|
73
|
+
});
|
|
74
|
+
// Markdown has no native video/audio primitive. Emit inline HTML so
|
|
75
|
+
// renderers that support it (GitHub, VS Code preview …) still play the
|
|
76
|
+
// media; viewers that don't simply show the tag as text, which is still
|
|
77
|
+
// more information than dropping the node outright.
|
|
78
|
+
td.addRule('videoElement', {
|
|
79
|
+
filter: (node) => node.nodeName === 'VIDEO',
|
|
80
|
+
replacement: (_content, node) => {
|
|
81
|
+
const el = node;
|
|
82
|
+
const src = el.getAttribute('src')
|
|
83
|
+
|| el.querySelector('source')?.getAttribute('src')
|
|
84
|
+
|| '';
|
|
85
|
+
if (!src)
|
|
86
|
+
return '';
|
|
87
|
+
const poster = el.getAttribute('poster') || '';
|
|
88
|
+
return `\n<video src="${src}" controls${poster ? ` poster="${poster}"` : ''}></video>\n`;
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
td.addRule('audioElement', {
|
|
92
|
+
filter: (node) => node.nodeName === 'AUDIO',
|
|
93
|
+
replacement: (_content, node) => {
|
|
94
|
+
const el = node;
|
|
95
|
+
const src = el.getAttribute('src')
|
|
96
|
+
|| el.querySelector('source')?.getAttribute('src')
|
|
97
|
+
|| '';
|
|
98
|
+
return src ? `\n<audio src="${src}" controls></audio>\n` : '';
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
// Iframes (YouTube, Twitter, CodePen …) degrade to a markdown link so the
|
|
102
|
+
// embedded resource is still reachable from the exported file.
|
|
103
|
+
td.addRule('iframeToLink', {
|
|
104
|
+
filter: (node) => node.nodeName === 'IFRAME',
|
|
105
|
+
replacement: (_content, node) => {
|
|
106
|
+
const el = node;
|
|
107
|
+
const src = el.getAttribute('src') || '';
|
|
108
|
+
if (!src)
|
|
109
|
+
return '';
|
|
110
|
+
const title = el.getAttribute('title') || 'Embedded content';
|
|
111
|
+
return `\n[${title}](${src})\n`;
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
// Per-adapter dirty-node removal. Adapters know their site's specific noise
|
|
115
|
+
// (zhihu 折叠卡, weixin 赞赏栏, wiki 折叠 infobox …); we keep the default set
|
|
116
|
+
// empty so the generic converter stays untouched.
|
|
117
|
+
const selectorRules = (cleanSelectors ?? [])
|
|
118
|
+
.map(sel => sel.trim())
|
|
119
|
+
.filter(Boolean);
|
|
120
|
+
if (selectorRules.length > 0) {
|
|
121
|
+
td.addRule('cleanSelectors', {
|
|
122
|
+
filter: (node) => {
|
|
123
|
+
const match = node.matches;
|
|
124
|
+
if (typeof match !== 'function')
|
|
125
|
+
return false;
|
|
126
|
+
return selectorRules.some((sel) => {
|
|
127
|
+
try {
|
|
128
|
+
return match.call(node, sel);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
replacement: () => '',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (configure)
|
|
139
|
+
configure(td);
|
|
140
|
+
return td;
|
|
141
|
+
}
|
|
142
|
+
function convertToMarkdown(contentHtml, codeBlocks, configure, cleanSelectors) {
|
|
143
|
+
const td = createTurndown(configure, cleanSelectors);
|
|
144
|
+
let md = td.turndown(contentHtml);
|
|
145
|
+
// Restore code block placeholders
|
|
146
|
+
codeBlocks.forEach((block, i) => {
|
|
147
|
+
const placeholder = `CODEBLOCK-PLACEHOLDER-${i}`;
|
|
148
|
+
const fenced = `\n\`\`\`${block.lang}\n${block.code}\n\`\`\`\n`;
|
|
149
|
+
md = md.replace(placeholder, fenced);
|
|
150
|
+
});
|
|
151
|
+
// Clean up
|
|
152
|
+
md = md.replace(/\u00a0/g, ' ');
|
|
153
|
+
// Turndown leaves behind lone dashes / middle dots when list bullets or
|
|
154
|
+
// decorative separators lose their surrounding inline context.
|
|
155
|
+
md = md.replace(/^[ \t]*[-·][ \t]*$/gm, '');
|
|
156
|
+
md = md.replace(/^[ \t]+$/gm, '');
|
|
157
|
+
md = md.replace(/[ \t]+$/gm, '');
|
|
158
|
+
md = md.replace(/\n{3,}/g, '\n\n');
|
|
159
|
+
return md;
|
|
160
|
+
}
|
|
161
|
+
function replaceImageUrls(md, urlMap) {
|
|
162
|
+
return md.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, imgUrl) => {
|
|
163
|
+
const local = urlMap[imgUrl];
|
|
164
|
+
return local ? `` : match;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
// ============================================================
|
|
168
|
+
// Image Downloading
|
|
169
|
+
// ============================================================
|
|
170
|
+
function defaultDetectImageExt(url) {
|
|
171
|
+
const extMatch = url.match(/\.(\w{3,4})(?:\?|$)/);
|
|
172
|
+
return extMatch ? extMatch[1] : 'jpg';
|
|
173
|
+
}
|
|
174
|
+
async function downloadImages(imgUrls, imgDir, headers, detectExt) {
|
|
175
|
+
const urlMap = {};
|
|
176
|
+
if (imgUrls.length === 0)
|
|
177
|
+
return urlMap;
|
|
178
|
+
const detect = detectExt || defaultDetectImageExt;
|
|
179
|
+
// Deduplicate image URLs
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
const uniqueUrls = imgUrls.filter(url => {
|
|
182
|
+
if (seen.has(url))
|
|
183
|
+
return false;
|
|
184
|
+
seen.add(url);
|
|
185
|
+
return true;
|
|
186
|
+
});
|
|
187
|
+
for (let i = 0; i < uniqueUrls.length; i += IMAGE_CONCURRENCY) {
|
|
188
|
+
const batch = uniqueUrls.slice(i, i + IMAGE_CONCURRENCY);
|
|
189
|
+
const results = await Promise.all(batch.map(async (rawUrl, j) => {
|
|
190
|
+
const index = i + j + 1;
|
|
191
|
+
let imgUrl = rawUrl;
|
|
192
|
+
if (imgUrl.startsWith('//'))
|
|
193
|
+
imgUrl = `https:${imgUrl}`;
|
|
194
|
+
const ext = detect(imgUrl);
|
|
195
|
+
const filename = `img_${String(index).padStart(3, '0')}.${ext}`;
|
|
196
|
+
const filepath = path.join(imgDir, filename);
|
|
197
|
+
try {
|
|
198
|
+
const result = await httpDownload(imgUrl, filepath, {
|
|
199
|
+
headers,
|
|
200
|
+
timeout: 15000,
|
|
201
|
+
});
|
|
202
|
+
if (result.success) {
|
|
203
|
+
return { remoteUrl: rawUrl, localPath: `images/${filename}` };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Skip failed downloads
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}));
|
|
211
|
+
for (const r of results) {
|
|
212
|
+
if (r)
|
|
213
|
+
urlMap[r.remoteUrl] = r.localPath;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return urlMap;
|
|
217
|
+
}
|
|
218
|
+
// ============================================================
|
|
219
|
+
// Main API
|
|
220
|
+
// ============================================================
|
|
221
|
+
/**
|
|
222
|
+
* Download an article to Markdown with optional image localization.
|
|
223
|
+
*
|
|
224
|
+
* Handles the full pipeline:
|
|
225
|
+
* 1. HTML → Markdown (via TurndownService)
|
|
226
|
+
* 2. Code block placeholder restoration
|
|
227
|
+
* 3. Batch image downloading with concurrency + deduplication
|
|
228
|
+
* 4. Image URL replacement in Markdown
|
|
229
|
+
* 5. Frontmatter generation (customizable labels)
|
|
230
|
+
* 6. File write
|
|
231
|
+
*/
|
|
232
|
+
export async function downloadArticle(data, options) {
|
|
233
|
+
const { output, downloadImages: shouldDownloadImages = true, imageHeaders, maxTitleLength = 80, configureTurndown, detectImageExt, frontmatterLabels, cleanSelectors, stdout = false, } = options;
|
|
234
|
+
const labels = { ...DEFAULT_LABELS, ...frontmatterLabels };
|
|
235
|
+
if (!data.title) {
|
|
236
|
+
return [{
|
|
237
|
+
title: 'Error',
|
|
238
|
+
author: '-',
|
|
239
|
+
publish_time: '-',
|
|
240
|
+
status: 'failed — no title',
|
|
241
|
+
size: '-',
|
|
242
|
+
saved: '-',
|
|
243
|
+
}];
|
|
244
|
+
}
|
|
245
|
+
if (!data.contentHtml) {
|
|
246
|
+
return [{
|
|
247
|
+
title: data.title,
|
|
248
|
+
author: data.author || '-',
|
|
249
|
+
publish_time: data.publishTime || '-',
|
|
250
|
+
status: 'failed — no content',
|
|
251
|
+
size: '-',
|
|
252
|
+
saved: '-',
|
|
253
|
+
}];
|
|
254
|
+
}
|
|
255
|
+
// Convert HTML to Markdown
|
|
256
|
+
let markdown = convertToMarkdown(data.contentHtml, data.codeBlocks || [], configureTurndown, cleanSelectors);
|
|
257
|
+
const safeTitle = sanitizeFilename(data.title, maxTitleLength);
|
|
258
|
+
// Download images only when writing to disk. In stdout mode remote URLs
|
|
259
|
+
// stay intact so the piped output is self-contained.
|
|
260
|
+
if (!stdout && shouldDownloadImages && data.imageUrls && data.imageUrls.length > 0) {
|
|
261
|
+
const articleDir = path.join(output, safeTitle);
|
|
262
|
+
fs.mkdirSync(articleDir, { recursive: true });
|
|
263
|
+
const imagesDir = path.join(articleDir, 'images');
|
|
264
|
+
fs.mkdirSync(imagesDir, { recursive: true });
|
|
265
|
+
const urlMap = await downloadImages(data.imageUrls, imagesDir, imageHeaders, detectImageExt);
|
|
266
|
+
markdown = replaceImageUrls(markdown, urlMap);
|
|
267
|
+
}
|
|
268
|
+
// Build frontmatter with customizable labels.
|
|
269
|
+
// Shape: `# Title\n[> meta\n...]\n---\n\n<markdown>` — exactly one blank
|
|
270
|
+
// line separates every section, so we never produce ≥3 consecutive newlines.
|
|
271
|
+
const headerLines = [`# ${data.title}`];
|
|
272
|
+
if (data.author)
|
|
273
|
+
headerLines.push(`> ${labels.author}: ${data.author}`);
|
|
274
|
+
if (data.publishTime)
|
|
275
|
+
headerLines.push(`> ${labels.publishTime}: ${data.publishTime}`);
|
|
276
|
+
if (data.sourceUrl)
|
|
277
|
+
headerLines.push(`> ${labels.sourceUrl}: ${data.sourceUrl}`);
|
|
278
|
+
const frontmatter = headerLines.join('\n') + '\n\n---\n\n';
|
|
279
|
+
const fullContent = frontmatter + markdown;
|
|
280
|
+
const size = Buffer.byteLength(fullContent, 'utf-8');
|
|
281
|
+
if (stdout) {
|
|
282
|
+
process.stdout.write(fullContent.endsWith('\n') ? fullContent : fullContent + '\n');
|
|
283
|
+
return [{
|
|
284
|
+
title: data.title,
|
|
285
|
+
author: data.author || '-',
|
|
286
|
+
publish_time: data.publishTime || '-',
|
|
287
|
+
status: 'success',
|
|
288
|
+
size: formatBytes(size),
|
|
289
|
+
saved: '-',
|
|
290
|
+
}];
|
|
291
|
+
}
|
|
292
|
+
const articleDir = path.join(output, safeTitle);
|
|
293
|
+
fs.mkdirSync(articleDir, { recursive: true });
|
|
294
|
+
const filename = `${safeTitle}.md`;
|
|
295
|
+
const filePath = path.join(articleDir, filename);
|
|
296
|
+
fs.writeFileSync(filePath, fullContent, 'utf-8');
|
|
297
|
+
return [{
|
|
298
|
+
title: data.title,
|
|
299
|
+
author: data.author || '-',
|
|
300
|
+
publish_time: data.publishTime || '-',
|
|
301
|
+
status: 'success',
|
|
302
|
+
size: formatBytes(size),
|
|
303
|
+
saved: filePath,
|
|
304
|
+
}];
|
|
305
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|