@runweave/server-linux-x64 0.1.8 → 0.1.10
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/manifest.json +1494 -161
- package/native.tar.gz +0 -0
- package/package.json +1 -1
- package/payload/.agents/plugins/marketplace.json +20 -0
- package/payload/app-server/build-info.json +4 -4
- package/payload/app-server/index.cjs +6 -2
- package/payload/app-server/index.cjs.map +1 -1
- package/payload/backend/activity-sqlite-worker.cjs +2 -0
- package/payload/backend/build-info.json +4 -4
- package/payload/backend/dev-resources/control.cjs +5 -1
- package/payload/backend/index.cjs +466 -295
- package/payload/backend/ios-simulators/pool.py +3 -3
- package/payload/backend/scheduled-tasks-sqlite-worker.cjs +2 -0
- package/payload/frontend/dist/assets/{_baseUniq-vo4uuhMd.js → _baseUniq--CWv8e7O.js} +1 -1
- package/payload/frontend/dist/assets/{access-_jCu7Rgu.js → access-BCF23Qto.js} +1 -1
- package/payload/frontend/dist/assets/{advanced-CqOhObYj.js → advanced-D8viCLue.js} +1 -1
- package/payload/frontend/dist/assets/{appearance-CrrD5gzC.js → appearance-wPVMx-tB.js} +1 -1
- package/payload/frontend/dist/assets/{arc-dbfyHSF0.js → arc-BXffHD7A.js} +1 -1
- package/payload/frontend/dist/assets/{architectureDiagram-Q4EWVU46-CntSCAsO.js → architectureDiagram-Q4EWVU46-BXHN9QCj.js} +1 -1
- package/payload/frontend/dist/assets/{blockDiagram-DXYQGD6D-Chp0D4qy.js → blockDiagram-DXYQGD6D-DoZjab_0.js} +1 -1
- package/payload/frontend/dist/assets/{c4Diagram-AHTNJAMY-CF91pOuW.js → c4Diagram-AHTNJAMY-Ds2E62Z8.js} +1 -1
- package/payload/frontend/dist/assets/channel-83CTXkVl.js +1 -0
- package/payload/frontend/dist/assets/{chunk-4BX2VUAB-B1gxlVu2.js → chunk-4BX2VUAB-Dk1H2-5_.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-4TB4RGXK-Cxt3mj9b.js → chunk-4TB4RGXK-Sg0gzSvg.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-55IACEB6-BJT6qNik.js → chunk-55IACEB6-Du_EJG4S.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-EDXVE4YY-yI59bCa0.js → chunk-EDXVE4YY-BM1Cs5dU.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-FMBD7UC4-CDvncKKJ.js → chunk-FMBD7UC4-BM9thfyY.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-OYMX7WX6-BblOIU57.js → chunk-OYMX7WX6-C9Bvn4fo.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-QZHKN3VN-rODzNdO0.js → chunk-QZHKN3VN-CaakbFhJ.js} +1 -1
- package/payload/frontend/dist/assets/{chunk-YZCP3GAM-B6D4wQwk.js → chunk-YZCP3GAM-DkVNrd40.js} +1 -1
- package/payload/frontend/dist/assets/classDiagram-6PBFFD2Q-oR8IYJwC.js +1 -0
- package/payload/frontend/dist/assets/classDiagram-v2-HSJHXN6E-oR8IYJwC.js +1 -0
- package/payload/frontend/dist/assets/clone-BIDF-bd9.js +1 -0
- package/payload/frontend/dist/assets/connection-DIdF_3aa.js +12 -0
- package/payload/frontend/dist/assets/{connections-DqDmxS-O.js → connections-DBYC8SFl.js} +1 -1
- package/payload/frontend/dist/assets/{cose-bilkent-S5V4N54A-BvwVWp29.js → cose-bilkent-S5V4N54A-B4-fwllY.js} +1 -1
- package/payload/frontend/dist/assets/{dagre-KV5264BT-h_K1TH11.js → dagre-KV5264BT-Byu4wqLk.js} +1 -1
- package/payload/frontend/dist/assets/{desktop-BQ3kGtKG.js → desktop-aESkh0HL.js} +1 -1
- package/payload/frontend/dist/assets/{diagram-5BDNPKRD-B8g2SV6L.js → diagram-5BDNPKRD-DJW8DuCX.js} +1 -1
- package/payload/frontend/dist/assets/{diagram-G4DWMVQ6-BOEnrqYm.js → diagram-G4DWMVQ6-DTBvds5J.js} +1 -1
- package/payload/frontend/dist/assets/{diagram-MMDJMWI5-DlaN_z71.js → diagram-MMDJMWI5-D42e4xzm.js} +1 -1
- package/payload/frontend/dist/assets/{diagram-TYMM5635-C_pSKftn.js → diagram-TYMM5635-BjV43-C_.js} +1 -1
- package/payload/frontend/dist/assets/{erDiagram-SMLLAGMA-C2dL-h3I.js → erDiagram-SMLLAGMA-DE47Ibp_.js} +1 -1
- package/payload/frontend/dist/assets/{flowDiagram-DWJPFMVM-BmXhKWFj.js → flowDiagram-DWJPFMVM-CjGBlzS4.js} +1 -1
- package/payload/frontend/dist/assets/{ganttDiagram-T4ZO3ILL-D_Odnssf.js → ganttDiagram-T4ZO3ILL-vfj40KZi.js} +1 -1
- package/payload/frontend/dist/assets/{gitGraphDiagram-UUTBAWPF-DcQdcwKZ.js → gitGraphDiagram-UUTBAWPF-DT5xfRNb.js} +1 -1
- package/payload/frontend/dist/assets/{graph-C_XsxefA.js → graph-D-AF6yQT.js} +1 -1
- package/payload/frontend/dist/assets/{image-D3hgVr8O.js → image-D9gtSsD9.js} +1 -1
- package/payload/frontend/dist/assets/{index-CEUsFFQq.js → index-BPVP2P-2.js} +95 -105
- package/payload/frontend/dist/assets/{infoDiagram-42DDH7IO-C6TnRyMi.js → infoDiagram-42DDH7IO-C7ew_uLJ.js} +1 -1
- package/payload/frontend/dist/assets/{ishikawaDiagram-UXIWVN3A-BL7rfALJ.js → ishikawaDiagram-UXIWVN3A-B6-YebU9.js} +1 -1
- package/payload/frontend/dist/assets/{journeyDiagram-VCZTEJTY-C_6407GW.js → journeyDiagram-VCZTEJTY-BOFgJDA5.js} +1 -1
- package/payload/frontend/dist/assets/{kanban-definition-6JOO6SKY-CK61Fd0p.js → kanban-definition-6JOO6SKY-jh06wZYz.js} +1 -1
- package/payload/frontend/dist/assets/{layout-DVn6JKbl.js → layout-B4p7V5fB.js} +1 -1
- package/payload/frontend/dist/assets/{linear-DyWskq8H.js → linear-D2JWfvdc.js} +1 -1
- package/payload/frontend/dist/assets/main-CuRgiPQf.js +706 -0
- package/payload/frontend/dist/assets/main-D4_uoZQa.css +33 -0
- package/payload/frontend/dist/assets/{min-apbLpSJ3.js → min--jD4Arnm.js} +1 -1
- package/payload/frontend/dist/assets/{mindmap-definition-QFDTVHPH-MVHZZbma.js → mindmap-definition-QFDTVHPH-DmzjmSuf.js} +1 -1
- package/payload/frontend/dist/assets/{monitoring-BApsVez_.js → monitoring-Tvvk6TfB.js} +1 -1
- package/payload/frontend/dist/assets/{notifications-CjxSNLU1.js → notifications-CqP2AQrW.js} +1 -1
- package/payload/frontend/dist/assets/{pieDiagram-DEJITSTG-Dhbb0AYU.js → pieDiagram-DEJITSTG-B79qIVe5.js} +1 -1
- package/payload/frontend/dist/assets/{quadrantDiagram-34T5L4WZ-Yk6LOF0S.js → quadrantDiagram-34T5L4WZ-DIOj1DhE.js} +1 -1
- package/payload/frontend/dist/assets/{record-content-BCJ80KS5.js → record-content-CzGGCbNs.js} +1 -1
- package/payload/frontend/dist/assets/{requirementDiagram-MS252O5E-BUhS0PEq.js → requirementDiagram-MS252O5E-C-n1j3tN.js} +1 -1
- package/payload/frontend/dist/assets/{sankeyDiagram-XADWPNL6-D2sdxz14.js → sankeyDiagram-XADWPNL6-BVKqWE6C.js} +1 -1
- package/payload/frontend/dist/assets/{sequenceDiagram-FGHM5R23-CRere4Fy.js → sequenceDiagram-FGHM5R23-B980Icqs.js} +1 -1
- package/payload/frontend/dist/assets/{stateDiagram-FHFEXIEX-_pk-OMlm.js → stateDiagram-FHFEXIEX-C-mJHzhL.js} +1 -1
- package/payload/frontend/dist/assets/stateDiagram-v2-QKLJ7IA2-B4L4fK9G.js +1 -0
- package/payload/frontend/dist/assets/{status-Dk5rRVQF.js → status-Bv4Czr9-.js} +1 -1
- package/payload/frontend/dist/assets/suiji-DyzQqRTE.js +1 -0
- package/payload/frontend/dist/assets/{svg-X7YkMMvx.js → svg-C6Aj62ig.js} +1 -1
- package/payload/frontend/dist/assets/{tabs-BmKbcPVh.js → tabs-BTUOhdAF.js} +1 -1
- package/payload/frontend/dist/assets/{timeline-definition-GMOUNBTQ-BPsI2pMC.js → timeline-definition-GMOUNBTQ-Dr9yRuVq.js} +1 -1
- package/payload/frontend/dist/assets/{vennDiagram-DHZGUBPP-BNJkZ1Az.js → vennDiagram-DHZGUBPP-B0Xsg1-Z.js} +1 -1
- package/payload/frontend/dist/assets/{wardley-RL74JXVD-CVcYeATS.js → wardley-RL74JXVD-BjIKKaA_.js} +1 -1
- package/payload/frontend/dist/assets/{wardleyDiagram-NUSXRM2D-BpLJ0alk.js → wardleyDiagram-NUSXRM2D-DMJ7MEA9.js} +1 -1
- package/payload/frontend/dist/assets/{xychartDiagram-5P7HB3ND-BdsYYk4M.js → xychartDiagram-5P7HB3ND-2tvgfdab.js} +1 -1
- package/payload/frontend/dist/build-info.json +4 -4
- package/payload/frontend/dist/companion.html +1 -1
- package/payload/frontend/dist/index.html +3 -3
- package/payload/packages/agent-bridge/src/install-pi.mjs +81 -0
- package/payload/plugins/pi/dist/SKILL.md +22 -0
- package/payload/plugins/pi/dist/bridge/app-server-client.cjs +82 -0
- package/payload/plugins/pi/dist/bridge/feishu_stop_notify.sh +169 -0
- package/payload/plugins/pi/dist/bridge/runweave-hook-bridge.cjs +583 -0
- package/payload/plugins/pi/dist/bridge/runweave-hook-completion.cjs +42 -0
- package/payload/plugins/pi/dist/bridge/runweave-hook-payload.cjs +578 -0
- package/payload/plugins/pi/dist/bridge/runweave-task-supervision.cjs +167 -0
- package/payload/plugins/pi/dist/runweave.js +316 -0
- package/payload/plugins/toolkit/.codex-plugin/plugin.json +31 -0
- package/payload/plugins/toolkit/README.md +185 -0
- package/payload/plugins/toolkit/hooks/AGENTS.md +6 -0
- package/payload/plugins/toolkit/hooks/app-server-client.cjs +82 -0
- package/payload/plugins/toolkit/hooks/feishu_stop_notify.sh +169 -0
- package/payload/plugins/toolkit/hooks/runweave-hook-bridge.cjs +583 -0
- package/payload/plugins/toolkit/hooks/runweave-hook-completion.cjs +42 -0
- package/payload/plugins/toolkit/hooks/runweave-hook-dispatch.cjs +123 -0
- package/payload/plugins/toolkit/hooks/runweave-hook-payload.cjs +578 -0
- package/payload/plugins/toolkit/hooks/runweave-task-supervision.cjs +167 -0
- package/payload/plugins/toolkit/hooks.json +108 -0
- package/payload/plugins/toolkit/skills/acceptance/SKILL.md +70 -0
- package/payload/plugins/toolkit/skills/acceptance/agents/openai.yaml +4 -0
- package/payload/plugins/toolkit/skills/acceptance/references/capture.md +38 -0
- package/payload/plugins/toolkit/skills/acceptance/references/delivery.md +54 -0
- package/payload/plugins/toolkit/skills/acceptance/references/gallery-contract.md +84 -0
- package/payload/plugins/toolkit/skills/acceptance/references/gallery-copy.md +34 -0
- package/payload/plugins/toolkit/skills/acceptance/references/video.md +52 -0
- package/payload/plugins/toolkit/skills/acceptance/scripts/capture.py +137 -0
- package/payload/plugins/toolkit/skills/acceptance/scripts/common.py +63 -0
- package/payload/plugins/toolkit/skills/acceptance/scripts/gallery.py +353 -0
- package/payload/plugins/toolkit/skills/acceptance/scripts/preview.py +109 -0
- package/payload/plugins/toolkit/skills/acceptance/scripts/video.py +283 -0
- package/payload/plugins/toolkit/skills/acceptance/templates/reader.css +576 -0
- package/payload/plugins/toolkit/skills/acceptance/templates/reader.html +63 -0
- package/payload/plugins/toolkit/skills/acceptance/templates/reader.js +326 -0
- package/payload/plugins/toolkit/skills/acceptance/templates/viewer.css +55 -0
- package/payload/plugins/toolkit/skills/acceptance-gallery/SKILL.md +8 -0
- package/payload/plugins/toolkit/skills/acceptance-gallery/agents/openai.yaml +6 -0
- package/payload/plugins/toolkit/skills/acceptance-gallery/references/capture-and-copy.md +3 -0
- package/payload/plugins/toolkit/skills/acceptance-gallery/references/content-contract.md +3 -0
- package/payload/plugins/toolkit/skills/acceptance-gallery/scripts/gallery.py +9 -0
- package/payload/plugins/toolkit/skills/acceptance-video/SKILL.md +8 -0
- package/payload/plugins/toolkit/skills/acceptance-video/agents/openai.yaml +6 -0
- package/payload/plugins/toolkit/skills/acceptance-video/references/input-and-capture.md +3 -0
- package/payload/plugins/toolkit/skills/acceptance-video/references/mobile-review.md +3 -0
- package/payload/plugins/toolkit/skills/acceptance-video/scripts/capture.py +9 -0
- package/payload/plugins/toolkit/skills/acceptance-video/scripts/video.py +9 -0
- package/payload/plugins/toolkit/skills/agent-device/SKILL.md +86 -0
- package/payload/plugins/toolkit/skills/agent-device/agents/openai.yaml +4 -0
- package/payload/plugins/toolkit/skills/agent-device/references/ios-setup.md +36 -0
- package/payload/plugins/toolkit/skills/agent-device/scripts/device.py +398 -0
- package/payload/plugins/toolkit/skills/agent-device/scripts/resources.py +248 -0
- package/payload/plugins/toolkit/skills/architecture-flow-prototyping/SKILL.md +128 -0
- package/payload/plugins/toolkit/skills/architecture-flow-prototyping/agents/openai.yaml +4 -0
- package/payload/plugins/toolkit/skills/architecture-flow-prototyping/references/architecture-flow-checklist.md +59 -0
- package/payload/plugins/toolkit/skills/architecture-flow-prototyping/references/architecture-flow-guide.md +135 -0
- package/payload/plugins/toolkit/skills/architecture-flow-prototyping/references/architecture-flow-handoff-template.md +57 -0
- package/payload/plugins/toolkit/skills/ask-first/SKILL.md +81 -0
- package/payload/plugins/toolkit/skills/brainstorming/SKILL.md +41 -0
- package/payload/plugins/toolkit/skills/browse-record/SKILL.md +35 -0
- package/payload/plugins/toolkit/skills/browse-record/references/app-usage.md +19 -0
- package/payload/plugins/toolkit/skills/browse-record/references/daily-reporting.md +115 -0
- package/payload/plugins/toolkit/skills/browse-record/references/zhihu.md +33 -0
- package/payload/plugins/toolkit/skills/browse-record/scripts/app_usage.py +110 -0
- package/payload/plugins/toolkit/skills/browse-record/scripts/browse_job.py +385 -0
- package/payload/plugins/toolkit/skills/browse-record/scripts/zhihu-read.js +126 -0
- package/payload/plugins/toolkit/skills/browse-record/scripts/zhihu_http.py +273 -0
- package/payload/plugins/toolkit/skills/clear-technical-explanations/LICENSE +21 -0
- package/payload/plugins/toolkit/skills/clear-technical-explanations/SKILL.md +103 -0
- package/payload/plugins/toolkit/skills/code-grounded-requirements/SKILL.md +54 -0
- package/payload/plugins/toolkit/skills/daily-doc-maintenance/SKILL.md +151 -0
- package/payload/plugins/toolkit/skills/daily-record/SKILL.md +63 -0
- package/payload/plugins/toolkit/skills/daily-record/agents/openai.yaml +4 -0
- package/payload/plugins/toolkit/skills/daily-record/references/browser-history.md +6 -0
- package/payload/plugins/toolkit/skills/daily-record/references/feishu.md +90 -0
- package/payload/plugins/toolkit/skills/daily-record/references/scheduled-reporting.md +144 -0
- package/payload/plugins/toolkit/skills/daily-record/references/scheduled-task.template.json +15 -0
- package/payload/plugins/toolkit/skills/daily-record/references/suiji-work-records.md +57 -0
- package/payload/plugins/toolkit/skills/daily-record/scripts/feishu.py +280 -0
- package/payload/plugins/toolkit/skills/daily-record/scripts/job.py +416 -0
- package/payload/plugins/toolkit/skills/daily-record/scripts/job_browser.py +77 -0
- package/payload/plugins/toolkit/skills/daily-record/scripts/job_facts.py +132 -0
- package/payload/plugins/toolkit/skills/daily-record/scripts/job_isolation.py +69 -0
- package/payload/plugins/toolkit/skills/daily-record/scripts/job_materials.py +154 -0
- package/payload/plugins/toolkit/skills/daily-record/scripts/job_screen.py +77 -0
- package/payload/plugins/toolkit/skills/daily-record/scripts/job_state.py +309 -0
- package/payload/plugins/toolkit/skills/daily-refactor/SKILL.md +101 -0
- package/payload/plugins/toolkit/skills/daily-refactor/scripts/compare-architecture-reports.mjs +168 -0
- package/payload/plugins/toolkit/skills/debugging-strategies/SKILL.md +38 -0
- package/payload/plugins/toolkit/skills/debugging-strategies/resources/implementation-playbook.md +520 -0
- package/payload/plugins/toolkit/skills/diagnostic-log-debugging/SKILL.md +79 -0
- package/payload/plugins/toolkit/skills/doc-coauthoring/SKILL.md +64 -0
- package/payload/plugins/toolkit/skills/eli5/LICENSE +21 -0
- package/payload/plugins/toolkit/skills/eli5/SKILL.md +18 -0
- package/payload/plugins/toolkit/skills/experience/SKILL.md +71 -0
- package/payload/plugins/toolkit/skills/experience/package.json +13 -0
- package/payload/plugins/toolkit/skills/git-advanced-workflows/SKILL.md +457 -0
- package/payload/plugins/toolkit/skills/github-pr/SKILL.md +109 -0
- package/payload/plugins/toolkit/skills/github-pr/scripts/github-access.mjs +292 -0
- package/payload/plugins/toolkit/skills/github-pr/scripts/preflight.mjs +191 -0
- package/payload/plugins/toolkit/skills/github-pr/scripts/source-worktree.mjs +354 -0
- package/payload/plugins/toolkit/skills/grill-with-docs/SKILL.md +66 -0
- package/payload/plugins/toolkit/skills/grill-with-docs/agents/openai.yaml +6 -0
- package/payload/plugins/toolkit/skills/grilling/SKILL.md +14 -0
- package/payload/plugins/toolkit/skills/handoff/SKILL.md +98 -0
- package/payload/plugins/toolkit/skills/karpathy-guidelines/SKILL.md +73 -0
- package/payload/plugins/toolkit/skills/playwright-cli/SKILL.md +562 -0
- package/payload/plugins/toolkit/skills/playwright-cli/references/element-attributes.md +23 -0
- package/payload/plugins/toolkit/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/payload/plugins/toolkit/skills/playwright-cli/references/request-mocking.md +87 -0
- package/payload/plugins/toolkit/skills/playwright-cli/references/running-code.md +240 -0
- package/payload/plugins/toolkit/skills/playwright-cli/references/session-management.md +226 -0
- package/payload/plugins/toolkit/skills/playwright-cli/references/spec-driven-testing.md +312 -0
- package/payload/plugins/toolkit/skills/playwright-cli/references/storage-state.md +275 -0
- package/payload/plugins/toolkit/skills/playwright-cli/references/test-generation.md +138 -0
- package/payload/plugins/toolkit/skills/playwright-cli/references/tracing.md +142 -0
- package/payload/plugins/toolkit/skills/playwright-cli/references/video-recording.md +157 -0
- package/payload/plugins/toolkit/skills/playwright-cli/templates/standalone.config.json +7 -0
- package/payload/plugins/toolkit/skills/product-exploration/SKILL.md +64 -0
- package/payload/plugins/toolkit/skills/product-exploration/agents/openai.yaml +5 -0
- package/payload/plugins/toolkit/skills/prototype-first-development/SKILL.md +134 -0
- package/payload/plugins/toolkit/skills/prototype-first-development/references/html-prototype-template/README.md +88 -0
- package/payload/plugins/toolkit/skills/prototype-first-development/references/html-prototype-template/app.js +145 -0
- package/payload/plugins/toolkit/skills/prototype-first-development/references/html-prototype-template/index.html +229 -0
- package/payload/plugins/toolkit/skills/prototype-first-development/references/html-prototype-template/mock-state.json +60 -0
- package/payload/plugins/toolkit/skills/prototype-first-development/references/implementation-plan-handoff-template.md +61 -0
- package/payload/plugins/toolkit/skills/prototype-first-development/references/prototype-workflow-checklist.md +64 -0
- package/payload/plugins/toolkit/skills/publish-runweave-server/SKILL.md +69 -0
- package/payload/plugins/toolkit/skills/publish-runweave-server/agents/openai.yaml +4 -0
- package/payload/plugins/toolkit/skills/react-best-practices/AGENTS.md +3227 -0
- package/payload/plugins/toolkit/skills/react-best-practices/README.md +127 -0
- package/payload/plugins/toolkit/skills/react-best-practices/SKILL.md +143 -0
- package/payload/plugins/toolkit/skills/react-best-practices/metadata.json +15 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/advanced/event-handler-refs.md +55 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/advanced/init-once.md +42 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/advanced/use-latest.md +39 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/async/api-routes.md +38 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/async/defer-await.md +80 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/async/dependencies.md +52 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/async/parallel.md +28 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/async/suspense-boundaries.md +99 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/bundle/barrel-imports.md +59 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/bundle/conditional.md +37 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/bundle/defer-third-party.md +49 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/bundle/dynamic-imports.md +35 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/bundle/preload.md +46 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/client/event-listeners.md +78 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/client/localstorage-schema.md +77 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/client/passive-event-listeners.md +48 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/client/swr-dedup.md +56 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/batch-dom-css.md +110 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/cache-function-results.md +80 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/cache-property-access.md +28 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/cache-storage.md +70 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/combine-iterations.md +32 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/early-exit.md +50 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/flatmap-filter.md +55 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/hoist-regexp.md +45 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/index-maps.md +37 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/length-check-first.md +50 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/min-max-loop.md +82 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/set-map-lookups.md +24 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/js/tosorted-immutable.md +57 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/meta/sections.md +46 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/meta/template.md +28 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rendering/activity.md +26 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rendering/animate-svg-wrapper.md +38 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rendering/conditional-render.md +32 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rendering/content-visibility.md +38 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rendering/hoist-jsx.md +36 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rendering/hydration-no-flicker.md +72 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rendering/hydration-suppress-warning.md +26 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rendering/resource-hints.md +89 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rendering/script-defer-async.md +71 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rendering/svg-precision.md +28 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rendering/usetransition-loading.md +75 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/defer-reads.md +39 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/dependencies.md +45 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/derived-state-no-effect.md +40 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/derived-state.md +29 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/functional-setstate.md +77 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/lazy-state-init.md +58 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/memo-with-default-value.md +36 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/memo.md +44 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/move-effect-to-event.md +45 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/no-inline-components.md +83 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/simple-expression-in-memo.md +35 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/transitions.md +40 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/rerender/use-ref-transient-values.md +73 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/server/after-nonblocking.md +74 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/server/auth-actions.md +96 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/server/cache-lru.md +41 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/server/cache-react.md +76 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/server/dedup-props.md +65 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/server/hoist-static-io.md +139 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/server/parallel-fetching.md +83 -0
- package/payload/plugins/toolkit/skills/react-best-practices/rules/server/serialization.md +38 -0
- package/payload/plugins/toolkit/skills/recorded-browser-mcp-verification/SKILL.md +8 -0
- package/payload/plugins/toolkit/skills/recorded-browser-mcp-verification/agents/openai.yaml +6 -0
- package/payload/plugins/toolkit/skills/reproduce-before-fix/SKILL.md +93 -0
- package/payload/plugins/toolkit/skills/reproduce-before-fix/agents/openai.yaml +7 -0
- package/payload/plugins/toolkit/skills/review-only/SKILL.md +72 -0
- package/payload/plugins/toolkit/skills/review-with-runtime-evidence/SKILL.md +134 -0
- package/payload/plugins/toolkit/skills/review-with-runtime-evidence/agents/openai.yaml +7 -0
- package/payload/plugins/toolkit/skills/review-with-runtime-evidence/references/contracts.md +114 -0
- package/payload/plugins/toolkit/skills/review-with-runtime-evidence/scripts/classify-runtime-verdict.mjs +140 -0
- package/payload/plugins/toolkit/skills/review-with-runtime-evidence/scripts/run-static-review.mjs +389 -0
- package/payload/plugins/toolkit/skills/review-with-runtime-evidence/scripts/static-review-contract.mjs +243 -0
- package/payload/plugins/toolkit/skills/run-test-cases/SKILL.md +63 -0
- package/payload/plugins/toolkit/skills/runweave-change-validation/SKILL.md +196 -0
- package/payload/plugins/toolkit/skills/runweave-change-validation/agents/openai.yaml +7 -0
- package/payload/plugins/toolkit/skills/runweave-change-validation/scripts/assert-dev-session-status.mjs +181 -0
- package/payload/plugins/toolkit/skills/runweave-dev-session/SKILL.md +126 -0
- package/payload/plugins/toolkit/skills/runweave-dev-session/agents/openai.yaml +7 -0
- package/payload/plugins/toolkit/skills/runweave-management/SKILL.md +47 -0
- package/payload/plugins/toolkit/skills/scheduled-task/SKILL.md +35 -0
- package/payload/plugins/toolkit/skills/shadcn/SKILL.md +245 -0
- package/payload/plugins/toolkit/skills/shadcn/agents/openai.yml +5 -0
- package/payload/plugins/toolkit/skills/shadcn/assets/shadcn-small.png +0 -0
- package/payload/plugins/toolkit/skills/shadcn/assets/shadcn.png +0 -0
- package/payload/plugins/toolkit/skills/shadcn/cli.md +276 -0
- package/payload/plugins/toolkit/skills/shadcn/customization.md +209 -0
- package/payload/plugins/toolkit/skills/shadcn/evals/evals.json +47 -0
- package/payload/plugins/toolkit/skills/shadcn/mcp.md +94 -0
- package/payload/plugins/toolkit/skills/shadcn/rules/base-vs-radix.md +310 -0
- package/payload/plugins/toolkit/skills/shadcn/rules/composition.md +197 -0
- package/payload/plugins/toolkit/skills/shadcn/rules/forms.md +198 -0
- package/payload/plugins/toolkit/skills/shadcn/rules/icons.md +101 -0
- package/payload/plugins/toolkit/skills/shadcn/rules/styling.md +163 -0
- package/payload/plugins/toolkit/skills/suiji/SKILL.md +54 -0
- package/payload/plugins/toolkit/skills/suiji/references/api.md +72 -0
- package/payload/plugins/toolkit/skills/suiji/references/source-adapter.md +40 -0
- package/payload/plugins/toolkit/skills/suiji/references/work-records.md +36 -0
- package/payload/plugins/toolkit/skills/suiji/scripts/source_adapter.py +46 -0
- package/payload/plugins/toolkit/skills/suiji/scripts/suiji.py +358 -0
- package/payload/plugins/toolkit/skills/suiji/scripts/work_records.py +328 -0
- package/payload/plugins/toolkit/skills/suiji/scripts/work_reports.py +75 -0
- package/payload/plugins/toolkit/skills/sync-branch-hard-reset/SKILL.md +59 -0
- package/payload/plugins/toolkit/skills/update-runweave-backend/SKILL.md +90 -0
- package/payload/plugins/toolkit/skills/update-runweave-backend/agents/openai.yaml +4 -0
- package/payload/plugins/toolkit/skills/update-runweave-backend/references/npm-update.md +68 -0
- package/payload/plugins/toolkit/skills/update-runweave-backend/references/systemd.md +34 -0
- package/payload/plugins/toolkit/skills/update-runweave-desktop/SKILL.md +102 -0
- package/payload/plugins/toolkit/skills/update-runweave-ios/SKILL.md +16 -0
- package/payload/plugins/toolkit/skills/update-runweave-ios/references/ios-wireless-recovery.md +54 -0
- package/payload/plugins/toolkit/skills/using-rw/SKILL.md +265 -0
- package/payload/plugins/toolkit/skills/write-plan/SKILL.md +72 -0
- package/payload/plugins/toolkit/skills/write-test-cases/SKILL.md +100 -0
- package/payload/plugins/toolkit/skills/write-test-cases/assets/test-plan-template.yaml +15 -0
- package/payload/plugins/toolkit/skills/write-test-cases/references/coverage-checklist.md +75 -0
- package/payload/plugins/toolkit/skills/write-test-cases/references/design-techniques.md +91 -0
- package/payload/plugins/toolkit/skills/youtube-reading/SKILL.md +35 -0
- package/payload/plugins/toolkit/skills/youtube-reading/agents/openai.yaml +4 -0
- package/payload/plugins/toolkit/skills/youtube-reading/assets/runtime-lock.json +92 -0
- package/payload/plugins/toolkit/skills/youtube-reading/references/content-contract.md +52 -0
- package/payload/plugins/toolkit/skills/youtube-reading/references/setup.md +70 -0
- package/payload/plugins/toolkit/skills/youtube-reading/requirements.lock +9 -0
- package/payload/plugins/toolkit/skills/youtube-reading/scripts/artifacts.py +206 -0
- package/payload/plugins/toolkit/skills/youtube-reading/scripts/runtime.py +424 -0
- package/payload/plugins/toolkit/skills/youtube-reading/scripts/youtube_reading.py +314 -0
- package/payload/plugins/toolkit-release.json +1 -0
- package/payload/pty-host/index.cjs +99 -31
- package/payload/scripts/toolkit/codex-state.mjs +75 -0
- package/payload/scripts/toolkit/release.mjs +210 -0
- package/payload/scripts/toolkit/sync-runtime.mjs +108 -0
- package/payload/scripts/toolkit/sync.mjs +585 -0
- package/payload/server/index.cjs +5 -1
- package/payload/frontend/dist/assets/channel-Bf3IeONI.js +0 -1
- package/payload/frontend/dist/assets/classDiagram-6PBFFD2Q-CFsG2SvG.js +0 -1
- package/payload/frontend/dist/assets/classDiagram-v2-HSJHXN6E-CFsG2SvG.js +0 -1
- package/payload/frontend/dist/assets/clone-CeQXm2YN.js +0 -1
- package/payload/frontend/dist/assets/connection-XriXysSc.js +0 -12
- package/payload/frontend/dist/assets/main-Cg6Oo1xK.css +0 -33
- package/payload/frontend/dist/assets/main-H5R-3_w1.js +0 -696
- package/payload/frontend/dist/assets/stateDiagram-v2-QKLJ7IA2-CzpdUl_0.js +0 -1
- package/payload/frontend/dist/assets/suiji-CO_L5W1X.js +0 -1
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Private, frozen browsing batches; never writes work facts or schedules tasks."""
|
|
3
|
+
import argparse
|
|
4
|
+
import contextlib
|
|
5
|
+
import datetime as dt
|
|
6
|
+
import fcntl
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import socket
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
import uuid
|
|
16
|
+
from zoneinfo import ZoneInfo
|
|
17
|
+
sys.dont_write_bytecode = True
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "suiji" / "scripts"))
|
|
19
|
+
import suiji
|
|
20
|
+
import zhihu_http
|
|
21
|
+
import app_usage
|
|
22
|
+
import source_adapter
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def require(ok, code):
|
|
26
|
+
if not ok:
|
|
27
|
+
raise suiji.Failure(code)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read(path):
|
|
31
|
+
return json.loads(suiji.private_read(path))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write(path, value):
|
|
35
|
+
path = Path(path)
|
|
36
|
+
fd, temporary = tempfile.mkstemp(dir=path.parent, prefix=".browse-")
|
|
37
|
+
try:
|
|
38
|
+
with os.fdopen(fd, "w") as stream:
|
|
39
|
+
json.dump(value, stream, ensure_ascii=False, indent=2)
|
|
40
|
+
stream.flush()
|
|
41
|
+
os.fsync(stream.fileno())
|
|
42
|
+
os.replace(temporary, path)
|
|
43
|
+
directory = os.open(path.parent, os.O_RDONLY)
|
|
44
|
+
try:
|
|
45
|
+
os.fsync(directory)
|
|
46
|
+
finally:
|
|
47
|
+
os.close(directory)
|
|
48
|
+
finally:
|
|
49
|
+
if os.path.exists(temporary):
|
|
50
|
+
os.unlink(temporary)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def digest(value):
|
|
54
|
+
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()).hexdigest()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def directory(path):
|
|
58
|
+
p = Path(path)
|
|
59
|
+
require(not p.is_symlink(), "SYMLINK_REJECTED")
|
|
60
|
+
p.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
61
|
+
require(not p.stat().st_mode & 0o077, "PRIVATE_DIRECTORY_REQUIRED")
|
|
62
|
+
return p
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def config(path):
|
|
66
|
+
c = read(path)
|
|
67
|
+
require(set(c) - {"scheduledGroup", "transport", "sessionFile", "effectiveFrom", "archivePath"} == {"schemaVersion", "timeZone", "serverId", "ownerId", "connectionId", "externalAccountId", "machine", "profile", "group", "stateDir", "suijiConfig", "tokenFile"}, "INVALID_CONFIG_FIELDS")
|
|
68
|
+
require(type(c.get("scheduledGroup", False)) is bool, "INVALID_SCHEDULED_GROUP_POLICY")
|
|
69
|
+
require(c.get("schemaVersion") == 1 and c.get("timeZone") == "Asia/Shanghai", "INVALID_CONFIG")
|
|
70
|
+
for key in ("serverId", "ownerId", "connectionId"):
|
|
71
|
+
uuid.UUID(c[key])
|
|
72
|
+
require(c.get("externalAccountId") and c.get("machine") == socket.gethostname(), "MACHINE_OR_ACCOUNT_BINDING_MISMATCH")
|
|
73
|
+
for key in ("stateDir", "suijiConfig", "tokenFile"):
|
|
74
|
+
require(Path(c[key]).is_absolute(), "ABSOLUTE_PRIVATE_PATH_REQUIRED")
|
|
75
|
+
connection = read(c["suijiConfig"])
|
|
76
|
+
require(connection.get("mcpPath") == "/mcp/browse" and all(connection.get(k) == c[k] for k in ("serverId", "ownerId")), "BROWSE_CREDENTIAL_IDENTITY_REQUIRED")
|
|
77
|
+
require(c.get("transport", "browser") in ("browser", "http", "app-usage"), "INVALID_TRANSPORT")
|
|
78
|
+
if c.get("transport") == "app-usage":
|
|
79
|
+
require(c.get("profile") == "local-archive" and c.get("group") == "app-usage", "ARCHIVE_BINDING_REQUIRED")
|
|
80
|
+
require(isinstance(c.get("archivePath"), str) and Path(c["archivePath"]).is_absolute(), "ABSOLUTE_ARCHIVE_PATH_REQUIRED")
|
|
81
|
+
require(c["externalAccountId"] == app_usage.source_identity(c["archivePath"]), "ARCHIVE_IDENTITY_CHANGED")
|
|
82
|
+
else:
|
|
83
|
+
require("archivePath" not in c, "ARCHIVE_REQUIRES_APP_USAGE_TRANSPORT")
|
|
84
|
+
require(c.get("profile") in ("profile-1", "profile-2", "profile-3") and c.get("group"), "BROWSER_BINDING_REQUIRED")
|
|
85
|
+
if c.get("transport") == "http":
|
|
86
|
+
require(isinstance(c.get("sessionFile"), str) and Path(c["sessionFile"]).is_absolute(), "ABSOLUTE_SESSION_PATH_REQUIRED")
|
|
87
|
+
else:
|
|
88
|
+
require("sessionFile" not in c, "HTTP_SESSION_REQUIRES_HTTP_TRANSPORT")
|
|
89
|
+
if "effectiveFrom" in c:
|
|
90
|
+
require(dt.date.fromisoformat(c["effectiveFrom"]).isoformat() == c["effectiveFrom"], "INVALID_EFFECTIVE_DATE")
|
|
91
|
+
return c
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@contextlib.contextmanager
|
|
95
|
+
def lock(c):
|
|
96
|
+
root = directory(c["stateDir"])
|
|
97
|
+
locks = directory(Path.home() / ".runweave" / "browse-record" / "locks")
|
|
98
|
+
path = locks / (c["serverId"] + "-" + c["ownerId"] + "-" + c["connectionId"] + ".lock")
|
|
99
|
+
require(not path.is_symlink(), "SYMLINK_REJECTED")
|
|
100
|
+
with os.fdopen(os.open(path, os.O_CREAT | os.O_RDWR, 0o600), "w") as stream:
|
|
101
|
+
try:
|
|
102
|
+
fcntl.flock(stream, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
103
|
+
except BlockingIOError:
|
|
104
|
+
raise suiji.Failure("CONNECTION_BUSY") from None
|
|
105
|
+
yield root
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def command(argv, timeout=120):
|
|
109
|
+
result = subprocess.run(argv, capture_output=True, text=True, timeout=timeout, check=False)
|
|
110
|
+
if result.returncode != 0 or "### Error" in result.stdout:
|
|
111
|
+
code = next((c for c in ("NEEDS_LOGIN", "ACCOUNT_CHANGED_DURING_RUN", "ACCOUNT_CHANGED", "IDENTITY_UNAVAILABLE", "HISTORY_UNAVAILABLE_OR_PARSE_FAILED") if c in result.stdout), "BROWSER_UNAVAILABLE_OR_READ_FAILED")
|
|
112
|
+
raise suiji.Failure(code)
|
|
113
|
+
return result.stdout
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def browser_read(c, options, folder):
|
|
117
|
+
group = c["group"]
|
|
118
|
+
scheduled_run = os.environ.get("RUNWEAVE_SCHEDULED_TASK_RUN_ID")
|
|
119
|
+
if scheduled_run:
|
|
120
|
+
require(c.get("scheduledGroup") is True, "SCHEDULED_GROUP_NOT_AUTHORIZED")
|
|
121
|
+
uuid.UUID(scheduled_run)
|
|
122
|
+
group = "browser-group-scheduled-" + scheduled_run
|
|
123
|
+
resolution = json.loads(command([os.environ.get("RUNWEAVE_CLI_BIN", "rw"), "browser", "profile", "resolve", "--profile", c["profile"].split("-")[-1], "--group-id", group, "--json"]))
|
|
124
|
+
require(resolution.get("profileId") == c["profile"] and resolution.get("browserGroupId") == group, "BROWSER_BINDING_CHANGED")
|
|
125
|
+
write(folder / "browser-binding.json", {"machine": c["machine"], "profile": c["profile"], "group": group, "scheduledRunId": scheduled_run})
|
|
126
|
+
session = "browse-" + c["connectionId"]
|
|
127
|
+
cli = ["pnpm", "exec", "playwright", "cli", "-s=" + session]
|
|
128
|
+
# CDP credentials exist in process memory only; never saved in the config or batch.
|
|
129
|
+
command(cli + ["attach", "--cdp=" + resolution["cdpEndpoint"]])
|
|
130
|
+
try:
|
|
131
|
+
source = Path(__file__).with_name("zhihu-read.js").read_text()
|
|
132
|
+
script = folder / "reader.js"
|
|
133
|
+
descriptor = os.open(script, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
|
134
|
+
with os.fdopen(descriptor, "w") as stream:
|
|
135
|
+
stream.write(source.replace("/*BROWSE_OPTIONS*/ {}", json.dumps(options)))
|
|
136
|
+
try:
|
|
137
|
+
output = command(cli + ["run-code", "--filename=" + str(script)], timeout=1800)
|
|
138
|
+
require("### Result\n" in output, "BROWSER_RESULT_UNAVAILABLE")
|
|
139
|
+
return json.JSONDecoder().raw_decode(output.split("### Result\n", 1)[1])[0]
|
|
140
|
+
finally:
|
|
141
|
+
script.unlink(missing_ok=True)
|
|
142
|
+
finally:
|
|
143
|
+
command(cli + ["detach"])
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def authorize_http(c):
|
|
147
|
+
require(c.get("transport") == "http" and not os.environ.get("RUNWEAVE_SCHEDULED_TASK_RUN_ID"), "INTERACTIVE_HTTP_AUTHORIZATION_REQUIRED")
|
|
148
|
+
path = Path(c["sessionFile"])
|
|
149
|
+
directory(path.parent)
|
|
150
|
+
if path.exists() or path.is_symlink():
|
|
151
|
+
suiji.private_read(path)
|
|
152
|
+
resolution = json.loads(command([os.environ.get("RUNWEAVE_CLI_BIN", "rw"), "browser", "profile", "resolve", "--profile", c["profile"].split("-")[-1], "--group-id", c["group"], "--json"]))
|
|
153
|
+
require(resolution.get("profileId") == c["profile"] and resolution.get("browserGroupId") == c["group"], "BROWSER_BINDING_CHANGED")
|
|
154
|
+
cli = ["pnpm", "exec", "playwright", "cli", "-s=browse-http-auth-" + c["connectionId"]]
|
|
155
|
+
command(cli + ["attach", "--cdp=" + resolution["cdpEndpoint"]])
|
|
156
|
+
try:
|
|
157
|
+
script = """async page => {
|
|
158
|
+
const cookies = await page.context().cookies([
|
|
159
|
+
'https://www.zhihu.com/api/v4/me',
|
|
160
|
+
'https://api.zhihu.com/unify-consumption/read_history',
|
|
161
|
+
'https://zhuanlan.zhihu.com/api/articles/1'
|
|
162
|
+
]);
|
|
163
|
+
return {cookies, userAgent:await page.evaluate(() => navigator.userAgent)};
|
|
164
|
+
}"""
|
|
165
|
+
output = command(cli + ["run-code", script])
|
|
166
|
+
require("### Result\n" in output, "HTTP_AUTHORIZATION_UNAVAILABLE")
|
|
167
|
+
auth = json.JSONDecoder().raw_decode(output.split("### Result\n", 1)[1])[0]
|
|
168
|
+
auth["cookies"] = [cookie for cookie in auth["cookies"] if cookie.get("domain", "").lstrip(".") in ("zhihu.com", "www.zhihu.com", "api.zhihu.com", "zhuanlan.zhihu.com")]
|
|
169
|
+
auth.update(schemaVersion=1, **{k: c[k] for k in ("machine", "profile", "externalAccountId")})
|
|
170
|
+
require(zhihu_http.Session(auth, c).identity() == c["externalAccountId"], "ACCOUNT_CHANGED")
|
|
171
|
+
write(path, auth)
|
|
172
|
+
return {"authorized": True, "transport": "http", "sessionFile": str(path), "accountHash": hashlib.sha256(c["externalAccountId"].encode()).hexdigest()[:12]}
|
|
173
|
+
finally:
|
|
174
|
+
command(cli + ["detach"])
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def client(c):
|
|
178
|
+
value, info = suiji.connect(read(c["suijiConfig"]), c["tokenFile"])
|
|
179
|
+
require(info.get("features", {}).get("browse"), "BROWSE_SERVICE_UNAVAILABLE")
|
|
180
|
+
return value
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def adapter_receipt(c, start, end, result=None, error=None):
|
|
184
|
+
begin = dt.datetime.combine(dt.date.fromisoformat(start), dt.time(), ZoneInfo(c["timeZone"]))
|
|
185
|
+
finish = dt.datetime.combine(dt.date.fromisoformat(end) + dt.timedelta(days=1), dt.time(), ZoneInfo(c["timeZone"]))
|
|
186
|
+
items = {}
|
|
187
|
+
for value in (result or {}).get("items", []):
|
|
188
|
+
observed, entry = value["observation"], value["entry"]
|
|
189
|
+
key = observed["contentType"] + ":" + observed["contentId"] + ":" + entry["browseDate"]
|
|
190
|
+
item = {"id": key, "version": digest({k: entry[k] for k in ("title", "body", "sourceUrl", "contentState")}), "occurredAt": observed["readTime"]}
|
|
191
|
+
if key not in items or item["occurredAt"] > items[key]["occurredAt"]:
|
|
192
|
+
items[key] = item
|
|
193
|
+
provider = "app-usage" if c.get("transport") == "app-usage" else "zhihu"
|
|
194
|
+
return source_adapter.receipt("browse", "browse:" + provider + ":" + c["connectionId"], begin.isoformat(), finish.isoformat(), c.get("effectiveFrom"),
|
|
195
|
+
dt.datetime.now(dt.timezone.utc).isoformat(), "unavailable" if error else result["coverage"]["state"], list(items.values()),
|
|
196
|
+
[error] if error else result["coverage"]["errors"])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def prepare(c, config_path, start, end):
|
|
200
|
+
require(dt.date.fromisoformat(start).isoformat() == start and dt.date.fromisoformat(end).isoformat() == end and start <= end, "INVALID_DATE_RANGE")
|
|
201
|
+
with lock(c) as root:
|
|
202
|
+
active_root = directory(Path.home() / ".runweave" / "browse-record" / "active")
|
|
203
|
+
active_file = active_root / (c["serverId"] + "-" + c["ownerId"] + "-" + c["connectionId"] + ".json")
|
|
204
|
+
resume = None
|
|
205
|
+
if active_file.exists():
|
|
206
|
+
active = read(active_file)
|
|
207
|
+
path = Path(active["path"])
|
|
208
|
+
manifest = read(path / "manifest.json")
|
|
209
|
+
if manifest["state"] in ("prepared", "pending"):
|
|
210
|
+
require(path.parent == root.resolve(), "ORIGINAL_BATCH_BELONGS_TO_ANOTHER_STATE_DIRECTORY")
|
|
211
|
+
require(manifest["from"] == start and manifest["to"] == end, "RESOLVE_ORIGINAL_BATCH_FIRST")
|
|
212
|
+
return {"batch": str(path), "runId": manifest["runId"], "state": manifest["state"], "reused": True,
|
|
213
|
+
**({"adapterReceipt": str(path / "adapter-receipt.json")} if (path / "adapter-receipt.json").exists() else {})}
|
|
214
|
+
if manifest["state"] == "collecting":
|
|
215
|
+
require(path.parent == root.resolve() and manifest["from"] == start and manifest["to"] == end and manifest["configHash"] == digest(c), "RECOVER_ORIGINAL_RANGE_FIRST")
|
|
216
|
+
if (path / "payload.json").exists():
|
|
217
|
+
payload = read(path / "payload.json")
|
|
218
|
+
require(payload["identityBefore"] == c["externalAccountId"] == payload["identityAfter"], "ACCOUNT_CHANGED")
|
|
219
|
+
manifest.update(state="prepared", payloadHash=digest(payload))
|
|
220
|
+
write(path / "manifest.json", manifest)
|
|
221
|
+
return {"batch": str(path), "runId": manifest["runId"], "state": "prepared", "reused": True,
|
|
222
|
+
**({"adapterReceipt": str(path / "adapter-receipt.json")} if (path / "adapter-receipt.json").exists() else {})}
|
|
223
|
+
resume = (manifest["runId"], path)
|
|
224
|
+
run = resume[0] if resume else str(uuid.uuid4())
|
|
225
|
+
path = resume[1] if resume else directory(root / run)
|
|
226
|
+
manifest = {"state": "collecting", "runId": run, "from": start, "to": end, "config": str(Path(config_path).resolve()), "configHash": digest(c), "leaseExpiresAt": (dt.datetime.now(dt.timezone.utc) + dt.timedelta(minutes=30)).isoformat()}
|
|
227
|
+
write(path / "manifest.json", manifest)
|
|
228
|
+
write(active_file, {"path": str(path), "runId": run})
|
|
229
|
+
try:
|
|
230
|
+
options = {"from": start, "to": end, "externalAccountId": c["externalAccountId"], "connectionId": c["connectionId"]}
|
|
231
|
+
if c.get("transport") == "app-usage":
|
|
232
|
+
result = app_usage.collect(c, options)
|
|
233
|
+
elif c.get("transport") == "http":
|
|
234
|
+
write(path / "http-binding.json", {"machine": c["machine"], "profile": c["profile"], "transport": "http", "scheduledRunId": os.environ.get("RUNWEAVE_SCHEDULED_TASK_RUN_ID")})
|
|
235
|
+
result = zhihu_http.collect(c, options)
|
|
236
|
+
else:
|
|
237
|
+
result = browser_read(c, options, path)
|
|
238
|
+
require(result.get("identityBefore") == c["externalAccountId"] == result.get("identityAfter"), "ACCOUNT_CHANGED")
|
|
239
|
+
write(path / "adapter-receipt.json", adapter_receipt(c, start, end, result))
|
|
240
|
+
payload = {k: result[k] for k in ("identityBefore", "identityAfter", "items", "coverage")}
|
|
241
|
+
payload.update(runId=run, connectionId=c["connectionId"], externalAccountId=c["externalAccountId"], **{"from": start, "to": end})
|
|
242
|
+
write(path / "payload.json", payload)
|
|
243
|
+
manifest.update(state="prepared", payloadHash=digest(payload), historyUnchanged=result["historyUnchanged"])
|
|
244
|
+
write(path / "manifest.json", manifest)
|
|
245
|
+
return {"batch": str(path), "runId": run, "coverage": result["coverage"], "historyUnchanged": result["historyUnchanged"], "adapterReceipt": str(path / "adapter-receipt.json")}
|
|
246
|
+
except Exception as error:
|
|
247
|
+
code = str(error)
|
|
248
|
+
reason = "needs-login" if code == "NEEDS_LOGIN" else "identity-changed" if "ACCOUNT_CHANGED" in code else "parse-failed" if "PARSE" in code else "unavailable"
|
|
249
|
+
write(path / "adapter-receipt.json", adapter_receipt(c, start, end, error=reason))
|
|
250
|
+
manifest["error"] = reason
|
|
251
|
+
try:
|
|
252
|
+
client(c).call("report_browse_failure", {"idempotencyKey": run, "input": {"runId": run, "connectionId": c["connectionId"], "externalAccountId": c["externalAccountId"], "from": start, "to": end, "reason": reason}})
|
|
253
|
+
except Exception:
|
|
254
|
+
manifest["failureReceiptUnconfirmed"] = True
|
|
255
|
+
manifest["state"] = "failed"
|
|
256
|
+
write(path / "manifest.json", manifest)
|
|
257
|
+
raise
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def batch(path):
|
|
261
|
+
path = Path(path).resolve()
|
|
262
|
+
meta = read(path / "manifest.json")
|
|
263
|
+
c = config(meta["config"])
|
|
264
|
+
require(digest(c) == meta["configHash"] and path.parent == Path(c["stateDir"]).resolve(), "BATCH_IDENTITY_CHANGED")
|
|
265
|
+
return path, meta, c
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def import_batch(path, reconcile=False):
|
|
269
|
+
path, meta, c = batch(path)
|
|
270
|
+
with lock(c) as root:
|
|
271
|
+
if reconcile and meta["state"] == "failed":
|
|
272
|
+
cli = client(c)
|
|
273
|
+
try:
|
|
274
|
+
receipt = cli.call("get_browse_run", {"runId": meta["runId"]})
|
|
275
|
+
except suiji.Failure as error:
|
|
276
|
+
require(str(error) == "NOT_FOUND", "RECEIPT_READ_UNAVAILABLE")
|
|
277
|
+
receipt = cli.call("report_browse_failure", {"idempotencyKey": meta["runId"], "input": {"runId": meta["runId"], "connectionId": c["connectionId"], "externalAccountId": c["externalAccountId"], "from": meta["from"], "to": meta["to"], "reason": meta["error"]}})
|
|
278
|
+
require(meta["error"] in receipt["coverage"]["errors"], "FAILURE_RECEIPT_MISMATCH")
|
|
279
|
+
write(path / "receipt.json", receipt)
|
|
280
|
+
meta["failureReceiptUnconfirmed"] = False
|
|
281
|
+
write(path / "manifest.json", meta)
|
|
282
|
+
return {"runId": meta["runId"], "failed": True, "reported": True}
|
|
283
|
+
payload = read(path / "payload.json")
|
|
284
|
+
require(digest(payload) == meta["payloadHash"], "FROZEN_PAYLOAD_CHANGED")
|
|
285
|
+
require(meta["state"] in ("prepared", "pending", "confirmed"), "BATCH_NOT_PREPARED")
|
|
286
|
+
cli = client(c)
|
|
287
|
+
if reconcile or meta["state"] in ("pending", "confirmed"):
|
|
288
|
+
try:
|
|
289
|
+
receipt = cli.call("get_browse_run", {"runId": meta["runId"]})
|
|
290
|
+
except suiji.Failure as error:
|
|
291
|
+
require(str(error) == "NOT_FOUND", "RECEIPT_READ_UNAVAILABLE")
|
|
292
|
+
receipt = None
|
|
293
|
+
if receipt:
|
|
294
|
+
require(receipt["connectionId"] == c["connectionId"] and receipt["from"] == meta["from"] and receipt["to"] == meta["to"], "RECEIPT_IDENTITY_MISMATCH")
|
|
295
|
+
write(path / "receipt.json", receipt)
|
|
296
|
+
meta["state"] = "confirmed"; write(path / "manifest.json", meta)
|
|
297
|
+
write(root / (c["connectionId"] + ".checkpoint.json"), {"runId": meta["runId"], "from": meta["from"], "to": meta["to"], "coverage": receipt["coverage"]})
|
|
298
|
+
return {"runId": meta["runId"], "confirmed": True, "receipt": receipt["receipt"]}
|
|
299
|
+
meta["state"] = "pending"; write(path / "manifest.json", meta)
|
|
300
|
+
cli.call("import_browse_entries", {"idempotencyKey": meta["runId"], "input": payload})
|
|
301
|
+
# Independent readback confirms commit even when the original response was lost.
|
|
302
|
+
receipt = cli.call("get_browse_run", {"runId": meta["runId"]})
|
|
303
|
+
require(receipt["connectionId"] == c["connectionId"], "RECEIPT_IDENTITY_MISMATCH")
|
|
304
|
+
write(path / "receipt.json", receipt)
|
|
305
|
+
meta["state"] = "confirmed"; write(path / "manifest.json", meta)
|
|
306
|
+
write(root / (c["connectionId"] + ".checkpoint.json"), {"runId": meta["runId"], "from": meta["from"], "to": meta["to"], "coverage": receipt["coverage"]})
|
|
307
|
+
return {"runId": meta["runId"], "confirmed": True, "receipt": receipt["receipt"]}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def read_digest(c, config_path, day):
|
|
311
|
+
require(c.get("transport") != "app-usage", "APP_USAGE_HAS_NO_READING_DIGEST")
|
|
312
|
+
dt.date.fromisoformat(day)
|
|
313
|
+
with lock(c) as root:
|
|
314
|
+
cli = client(c)
|
|
315
|
+
entries, cursor, revision = [], None, None
|
|
316
|
+
while True:
|
|
317
|
+
args = {"source": "zhihu", "from": day, "to": day, "limit": 100}
|
|
318
|
+
if cursor:
|
|
319
|
+
args["cursor"] = cursor
|
|
320
|
+
page = cli.call("list_browse_entries", args)
|
|
321
|
+
require(revision in (None, page["browseRevision"]), "BROWSE_CHANGED: restart digest read")
|
|
322
|
+
revision = page["browseRevision"]
|
|
323
|
+
for entry in page["items"]:
|
|
324
|
+
if entry["source"]["connectionId"] == c["connectionId"]:
|
|
325
|
+
value = cli.call("get_browse_entry", {"id": entry["id"]})["entry"]
|
|
326
|
+
require(value["version"] == entry["version"] and not value["deletedAt"], "BROWSE_CHANGED")
|
|
327
|
+
entries.append(value)
|
|
328
|
+
cursor = page["nextCursor"]
|
|
329
|
+
if not cursor:
|
|
330
|
+
break
|
|
331
|
+
path = directory(root / str(uuid.uuid4()))
|
|
332
|
+
refs = [{"id": e["id"], "version": e["version"], "hash": e["contentHash"]} for e in entries]
|
|
333
|
+
value = {"date": day, "connectionId": c["connectionId"], "entries": entries, "entryRefs": refs, "inputRevision": revision}
|
|
334
|
+
write(path / "input.json", value)
|
|
335
|
+
write(path / "manifest.json", {"state": "digest-input", "config": str(Path(config_path).resolve()), "configHash": digest(c), "inputHash": digest(value), "key": str(uuid.uuid4())})
|
|
336
|
+
return {"batch": str(path), "input": str(path / "input.json"), "count": len(entries)}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def publish_digest(path, input_path):
|
|
340
|
+
path, meta, c = batch(path)
|
|
341
|
+
with lock(c):
|
|
342
|
+
frozen = read(path / "input.json")
|
|
343
|
+
require(digest(frozen) == meta["inputHash"], "DIGEST_INPUT_CHANGED")
|
|
344
|
+
value = read(input_path)
|
|
345
|
+
require(set(value) == {"title", "body", "entryRefs", "inputRevision"} and value["entryRefs"] == frozen["entryRefs"] and value["inputRevision"] == frozen["inputRevision"], "DIGEST_REFERENCES_CHANGED")
|
|
346
|
+
payload = {**value, "date": frozen["date"], "connectionId": c["connectionId"]}
|
|
347
|
+
if meta.get("payloadHash"):
|
|
348
|
+
require(meta["payloadHash"] == digest(payload), "FROZEN_DIGEST_CHANGED")
|
|
349
|
+
meta.update(state="pending-digest", payloadHash=digest(payload)); write(path / "manifest.json", meta)
|
|
350
|
+
cli = client(c)
|
|
351
|
+
result = cli.call("publish_browse_digest", {"idempotencyKey": meta["key"], "input": payload})
|
|
352
|
+
confirmed = cli.call("list_browse_digests", {"date": frozen["date"], "connectionId": c["connectionId"]})
|
|
353
|
+
require(any(item["id"] == result["digest"]["id"] and item["version"] == result["digest"]["version"] for item in confirmed["items"]), "DIGEST_READBACK_MISMATCH")
|
|
354
|
+
meta["state"] = "confirmed-digest"; write(path / "manifest.json", meta)
|
|
355
|
+
write(path / "receipt.json", result)
|
|
356
|
+
return {"confirmed": True, "id": result["digest"]["id"], "version": result["digest"]["version"]}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def main():
|
|
360
|
+
os.umask(0o077)
|
|
361
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
362
|
+
parser.add_argument("command", choices=["authorize-http", "prepare", "import", "reconcile", "read", "publish-digest"])
|
|
363
|
+
for name in ("config", "from", "to", "batch", "date", "input"):
|
|
364
|
+
parser.add_argument("--" + name)
|
|
365
|
+
args = parser.parse_args()
|
|
366
|
+
if args.command in ("import", "reconcile"):
|
|
367
|
+
return import_batch(args.batch, args.command == "reconcile")
|
|
368
|
+
if args.command == "publish-digest":
|
|
369
|
+
return publish_digest(args.batch, args.input)
|
|
370
|
+
c = config(args.config)
|
|
371
|
+
if args.command == "authorize-http":
|
|
372
|
+
return authorize_http(c)
|
|
373
|
+
if args.command == "read":
|
|
374
|
+
return read_digest(c, args.config, args.date)
|
|
375
|
+
now = dt.datetime.now(ZoneInfo("Asia/Shanghai")).date()
|
|
376
|
+
start = getattr(args, "from") or max((now - dt.timedelta(days=1)).isoformat(), c.get("effectiveFrom", "0001-01-01"))
|
|
377
|
+
return prepare(c, args.config, start, args.to or now.isoformat())
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
if __name__ == "__main__":
|
|
381
|
+
try:
|
|
382
|
+
print(json.dumps(main(), ensure_ascii=False))
|
|
383
|
+
except Exception as error:
|
|
384
|
+
print(json.dumps({"ok": False, "error": str(error) if isinstance(error, suiji.Failure) else type(error).__name__}), file=sys.stderr)
|
|
385
|
+
sys.exit(1)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/* global fetch, AbortSignal, URL, DOMParser, crypto */
|
|
2
|
+
// Playwright run-code consumes this file as one function expression; evaluate callbacks run in the browser.
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
4
|
+
async page => {
|
|
5
|
+
// prepare substitutes only non-secret identity/range options into this expression.
|
|
6
|
+
const options = /*BROWSE_OPTIONS*/ {};
|
|
7
|
+
const tabs = new Map();
|
|
8
|
+
const errors = [];
|
|
9
|
+
const capturedAt = new Date().toISOString();
|
|
10
|
+
const deadline = Date.now() + 20 * 60 * 1000;
|
|
11
|
+
const apiOrigin = "https://api.zhihu.com";
|
|
12
|
+
const allowed = url => /^https:\/\/(?:api\.zhihu\.com\/unify-consumption\/read_history(?:\?[^#]*)?|www\.zhihu\.com\/api\/v4\/(?:me|answers\/\d+)(?:\?[^#]*)?|zhuanlan\.zhihu\.com\/api\/articles\/\d+)$/.test(url);
|
|
13
|
+
const request = async url => {
|
|
14
|
+
if (Date.now() > deadline && url !== "https://www.zhihu.com/api/v4/me") throw new Error("RUN_BUDGET_EXCEEDED");
|
|
15
|
+
if (!allowed(url)) throw new Error("UNTRUSTED_API_URL");
|
|
16
|
+
const origin = url.match(/^https:\/\/[^/]+/)[0];
|
|
17
|
+
let tab = tabs.get(origin);
|
|
18
|
+
if (!tab) { tab = await page.context().newPage(); tabs.set(origin, tab); await tab.goto(url, { timeout: 15000, waitUntil: "domcontentloaded" }); }
|
|
19
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
20
|
+
const result = await tab.evaluate(async target => {
|
|
21
|
+
try {
|
|
22
|
+
const response = await fetch(target, { credentials: "include", signal: AbortSignal.timeout(15000) });
|
|
23
|
+
const text = await response.text();
|
|
24
|
+
let data; try { data = JSON.parse(text); } catch { return { status: response.status, error: "NON_JSON_OR_VERIFICATION" }; }
|
|
25
|
+
return { status: response.status, data };
|
|
26
|
+
} catch { return { status: 0, error: "TIMEOUT_OR_NETWORK" }; }
|
|
27
|
+
}, url);
|
|
28
|
+
if ((result.status === 0 || result.status === 429 || result.status >= 500) && attempt < 2) { await tab.waitForTimeout(500 * (attempt + 1)); continue; }
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const identity = async () => {
|
|
33
|
+
const r = await request("https://www.zhihu.com/api/v4/me");
|
|
34
|
+
if (r.status !== 200 || typeof r.data?.id !== "string" || !r.data.id) throw new Error(r.status === 401 || r.status === 403 ? "NEEDS_LOGIN" : "IDENTITY_UNAVAILABLE");
|
|
35
|
+
return r.data.id;
|
|
36
|
+
};
|
|
37
|
+
const historyUrl = `${apiOrigin}/unify-consumption/read_history?offset=0&limit=20`;
|
|
38
|
+
const parseHistory = response => {
|
|
39
|
+
if (response.status !== 200 || !Array.isArray(response.data?.data) || typeof response.data?.paging?.is_end !== "boolean") throw new Error(response.status === 401 || response.status === 403 ? "NEEDS_LOGIN" : "HISTORY_UNAVAILABLE_OR_PARSE_FAILED");
|
|
40
|
+
return response.data;
|
|
41
|
+
};
|
|
42
|
+
const parse = card => {
|
|
43
|
+
const value = card.card_type === "single_card" ? card.data : null, extra = value?.extra;
|
|
44
|
+
if (!extra || typeof extra.content_token !== "string" || typeof extra.content_type !== "string" || !Number.isFinite(extra.read_time)) throw new Error("UNKNOWN_HISTORY_STRUCTURE");
|
|
45
|
+
const occurredAt = new Date(extra.read_time * (extra.read_time > 1e12 ? 1 : 1000)).toISOString();
|
|
46
|
+
const day = new Date(Date.parse(occurredAt) + 8 * 3600000).toISOString().slice(0, 10);
|
|
47
|
+
return { type: extra.content_type, id: extra.content_token, occurredAt, day, questionId: extra.question_token,
|
|
48
|
+
title: typeof value.header?.title === "string" ? value.header.title : "知乎浏览内容" };
|
|
49
|
+
};
|
|
50
|
+
const signature = value => JSON.stringify(value.data.map(card => { const x = parse(card); return [x.type,x.id,x.occurredAt]; }));
|
|
51
|
+
try {
|
|
52
|
+
const identityBefore = await identity();
|
|
53
|
+
if (options.externalAccountId && identityBefore !== options.externalAccountId) throw new Error("ACCOUNT_CHANGED");
|
|
54
|
+
if (options.probe) return { identityBefore };
|
|
55
|
+
const observed = new Map(); let pages = 0, stable = false;
|
|
56
|
+
let baseline; let baselinePages = new Map();
|
|
57
|
+
for (let round = 0; round < 3; round++) {
|
|
58
|
+
baselinePages = new Map();
|
|
59
|
+
let next = historyUrl, first, complete = false; const seenPages = new Set();
|
|
60
|
+
for (let index = 0; index < 100; index++) {
|
|
61
|
+
if (seenPages.has(next)) { errors.push("PAGINATION_LOOP"); break; }
|
|
62
|
+
seenPages.add(next);
|
|
63
|
+
let value;
|
|
64
|
+
try { value = parseHistory(await request(next)); } catch (e) { errors.push(e.message); break; }
|
|
65
|
+
baselinePages.set(next, signature(value));
|
|
66
|
+
pages++; if (index === 0) { first = signature(value); baseline = baseline ?? first; }
|
|
67
|
+
if (value.data.length > 20) { errors.push("PAGE_LIMIT_EXCEEDED"); break; }
|
|
68
|
+
for (const card of value.data) {
|
|
69
|
+
try { const item = parse(card); if (item.day >= options.from && item.day <= options.to) observed.set(`${item.type}:${item.id}:${item.occurredAt}`, item); }
|
|
70
|
+
catch { errors.push("UNKNOWN_HISTORY_STRUCTURE"); }
|
|
71
|
+
}
|
|
72
|
+
if (value.paging.is_end) { complete = true; break; }
|
|
73
|
+
try { const url = await tabs.get(apiOrigin).evaluate(value => { const u = new URL(value); return { href: u.href, origin: u.origin, limit: u.searchParams.get("limit") }; }, value.paging.next); if (!allowed(url.href) || url.origin !== apiOrigin || url.limit !== "20") throw new Error(); next = url.href; }
|
|
74
|
+
catch { errors.push("UNTRUSTED_PAGINATION"); break; }
|
|
75
|
+
if (index === 99) errors.push("PAGE_CAP_REACHED");
|
|
76
|
+
}
|
|
77
|
+
try { const check = parseHistory(await request(historyUrl)); if (signature(check) === first) { stable = complete; break; } }
|
|
78
|
+
catch { errors.push("HOMEPAGE_RECHECK_FAILED"); break; }
|
|
79
|
+
if (round === 2) errors.push("HISTORY_UNSTABLE");
|
|
80
|
+
}
|
|
81
|
+
const plain = async html => {
|
|
82
|
+
const tab = tabs.get("https://www.zhihu.com");
|
|
83
|
+
return tab.evaluate(value => {
|
|
84
|
+
const safe = value.replace(/<(script|style|iframe|object|embed|video|audio)[\s\S]*?<\/\1\s*>/gi, "").replace(/<(img|source|link|meta)\b[^>]*>/gi, "");
|
|
85
|
+
const doc = new DOMParser().parseFromString(safe, "text/html");
|
|
86
|
+
for (const node of doc.querySelectorAll("p,div,br,li,h1,h2,h3,blockquote")) node.append("\n");
|
|
87
|
+
return (doc.body.textContent ?? "").replace(/\n{3,}/g, "\n\n").trim();
|
|
88
|
+
}, html);
|
|
89
|
+
};
|
|
90
|
+
const bodies = new Map(); const items = [];
|
|
91
|
+
for (const observation of observed.values()) {
|
|
92
|
+
const key = observation.type + ":" + observation.id;
|
|
93
|
+
let content = bodies.get(key);
|
|
94
|
+
if (!content) {
|
|
95
|
+
const supported = ["answer", "article"].includes(observation.type) && /^\d+$/.test(observation.id);
|
|
96
|
+
let body = "", state = "metadata-only", reason = supported ? null : "unsupported-content-type";
|
|
97
|
+
if (supported) {
|
|
98
|
+
const url = observation.type === "answer" ? `https://www.zhihu.com/api/v4/answers/${observation.id}?include=content,paid_info` : `https://zhuanlan.zhihu.com/api/articles/${observation.id}`;
|
|
99
|
+
try {
|
|
100
|
+
const r = await request(url);
|
|
101
|
+
if (r.status === 200 && String(r.data?.id) === observation.id && typeof r.data.content === "string") {
|
|
102
|
+
body = await plain(r.data.content); state = body ? "available" : "metadata-only";
|
|
103
|
+
if (r.data.is_paid === true && r.data.paid_info?.has_purchased !== true) { state = "metadata-only"; reason = "paid-content-limited"; }
|
|
104
|
+
if (!body) reason = "empty-content";
|
|
105
|
+
} else reason = r.status === 403 ? "forbidden" : r.status === 404 ? "not-found" : "content-unavailable-or-id-mismatch";
|
|
106
|
+
} catch { reason = "content-timeout-or-parse-failed"; }
|
|
107
|
+
}
|
|
108
|
+
content = { body, state, reason }; bodies.set(key, content);
|
|
109
|
+
}
|
|
110
|
+
const sourceUrl = observation.type === "answer" && /^\d+$/.test(observation.questionId ?? "") ? `https://www.zhihu.com/question/${observation.questionId}/answer/${observation.id}` : observation.type === "article" ? `https://zhuanlan.zhihu.com/p/${observation.id}` : null;
|
|
111
|
+
items.push({ observation: { contentType: observation.type, contentId: observation.id, readTime: observation.occurredAt, observedAt: capturedAt },
|
|
112
|
+
entry: { captureId: await tabs.get("https://www.zhihu.com").evaluate(() => crypto.randomUUID()), browseDate: observation.day, occurredAt: observation.occurredAt, capturedAt, title: observation.title, body: content.body, userNote: "", source: { provider: "zhihu", displayName: "知乎", connectionId: options.connectionId }, contentType: observation.type, captureMethod: "connector", sourceUrl, contentState: { state: content.state, reason: content.reason }, attachmentIds: [] } });
|
|
113
|
+
}
|
|
114
|
+
const identityAfter = await identity();
|
|
115
|
+
if (identityAfter !== identityBefore) throw new Error("ACCOUNT_CHANGED_DURING_RUN");
|
|
116
|
+
let historyUnchanged = false;
|
|
117
|
+
try { historyUnchanged = baseline === signature(parseHistory(await request(historyUrl))); }
|
|
118
|
+
catch { errors.push("HISTORY_RECHECK_FAILED"); }
|
|
119
|
+
for (const [url, before] of baselinePages) {
|
|
120
|
+
try { if (signature(parseHistory(await request(url))) !== before) historyUnchanged = false; }
|
|
121
|
+
catch { historyUnchanged = false; errors.push("HISTORY_RECHECK_FAILED"); }
|
|
122
|
+
}
|
|
123
|
+
if (!historyUnchanged) errors.push("HISTORY_CHANGED_DURING_CONTENT_READ");
|
|
124
|
+
return { identityBefore, identityAfter, items, historyUnchanged, coverage: { state: stable && errors.length === 0 ? "complete" : "partial", pages, metadata: items.length, content: items.filter(i => i.entry.contentState.state === "available").length, errors: [...new Set(errors)].slice(0,200) } };
|
|
125
|
+
} finally { for (const tab of tabs.values()) await tab.close(); }
|
|
126
|
+
}
|