@akairos/pipeline 0.18.1 → 0.19.0
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/ARTICLE-THEMES.md +2 -0
- package/README.md +27 -4
- package/package.json +7 -5
- package/src/adapter-loader.ts +24 -4
- package/src/adapter-types.ts +14 -0
- package/src/article-theme.test.ts +55 -0
- package/src/article-theme.ts +10 -2
- package/src/commands/article-theme.ts +1 -1
- package/src/commands/init.ts +2 -0
- package/src/commands/publish.ts +1 -1
- package/src/commands/typeset-assets-sync.test.ts +12 -1
- package/src/commands/wechat-export.ts +9 -4
- package/src/plugin-runner.ts +10 -4
- package/src/plugin-runtime.test.ts +19 -0
- package/src/plugin-runtime.ts +11 -2
- package/src/providers/index.ts +4 -2
- package/src/runtime-package.ts +13 -6
- package/src/wechat-preview/assets/browser-dist/.vite/manifest.json +1280 -5
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/src/wechat-preview/assets/browser-dist/assets/apl-B4CMkyY2.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/asciiarmor-Df11BRmG.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/asn1-EdZsLKOL.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/asterisk-B-8jnY81.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/brainfuck-C4LP7Hcl.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/clike-B9uivgTg.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/clojure-BMjYHr_A.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/cmake-BQqOBYOt.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/cobol-CWcv1MsR.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/coffeescript-S37ZYGWr.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/commonlisp-DBKNyK5s.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/crystal-SjHAIU92.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/css-BnMrqG3P.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/cypher-C_CwsFkJ.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/d-pRatUO7H.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/diff-DbItnlRl.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/dockerfile-BKs6k2Af.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/dtd-DF_7sFjM.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/dylan-DwRh75JA.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/ebnf-CDyGwa7X.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/ecl-Cabwm37j.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/eiffel-CnydiIhH.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/elm-vLlmbW-K.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/erlang-BNw1qcRV.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/factor-kuTfRLto.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/fcl-Kvtd6kyn.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/forth-Ffai-XNe.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/fortran-DYz_wnZ1.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/gas-Bneqetm1.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/gherkin-heZmZLOM.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/groovy-D9Dt4D0W.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/haskell-Cw1EW3IL.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/haxe-H-WmDvRZ.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/http-DBlCnlav.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/idl-BEugSyMb.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-B13lCSuc.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-BDdU9pNh.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-BFLGVgsp.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-BHJY0B4E.js +8 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-BOnKEJxh.js +7 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-BPmBcGI-.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-BTVIa90Y.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-BYV6TA8f.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-BlVpb0dF.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-BqKARoyW.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-BzHql5Rp.js +20 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-CPFEJ9fX.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-CdxKyTX6.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-CyXDvcwY.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-DA7Z4paG.js +13 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-DDQGWU_D.js +3 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-DENMoE5l.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-D_EI5vFS.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-DpuVrwze.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-IBlhKHQr.js +8 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-ifI5svkD.js +2 -0
- package/src/wechat-preview/assets/browser-dist/assets/index-vRiJxvj2.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/javascript-iXu5QeM3.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/julia-DuME0IfC.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/livescript-BwQOo05w.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/lua-VAEuO923.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/mathematica-DTrFuWx2.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/mbox-CNhZ1qSd.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/mirc-CjQqDB4T.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/mllike-CXdrOF99.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/modelica-Dc1JOy9r.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/mscgen-BA5vi2Kp.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/mumps-BT43cFF4.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/nginx-DdIZxoE0.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/nsis-LY0VT_u5.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/ntriples-BfvgReVJ.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/octave-Ck1zUtKM.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/oz-BzwKVEFT.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/pascal--L3eBynH.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/perl-CdXCOZ3F.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/pig-CevX1Tat.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/powershell-CFHJl5sT.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/properties-C78fOPTZ.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/protobuf-ChK-085T.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/pug-DeIclll2.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/puppet-DMA9R1ak.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/python-BuPzkPfP.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/q-pXgVlZs6.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/r-B6wPVr8A.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/rpm-CTu-6PCP.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/ruby-BK5aLJev.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/sas-B4kiWyti.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/scheme-C41bIUwD.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/shell-CjFT_Tl9.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/sieve-C3Gn_uJK.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/simple-mode-GW_nhZxv.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/smalltalk-CnHTOXQT.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/solr-DehyRSwq.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/sparql-DkYu6x3z.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/spreadsheet-BCZA_wO0.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/sql-D0XecflT.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/stex-C3f8Ysf7.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/style-Cif_gYUW.css +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/stylus-B533Al4x.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/swift-BzpIVaGY.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/tcl-DVfN8rqt.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/textile-CnDTJFAw.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/tiddlywiki-DO-Gjzrf.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/tiki-DGYXhP31.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/toml-Bm5Em-hy.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/troff-wAsdV37c.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/ttcn-CfJYG6tj.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/ttcn-cfg-B9xdYoR4.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/turtle-B1tBg_DP.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/vb-CmGdzxic.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/vbscript-BuJXcnF6.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/velocity-D8B20fx6.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/verilog-C6RDOZhf.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/vhdl-lSbBsy5d.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/webidl-ZXfAyPTL.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/{wechat-renderer-GgNrlSLi.js → wechat-renderer-CcHuq7M-.js} +83 -61
- package/src/wechat-preview/assets/browser-dist/assets/xquery-DzFWVndE.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/yacas-BJ4BC0dw.js +1 -0
- package/src/wechat-preview/assets/browser-dist/assets/z80-Hz9HOZM7.js +1 -0
- package/src/wechat-preview/assets/browser-dist/editor-export.js +1 -1
- package/src/wechat-preview/assets/browser-dist/src/wechat-preview/browser/studio/studio.html +215 -165
- package/src/wechat-preview/assets/browser-dist/studio.js +1148 -237
- package/src/wechat-preview/browser/studio/color-palettes.ts +28 -0
- package/src/wechat-preview/browser/studio/config-manager.ts +49 -0
- package/src/wechat-preview/browser/studio/milkdown-article-menu.ts +251 -0
- package/src/wechat-preview/browser/studio/myno-editor.ts +122 -0
- package/src/wechat-preview/browser/studio/studio.css +493 -250
- package/src/wechat-preview/browser/studio/studio.html +213 -163
- package/src/wechat-preview/browser/studio/studio.ts +684 -211
- package/src/wechat-preview/browser/studio/style-packs.ts +201 -0
- package/src/wechat-preview/browser/studio/syntax-presets.test.ts +10 -32
- package/src/wechat-preview/browser/studio/syntax-presets.ts +19 -357
- package/src/wechat-preview/fixtures/all-nodes.md +2 -11
- package/src/wechat-preview/index.ts +35 -14
- package/src/wechat-preview/server/http.ts +64 -8
- package/src/wechat-preview/server/studio.test.ts +60 -1
- package/src/wechat-preview/wechat-export.integration.test.ts +49 -8
- package/src/wechat-preview/wechat-renderer.ts +134 -11
- package/src/workflow.test.ts +2 -0
- package/src/wechat-preview/assets/browser-dist/assets/style-C9-YtTIm.css +0 -1
package/ARTICLE-THEMES.md
CHANGED
|
@@ -24,6 +24,8 @@ zzp publish --state ./workflow-state.json --article-theme ./my-template
|
|
|
24
24
|
|
|
25
25
|
账号默认使用 `wx.accounts.<account>.articleTheme`,相对路径以配置文件所在目录为基准。显式 `--article-theme` 优先,路径相对当前目录;handoff 支持同义的可选 `article_theme` 字段。没有选择正文模板时保持既有配置。封面仍使用 `--cover-theme`。
|
|
26
26
|
|
|
27
|
+
WeChat Visual Studio 的「导出配置」下载独立 `wechat-style.json`,同样通过 `--article-theme` 选择。文件含内嵌 `css`、`editorVars`、`exportTheme`、`syntaxPresets` 和 `structure`;页首图片以内嵌 Data URL 保存。不含账号凭据,不需要附带目录包文件。独立 JSON 使用自身样式,不叠加账号 `theme/customCss`,显式 `wechat-export --custom-css` 仍可覆盖。可用 `article-theme --path` 校验或安装;默认配置引用后由现有 `typeset-assets-sync` 收集部署,详见 [Studio 配置指南](docs/visual-studio-and-config-sync.md)。
|
|
28
|
+
|
|
27
29
|
模板样式叠加在现有基础样式上,既有账号 `theme` 覆盖和 `customCss` 最后生效。新版正文模板默认不附带内置账号署名,用户自己配置的 footerText 仍保留。旧 Markdown 插件接收可选 `articleThemePath`,可以继续忽略该字段;要支持付费模板预览,应使用内置渲染器或实现该可选参数。桌面端预览同样调用当前已配置的渲染插件。
|
|
28
30
|
|
|
29
31
|
发布前冻结模板版本、CSS 和资源到工作区 `.zzhub-media/article-themes`;渲染结果记录 `article_theme_hash`。同一篇文章更改模板、版本或声明素材后会重新导出。旧状态不含该字段时仍可使用。
|
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Pipeline 需要 [Bun](https://bun.sh/) 运行。npm 可以安装包,但命令
|
|
|
56
56
|
npm install -g @akairos/pipeline
|
|
57
57
|
|
|
58
58
|
# 或从源码运行
|
|
59
|
-
git clone https://github.com/
|
|
59
|
+
git clone https://github.com/k7airos/zzhub-pipeline.git
|
|
60
60
|
cd zzhub-pipeline
|
|
61
61
|
bun install
|
|
62
62
|
bun run src/cli.ts --help
|
|
@@ -160,7 +160,7 @@ Pipeline 在创建和更新草稿时,会自动读取封面图真实尺寸,
|
|
|
160
160
|
| **`@akairos/milkdown-article-plugins`** | Remark / Milkdown 全语法自定义解析与渲染插件 |
|
|
161
161
|
| **`@akairos/typeset-studio`** | 微信大图头图、多比例排版与卡片视觉合成引擎 |
|
|
162
162
|
|
|
163
|
-
配置在 `plugins.imageRenderer` / `plugins.markdownRenderer` 的外部插件在独立 Bun
|
|
163
|
+
配置在 `plugins.imageRenderer` / `plugins.markdownRenderer` 的外部插件在独立 Bun 子进程中加载,插件的 `render`、`doctor`、`browser` 和 `studio` 方法也在子进程调用。加载、资源入口查询与 doctor 默认上限为 10 秒,`render` 为 120 秒;Markdown 渲染显式传入 `timeoutMs` 时,子进程上限额外预留 10 秒。可通过 `ZZHUB_PIPELINE_PLUGIN_TIMEOUT_MS` 覆盖子进程调用上限。同步死循环、超时和异常退出会终止插件进程,非法返回值会给出 `PLUGIN_*` 错误,命令不会自动重试或静默切换模板。浏览器型插件返回入口后,由 pipeline 现有 Chrome 运行时加载页面,并按渲染 `timeoutMs` 管理浏览器生命周期。
|
|
164
164
|
|
|
165
165
|
图片渲染失败后任务进入失败状态,保留正文与审核结果。修复插件后执行 `zzp reset --state <state_path> --mode render`,再继续任务;也可以将对应的 `plugins.*Renderer` 配置设为 `null` 使用内置渲染器。外部插件通过 `onProgress` 回传进度,不应依赖修改输入对象或跨调用保存内存状态。子进程继承当前环境和文件权限,这一机制负责故障隔离,不是权限沙箱。
|
|
166
166
|
|
|
@@ -170,6 +170,27 @@ Pipeline 在创建和更新草稿时,会自动读取封面图真实尺寸,
|
|
|
170
170
|
|
|
171
171
|
旧的字符串入口继续有效,等同于没有配置专属参数;`null` 使用内置渲染器。`zzp config --import` 中的渲染器条目整体替换原条目,只改一个参数时使用 `--key plugins.imageRenderer.options.<参数名>`。
|
|
172
172
|
|
|
173
|
+
### myno 排版插件
|
|
174
|
+
|
|
175
|
+
myno 通过可选的 Markdown 渲染插件接入。从 pipeline 0.19.0 起可选依赖固定为 `@akairos/myno@0.3.0`,仍需在配置中显式启用;使用 `--omit optional` 安装时需自行提供插件。npm 配置示例:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"plugins": {
|
|
180
|
+
"markdownRenderer": {
|
|
181
|
+
"module": "@akairos/myno/adapter",
|
|
182
|
+
"options": {}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
本地开发时可先在 myno 仓库执行 `bun run build`,将 `module` 指向构建后的 `dist/adapter.js` 绝对路径。运行时部署包只接受 npm 入口和空 `options`,不会复制本机插件路径。
|
|
189
|
+
|
|
190
|
+
插件可以实现原有的 `render(input, options)`,也可以像 myno 一样只实现 `browser(options)`,返回浏览器排版入口的绝对路径 `{ scriptPath }`。`wechat-export` 和公众号文章发布通过同一个插件加载器调度;浏览器型插件复用 pipeline 现有的 Chrome、超时清理、离线模板校验和文件输出,仅页面排版实现来自插件。myno 不增加浏览器自动化依赖,调样式和导出 config 也不需要启动无头浏览器。独立样式仍与发布账号分离。
|
|
191
|
+
|
|
192
|
+
Markdown 插件可选提供 `studio(options)`,返回绝对资源目录 `root` 和该目录内的 HTML 相对路径 `entry`。`zzp wechat-preview studio` 将插件页面挂到 `/studio`,脚本和样式挂到 `/studio-assets/`;`/api/studio/config`、`/api/studio/save-config` 和历史记录继续由 pipeline 提供。插件不支持 Studio 时返回明确提示;配置为 `null` 保留内置工作台。切换插件后需重启已运行的预览服务。
|
|
193
|
+
|
|
173
194
|
### Typeset Studio 插件配置
|
|
174
195
|
|
|
175
196
|
Pipeline 已通过 npm 依赖 `@akairos/typeset-studio`。在本地配置文件启用适配器,并指定与 Studio 编辑器共用的数据目录:
|
|
@@ -212,6 +233,8 @@ zzp typeset-assets-sync --output ./pipeline-runtime --account-map '{"default":"W
|
|
|
212
233
|
|
|
213
234
|
配置包只包含插件入口、Studio 容器路径和账号主题/CSS/文章模板路径,不包含微信凭据、COS PAT 或 Worker token。账号的 `customCss`、`articleTheme` 为 null 时会清除服务器旧引用。服务器用 `nezus-infra` 的 `sync pipeline-runtime` 命令校验文件清单、合并排版白名单并备份切换;镜像与 npm 依赖继续固定版本部署。
|
|
214
235
|
|
|
236
|
+
WeChat Visual Studio 可通过「导出配置」下载不绑定账号的独立排版 JSON。CLI 用 `--article-theme ./wechat-style.json` 选择;在默认配置的账号 `articleTheme` 中引用后,也会由上述运行时包收集部署。文件内嵌 CSS 与页首图片,无需附带模板目录。详见 [Studio 独立配置与部署](docs/visual-studio-and-config-sync.md)。
|
|
237
|
+
|
|
215
238
|
### 推进一个发布任务
|
|
216
239
|
|
|
217
240
|
Pipeline 每次只推进一个动作。创建任务后,反复执行 `status`,按照返回的 `next_action` 继续;不要直接修改 `workflow-state.json`。
|
|
@@ -245,7 +268,7 @@ zzp publish --state <state_path>
|
|
|
245
268
|
|
|
246
269
|
实际顺序以 `status --view agent` 为准。需要发布贴图时,把 `--content-form` 改为 `newspic`;多页图片可以在渲染前附加 `newspic` 规格。
|
|
247
270
|
|
|
248
|
-
如果正文由 Agent
|
|
271
|
+
如果正文由 Agent 生成或审核,可以使用配套的 [kairos-skills](https://github.com/k7airos/kairos-skills) 仓库中的 [`zzhub-publish` Skill](https://github.com/k7airos/kairos-skills/tree/main/skills/zzhub-publish/SKILL.md)。Skill 负责编排 Agent 状态机,文章正文仍由调用方提供。
|
|
249
272
|
|
|
250
273
|
## 云端 Worker
|
|
251
274
|
|
|
@@ -375,7 +398,7 @@ zzp monitor stop
|
|
|
375
398
|
- [云端 Worker API、部署与更新](services/cloud-worker/README.md)
|
|
376
399
|
- [封面主题和图片排版](docs/render-themes.md)
|
|
377
400
|
- [离线正文模板](ARTICLE-THEMES.md)
|
|
378
|
-
- [Agent
|
|
401
|
+
- [Kairos Agent Skills 仓库](https://github.com/k7airos/kairos-skills):配套的 Agent 技能集合(包含 `zzhub-publish` 状态机编排、`milkdown-writing` 写作排版规范、`auto-ai-daily` 自动化发文)
|
|
379
402
|
- [第三方声明](THIRD-PARTY-NOTICES.md)
|
|
380
403
|
|
|
381
404
|
## 开发与验证
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akairos/pipeline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -27,20 +27,22 @@
|
|
|
27
27
|
"release:major": "changelogen --release --major --push"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@akairos/milkdown-article-plugins": "^0.1.
|
|
31
|
-
"@akairos/milkdown-article-style": "^0.1.
|
|
32
|
-
"@akairos/typeset-studio": "0.2.
|
|
30
|
+
"@akairos/milkdown-article-plugins": "^0.1.6",
|
|
31
|
+
"@akairos/milkdown-article-style": "^0.1.2",
|
|
32
|
+
"@akairos/typeset-studio": "0.2.6",
|
|
33
33
|
"@chenglou/pretext": "0.0.8",
|
|
34
34
|
"image-size": "^2.0.2",
|
|
35
35
|
"pngjs": "^7.0.0",
|
|
36
36
|
"zod": "^4.4.2"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
+
"@akairos/myno": "0.3.0",
|
|
39
40
|
"@napi-rs/canvas": "^0.1.97",
|
|
40
41
|
"cos-nodejs-sdk-v5": "^2.15.4"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"@milkdown/
|
|
44
|
+
"@milkdown/crepe": "^7.21.2",
|
|
45
|
+
"@milkdown/kit": "^7.21.2",
|
|
44
46
|
"@types/bun": "latest",
|
|
45
47
|
"@types/pngjs": "^6.0.5",
|
|
46
48
|
"changelogen": "^0.6.2",
|
package/src/adapter-loader.ts
CHANGED
|
@@ -15,7 +15,10 @@ import type {
|
|
|
15
15
|
} from "./adapter-types";
|
|
16
16
|
import { builtinImageRenderer } from "./adapters/builtin-image-renderer";
|
|
17
17
|
import { builtinMarkdownRenderer } from "./adapters/builtin-markdown-renderer";
|
|
18
|
-
import { PluginDoctorSchema, PluginImageOutputSchema, PluginMarkdownOutputSchema, PluginMetadataSchema, runExternalPlugin } from "./plugin-runtime";
|
|
18
|
+
import { PluginBrowserSchema, PluginDoctorSchema, PluginImageOutputSchema, PluginMarkdownOutputSchema, PluginMetadataSchema, PluginStudioSchema, runExternalPlugin } from "./plugin-runtime";
|
|
19
|
+
import { exportMarkdownToWechatHtml } from "./wechat-preview";
|
|
20
|
+
import { notifyProgress } from "./monitor/recorder";
|
|
21
|
+
import { findChrome } from "./imgx/runtime";
|
|
19
22
|
|
|
20
23
|
// ── Public API ───────────────────────────────────────────────────
|
|
21
24
|
|
|
@@ -75,9 +78,26 @@ export async function resolveMarkdownRenderer(
|
|
|
75
78
|
return {
|
|
76
79
|
name: metadata.name,
|
|
77
80
|
version: metadata.version,
|
|
78
|
-
...(metadata.hasDoctor ? { doctor: () =>
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
...(metadata.hasDoctor || metadata.hasBrowser ? { doctor: async () => {
|
|
82
|
+
const checks = metadata.hasDoctor ? await runExternalPlugin(pluginPath, "doctor", PluginDoctorSchema, undefined, { options }) : [];
|
|
83
|
+
if (metadata.hasBrowser) {
|
|
84
|
+
const chrome = findChrome();
|
|
85
|
+
checks.push({ name: "chrome", ok: Boolean(chrome), message: chrome ?? "Chrome/Chromium not found" });
|
|
86
|
+
}
|
|
87
|
+
return checks;
|
|
88
|
+
} } : {}),
|
|
89
|
+
...(metadata.hasStudio ? { studio: () => runExternalPlugin(pluginPath, "studio", PluginStudioSchema, undefined, { options }) } : {}),
|
|
90
|
+
render: async ({ onProgress, ...input }) => {
|
|
91
|
+
if (metadata.hasBrowser) {
|
|
92
|
+
const browser = await runExternalPlugin(pluginPath, "browser", PluginBrowserSchema, undefined, { options });
|
|
93
|
+
notifyProgress(onProgress, { stage: "render.html", message: "正在导出微信 HTML" });
|
|
94
|
+
const result = await exportMarkdownToWechatHtml({ ...input, browserScriptPath: browser.scriptPath });
|
|
95
|
+
notifyProgress(onProgress, { stage: "render.html", message: "微信 HTML 导出完成", current: 1, total: 1, unit: "files" });
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
return runExternalPlugin(pluginPath, "render", PluginMarkdownOutputSchema, input, { options }, onProgress,
|
|
99
|
+
input.timeoutMs && Number.isFinite(input.timeoutMs) && input.timeoutMs > 0 ? input.timeoutMs + 10_000 : 120_000);
|
|
100
|
+
},
|
|
81
101
|
};
|
|
82
102
|
}
|
|
83
103
|
|
package/src/adapter-types.ts
CHANGED
|
@@ -207,7 +207,21 @@ export interface MarkdownRenderPlugin {
|
|
|
207
207
|
/** Environment checks */
|
|
208
208
|
doctor?: (options?: Record<string, unknown>) => Promise<PipelinePluginDoctorCheck[]>;
|
|
209
209
|
|
|
210
|
+
/** 工作台静态资源由插件分发,配置读写仍由宿主提供。 */
|
|
211
|
+
studio?: (options?: Record<string, unknown>) => Promise<MarkdownStudioAssets>;
|
|
212
|
+
|
|
210
213
|
/** Render markdown to WeChat-compatible HTML */
|
|
211
214
|
/** 专属配置作为第二个参数传入,旧插件可忽略。 */
|
|
212
215
|
render: (input: MarkdownRenderInput, options?: Record<string, unknown>) => Promise<MarkdownRenderOutput>;
|
|
213
216
|
}
|
|
217
|
+
|
|
218
|
+
/** root 是插件资源目录,entry 是该目录内的 HTML 相对路径。 */
|
|
219
|
+
export interface MarkdownStudioAssets {
|
|
220
|
+
root: string;
|
|
221
|
+
entry: string;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** 仅提供浏览器排版资源的插件,Chrome 生命周期由 pipeline 管理。 */
|
|
225
|
+
export interface BrowserMarkdownRenderPlugin extends Omit<MarkdownRenderPlugin, "render"> {
|
|
226
|
+
browser: (options?: Record<string, unknown>) => Promise<{ scriptPath: string }>;
|
|
227
|
+
}
|
|
@@ -4,6 +4,9 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { readArticleTheme, snapshotArticleTheme } from "./article-theme";
|
|
6
6
|
import { filterIdempotent } from "./providers/publish-core";
|
|
7
|
+
import { resolveWechatArticleTheme } from "./wechat-preview";
|
|
8
|
+
import { prepareMarkdownRuntime, RUNTIME_ROOT } from "./runtime-package";
|
|
9
|
+
import { PipelineConfigSchema } from "./schema/config";
|
|
7
10
|
|
|
8
11
|
const roots: string[] = [];
|
|
9
12
|
afterEach(async () => { for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true }); });
|
|
@@ -53,6 +56,58 @@ test("越界、符号链接、网络样式与不兼容版本不能导入", async
|
|
|
53
56
|
}
|
|
54
57
|
});
|
|
55
58
|
|
|
59
|
+
test("Studio 单文件配置可跨账号复用、冻结及沿用运行时配置包部署", async () => {
|
|
60
|
+
const root = await mkdtemp(join(tmpdir(), "zzp-studio-config-"));
|
|
61
|
+
roots.push(root);
|
|
62
|
+
const path = join(root, "my-style.json");
|
|
63
|
+
const profile = {
|
|
64
|
+
schemaVersion: 1, engineVersion: 1, id: "wechat-style", name: "独立排版", version: "1.0.0",
|
|
65
|
+
editorVars: { "--brand": "#123456" },
|
|
66
|
+
exportTheme: { bodyFontSize: "18px", footerText: "", primaryColor: "#123456" },
|
|
67
|
+
syntaxPresets: { h2: "contrast-green" },
|
|
68
|
+
structure: { numberedHeadings: true, headingLabel: "章节", calloutPresets: { tip: { title: "建议" } }, headerImageUrl: "data:image/png;base64,aW1hZ2U=" },
|
|
69
|
+
css: "p { color: #123456; }",
|
|
70
|
+
};
|
|
71
|
+
await writeFile(path, JSON.stringify(profile));
|
|
72
|
+
const theme = await readArticleTheme(path);
|
|
73
|
+
expect(theme.files.size).toBe(1);
|
|
74
|
+
expect(theme.css).toBe(profile.css);
|
|
75
|
+
const accountOverrides = { editorVars: { "--brand": "#ff0000" }, exportTheme: { bodyFontSize: "14px" }, syntaxPresets: { h2: "pillar" } };
|
|
76
|
+
const first = resolveWechatArticleTheme("default", accountOverrides, theme);
|
|
77
|
+
const other = resolveWechatArticleTheme("ancientone", accountOverrides, theme);
|
|
78
|
+
expect(first).toEqual(other);
|
|
79
|
+
expect(first.theme.exportTheme.bodyFontSize).toBe("18px");
|
|
80
|
+
expect(first.syntaxPresets).toEqual(profile.syntaxPresets);
|
|
81
|
+
expect(first.structure?.headerImageUrl).toBe(profile.structure.headerImageUrl);
|
|
82
|
+
expect(first.structure?.calloutPresets).toEqual(profile.structure.calloutPresets);
|
|
83
|
+
const frozen = await snapshotArticleTheme(theme, join(root, "frozen"));
|
|
84
|
+
expect((await readArticleTheme(frozen.path)).hash).toBe(theme.hash);
|
|
85
|
+
|
|
86
|
+
const config = PipelineConfigSchema.parse({ wx: { accounts: { default: { articleTheme: path, customCss: join(root, "missing.css"), pat: "secret" } } } });
|
|
87
|
+
const deployed = await prepareMarkdownRuntime(config);
|
|
88
|
+
const manifest = "markdown/article-themes/default/manifest.json";
|
|
89
|
+
expect(deployed.accounts.default).toMatchObject({ articleTheme: `${RUNTIME_ROOT}/${manifest}`, customCss: null });
|
|
90
|
+
expect(JSON.stringify(deployed.accounts)).not.toContain("secret");
|
|
91
|
+
expect(JSON.parse(new TextDecoder().decode(deployed.files.get(manifest))).css).toBe(profile.css);
|
|
92
|
+
await writeFile(path, JSON.stringify({ ...profile, css: "p { color: blue; }" }));
|
|
93
|
+
expect((await readArticleTheme(path)).hash).not.toBe(theme.hash);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("独立配置拒绝凭据、外部素材与不安全 CSS", async () => {
|
|
97
|
+
const root = await mkdtemp(join(tmpdir(), "zzp-studio-invalid-"));
|
|
98
|
+
roots.push(root);
|
|
99
|
+
const path = join(root, "style.json");
|
|
100
|
+
const profile = { schemaVersion: 1, engineVersion: 1, id: "style", name: "Style", version: "1.0.0", css: "" };
|
|
101
|
+
for (const patch of [
|
|
102
|
+
{ account: "default" }, { pat: "secret" }, { assets: ["asset.png"] },
|
|
103
|
+
{ css: "p { background: url(file:///secret); }" },
|
|
104
|
+
{ structure: { headerImageUrl: "file:///secret" } },
|
|
105
|
+
]) {
|
|
106
|
+
await writeFile(path, JSON.stringify({ ...profile, ...patch }));
|
|
107
|
+
await expect(readArticleTheme(path)).rejects.toThrow();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
56
111
|
test("结果未知阻止普通重发,明确重置才建立新发布尝试", async () => {
|
|
57
112
|
const { root } = await fixture();
|
|
58
113
|
const { defaultState, readState, writeState } = await import("./state");
|
package/src/article-theme.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { WechatStructureOverridesSchema } from "./schema/config";
|
|
5
6
|
|
|
6
7
|
/** 模板只包含声明、CSS 与素材,不执行客户包中的脚本。 */
|
|
7
8
|
export const ArticleThemeSchema = z.object({
|
|
@@ -18,11 +19,16 @@ export const ArticleThemeSchema = z.object({
|
|
|
18
19
|
assets: z.array(z.string()).max(100).default([]),
|
|
19
20
|
editorVars: z.record(z.string(), z.string()).default({}),
|
|
20
21
|
exportTheme: z.record(z.string(), z.string()).default({}),
|
|
22
|
+
/** Studio 独立配置内嵌 CSS,无需附带模板目录。 */
|
|
23
|
+
css: z.string().max(256_000).optional(),
|
|
24
|
+
syntaxPresets: z.record(z.string(), z.string()).optional(),
|
|
21
25
|
structure: z.object({
|
|
22
26
|
numberedHeadings: z.boolean().default(false),
|
|
23
27
|
headingLabel: z.string().max(30).default(""),
|
|
24
28
|
quoteLabel: z.string().max(30).default(""),
|
|
25
29
|
headerImage: z.string().nullable().default(null),
|
|
30
|
+
headerImageUrl: z.string().regex(/^data:image\/(?:png|jpeg|gif|webp);base64,[A-Za-z0-9+/]+={0,2}$/).nullable().optional(),
|
|
31
|
+
calloutPresets: WechatStructureOverridesSchema.shape.calloutPresets,
|
|
26
32
|
}).default({ numberedHeadings: false, headingLabel: "", quoteLabel: "", headerImage: null }),
|
|
27
33
|
}).strict();
|
|
28
34
|
|
|
@@ -64,10 +70,12 @@ export async function readArticleTheme(inputPath: string): Promise<ArticleThemeP
|
|
|
64
70
|
}
|
|
65
71
|
const manifestName = basename(manifestPath);
|
|
66
72
|
const manifest = ArticleThemeSchema.parse(JSON.parse(new TextDecoder().decode(await read(manifestName))));
|
|
67
|
-
const
|
|
73
|
+
const standalone = manifest.css !== undefined;
|
|
74
|
+
if (standalone && (manifest.assets.length || manifest.structure.headerImage)) throw new Error("独立排版配置必须内嵌素材,不能引用模板目录文件");
|
|
75
|
+
const paths = standalone ? [] : [manifest.stylesheet, manifest.sample, manifest.preview, manifest.license, ...manifest.assets];
|
|
68
76
|
if (manifest.structure.headerImage) paths.push(manifest.structure.headerImage);
|
|
69
77
|
for (const name of new Set(paths)) await read(name);
|
|
70
|
-
const css = new TextDecoder().decode(files.get(manifest.stylesheet));
|
|
78
|
+
const css = manifest.css ?? new TextDecoder().decode(files.get(manifest.stylesheet));
|
|
71
79
|
// 模板无需 CSS 转义;拒绝用转义或注释隐藏的网络/文件资源。
|
|
72
80
|
const unsafeCss = (value: string) => /\\|@import\b|url\s*\(|expression\s*\(/i.test(value.replace(/\/\*[\s\S]*?\*\//g, ""));
|
|
73
81
|
if (Object.keys(manifest.editorVars).some(key => !/^--[a-zA-Z0-9_-]+$/.test(key))) throw new Error("模板变量名无效");
|
|
@@ -6,7 +6,7 @@ import { printResult } from "../output";
|
|
|
6
6
|
export async function articleTheme(args: string[]): Promise<void> {
|
|
7
7
|
const parsed = parseArgs(args);
|
|
8
8
|
if (parsed.help) {
|
|
9
|
-
console.log("Usage: zzp article-theme --path
|
|
9
|
+
console.log("Usage: zzp article-theme --path <Studio 独立 JSON、模板目录或 manifest.json> [--install <本地模板库>]\n只校验或复制模板,不联网、不发布。");
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
let theme = await readArticleTheme(requireArg(parsed, "path", "template path"));
|
package/src/commands/init.ts
CHANGED
|
@@ -64,6 +64,7 @@ Options:
|
|
|
64
64
|
--newspic-render-spec-file JSON file for newspic pagination / page-image intent (optional)
|
|
65
65
|
--style-hint e.g. fact_report (optional)
|
|
66
66
|
--cover-theme Cover theme ID (optional)
|
|
67
|
+
--article-theme Standalone Studio JSON or local article theme package (optional)
|
|
67
68
|
--requires-research Flag
|
|
68
69
|
--requires-style Flag
|
|
69
70
|
--requires-render Flag
|
|
@@ -140,6 +141,7 @@ Options:
|
|
|
140
141
|
explicit_constraints: [],
|
|
141
142
|
style_hint: styleHint,
|
|
142
143
|
cover_theme: coverTheme,
|
|
144
|
+
article_theme: optionalArg(parsed, "article-theme") ? resolve(optionalArg(parsed, "article-theme")!) : null,
|
|
143
145
|
newspic_render: newspicRender,
|
|
144
146
|
...(newspicFile ? { newspic_file: resolve(newspicFile) } : {}),
|
|
145
147
|
requires: {
|
package/src/commands/publish.ts
CHANGED
|
@@ -47,7 +47,7 @@ Usage: zzhub-pipeline publish [options]
|
|
|
47
47
|
Options:
|
|
48
48
|
--state Path to state JSON (required)
|
|
49
49
|
--route Only publish this route (optional; default: all targets)
|
|
50
|
-
--article-theme
|
|
50
|
+
--article-theme Standalone Studio JSON or local article theme package (optional)
|
|
51
51
|
--dry-run Print commands without executing (optional)
|
|
52
52
|
`.trim());
|
|
53
53
|
return;
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { PNG } from "pngjs";
|
|
6
6
|
import { createTypesetAssetsSyncCommand } from "./typeset-assets-sync";
|
|
7
7
|
import type { uploadFileToCos } from "../providers/cos";
|
|
8
|
+
import { readArticleTheme } from "../article-theme";
|
|
8
9
|
|
|
9
10
|
let root: string;
|
|
10
11
|
let previousConfig: string | undefined;
|
|
@@ -60,6 +61,13 @@ test("dry-run reads only referenced images and never uploads, verifies URLs, or
|
|
|
60
61
|
|
|
61
62
|
test("deduplicates shared images, rewrites only sources, and exports styles without credentials", async () => {
|
|
62
63
|
await writeFile(join(root, "templates/shared.json"), JSON.stringify({ ...template, id: "shared" }));
|
|
64
|
+
const style = { schemaVersion: 1, engineVersion: 1, id: "style", name: "Style", version: "1.0.0", css: "p { color: #123456; }", exportTheme: { bodyFontSize: "18px" } };
|
|
65
|
+
await writeFile(join(root, "style.json"), JSON.stringify(style));
|
|
66
|
+
const configPath = join(root, "config.json");
|
|
67
|
+
const config = await result(configPath);
|
|
68
|
+
config.wx.accounts.default.articleTheme = "style.json";
|
|
69
|
+
config.plugins.markdownRenderer = { module: "@akairos/myno/adapter", options: {} };
|
|
70
|
+
await writeFile(configPath, JSON.stringify(config));
|
|
63
71
|
let uploads = 0;
|
|
64
72
|
const command = createTypesetAssetsSyncCommand({ upload: uploader(async () => { uploads++; }), verifyUrl });
|
|
65
73
|
const destination = join(root, "runtime");
|
|
@@ -67,9 +75,12 @@ test("deduplicates shared images, rewrites only sources, and exports styles with
|
|
|
67
75
|
expect(uploads).toBe(1);
|
|
68
76
|
expect((await result(join(root, "templates/jinx.json"))).images[0]).toEqual({ ...template.images[0], src: "https://cdn.example/jinx.png" });
|
|
69
77
|
const patch = await result(join(destination, "pipeline.patch.json"));
|
|
70
|
-
expect(patch.
|
|
78
|
+
expect(patch.plugins.markdownRenderer).toEqual({ module: "@akairos/myno/adapter", options: {} });
|
|
79
|
+
expect(patch.wx.accounts.default).toMatchObject({ customCss: null, articleTheme: "/data/pipeline-runtime/markdown/article-themes/default/manifest.json" });
|
|
80
|
+
expect((await readArticleTheme(join(destination, "markdown/article-themes/default/manifest.json"))).css).toBe(style.css);
|
|
71
81
|
for (const secret of ["secret-cos-token", "private-secret", "private-app-id", "private-pat"]) expect(JSON.stringify(patch)).not.toContain(secret);
|
|
72
82
|
expect((await result(join(destination, "manifest.json"))).typesetTemplates).toBe(2);
|
|
83
|
+
expect(await readFile(join(destination, "typeset-studio/templates/.canvas-presets-v1"), "utf8")).toBe("1");
|
|
73
84
|
expect((await readFile(join(root, "uploads/jinx.png"))).equals(image)).toBe(true);
|
|
74
85
|
await command(["--output", destination]);
|
|
75
86
|
expect(uploads).toBe(1);
|
|
@@ -8,6 +8,7 @@ import { loadConfig, resolveConfigRelativePath } from "../config";
|
|
|
8
8
|
import { resolveMarkdownRenderer } from "../adapter-loader";
|
|
9
9
|
import { WechatExportError } from "../wechat-preview";
|
|
10
10
|
import { registerPreviewEntry } from "../wechat-preview/server";
|
|
11
|
+
import { readArticleTheme } from "../article-theme";
|
|
11
12
|
|
|
12
13
|
const CSS_DEMO = `
|
|
13
14
|
Custom CSS is cascaded after the built-in theme and converted to conservative
|
|
@@ -69,7 +70,7 @@ Options:
|
|
|
69
70
|
--account Account/theme key (optional; default: default)
|
|
70
71
|
--title Page title for the render shell (optional)
|
|
71
72
|
--preview-shell-out Path to exact final-HTML preview (optional)
|
|
72
|
-
--article-theme
|
|
73
|
+
--article-theme Standalone Studio JSON or local article theme package (optional)
|
|
73
74
|
--summary-only Omit HTML from stdout; still write the complete output file
|
|
74
75
|
--custom-css CSS override path; replaces account customCss (optional)
|
|
75
76
|
--timeout-ms Real browser wait limit in ms (default: 15000)
|
|
@@ -108,9 +109,13 @@ ${CSS_DEMO}
|
|
|
108
109
|
|
|
109
110
|
const config = loadConfig();
|
|
110
111
|
const wxAccount = config.wx.accounts[account] ?? config.wx.accounts[config.wx.defaultAccount];
|
|
112
|
+
const articleThemeArg = optionalArg(parsed, "article-theme");
|
|
113
|
+
const articleThemePath = articleThemeArg ? resolve(articleThemeArg) : resolveConfigRelativePath(wxAccount?.articleTheme);
|
|
114
|
+
// 独立配置替代账号样式;显式 CSS 参数仍可覆盖本次导出。
|
|
115
|
+
const standalone = articleThemePath ? (await readArticleTheme(articleThemePath)).manifest.css !== undefined : false;
|
|
111
116
|
const customCss = resolveWechatExportCustomCss(
|
|
112
117
|
cliCustomCss,
|
|
113
|
-
wxAccount?.customCss,
|
|
118
|
+
standalone ? null : wxAccount?.customCss,
|
|
114
119
|
);
|
|
115
120
|
const markdownRenderer = await resolveMarkdownRenderer(config);
|
|
116
121
|
|
|
@@ -125,8 +130,8 @@ ${CSS_DEMO}
|
|
|
125
130
|
title,
|
|
126
131
|
previewShellOutPath,
|
|
127
132
|
customCss,
|
|
128
|
-
articleThemePath
|
|
129
|
-
themeOverrides: wxAccount?.theme,
|
|
133
|
+
articleThemePath,
|
|
134
|
+
themeOverrides: standalone ? undefined : wxAccount?.theme,
|
|
130
135
|
timeoutMs: timeoutMs !== undefined && Number.isFinite(timeoutMs) ? timeoutMs : undefined,
|
|
131
136
|
debugDir: debugDir ? resolve(process.cwd(), debugDir) : undefined,
|
|
132
137
|
});
|
package/src/plugin-runner.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { MonitorProgress } from "./monitor/types";
|
|
2
2
|
|
|
3
3
|
/** 子进程只执行一次插件调用,进度与结果通过 IPC 返回。 */
|
|
4
|
-
process.on("message", async (request: { specifier: string; action: "describe" | "doctor" | "render"; input?: Record<string, unknown>; options?: Record<string, unknown> }) => {
|
|
4
|
+
process.on("message", async (request: { specifier: string; action: "describe" | "doctor" | "render" | "studio" | "browser"; input?: Record<string, unknown>; options?: Record<string, unknown> }) => {
|
|
5
5
|
try {
|
|
6
6
|
const module = await import(request.specifier);
|
|
7
7
|
const plugin = module.default ?? module.plugin ?? module;
|
|
8
8
|
if (!plugin || typeof plugin !== "object" || typeof plugin.name !== "string" || !plugin.name.trim() ||
|
|
9
|
-
typeof plugin.render !== "function"
|
|
10
|
-
|
|
9
|
+
(typeof plugin.render !== "function" && typeof plugin.browser !== "function") ||
|
|
10
|
+
(plugin.browser !== undefined && typeof plugin.browser !== "function") || (plugin.doctor !== undefined && typeof plugin.doctor !== "function") ||
|
|
11
|
+
(plugin.studio !== undefined && typeof plugin.studio !== "function")) {
|
|
12
|
+
throw new Error("插件必须导出 name、render 或 browser,以及可选的 doctor 函数和 studio 函数");
|
|
11
13
|
}
|
|
12
14
|
// 故障插件频繁报告进度时,限制 IPC 消息数量。
|
|
13
15
|
let lastProgressAt = 0;
|
|
@@ -18,9 +20,13 @@ process.on("message", async (request: { specifier: string; action: "describe" |
|
|
|
18
20
|
process.send?.({ type: "progress", value: progress });
|
|
19
21
|
};
|
|
20
22
|
const value = request.action === "describe"
|
|
21
|
-
? { name: plugin.name, version: plugin.version, hasDoctor: typeof plugin.doctor === "function" }
|
|
23
|
+
? { name: plugin.name, version: plugin.version, hasDoctor: typeof plugin.doctor === "function", hasStudio: typeof plugin.studio === "function", hasBrowser: typeof plugin.browser === "function" }
|
|
22
24
|
: request.action === "doctor"
|
|
23
25
|
? await plugin.doctor(request.options)
|
|
26
|
+
: request.action === "studio"
|
|
27
|
+
? await plugin.studio(request.options)
|
|
28
|
+
: request.action === "browser"
|
|
29
|
+
? await plugin.browser(request.options)
|
|
24
30
|
: await plugin.render({ ...request.input, onProgress }, request.options);
|
|
25
31
|
process.send?.({ type: "result", value });
|
|
26
32
|
} catch (error) {
|
|
@@ -115,6 +115,25 @@ test("markdown plugins receive their own options in doctor and render", async ()
|
|
|
115
115
|
expect((await plugin.render({ markdownPath: join(directory, "post.md"), outPath: join(directory, "post.html"), account: "default" })).html).toBe(options.html);
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
test("browser-only markdown plugins validate their entry and receive their own options", async () => {
|
|
119
|
+
const pluginPath = join(directory, "browser-plugin.mjs");
|
|
120
|
+
await writeFile(pluginPath, `export default {name:'browser-test',browser(options){return {scriptPath:options.scriptPath}}}`);
|
|
121
|
+
const plugin = await resolveMarkdownRenderer(PipelineConfigSchema.parse({ plugins: { markdownRenderer: { module: pluginPath, options: { scriptPath: "relative.js" } } } }));
|
|
122
|
+
await expect(plugin.render({ markdownPath: "unused.md", outPath: "out.html", account: "default" })).rejects.toMatchObject({ code: "PLUGIN_INVALID_RESULT" });
|
|
123
|
+
expect((await plugin.doctor!()).some(check => check.name === "chrome")).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("Studio capability is optional, isolated and validates asset directory paths", async () => {
|
|
127
|
+
const pluginPath = await fixture("return null", `async studio(options){return {root:${JSON.stringify(directory)},entry:options.entry}},`);
|
|
128
|
+
const config = (entry: string) => PipelineConfigSchema.parse({ plugins: { markdownRenderer: { module: pluginPath, options: { entry } } } });
|
|
129
|
+
const plugin = await resolveMarkdownRenderer(config("studio.html"));
|
|
130
|
+
expect(await plugin.studio!()).toEqual({ root: directory, entry: "studio.html" });
|
|
131
|
+
const invalid = await resolveMarkdownRenderer(config("../outside.html"));
|
|
132
|
+
await expect(invalid.studio!()).rejects.toMatchObject({ code: "PLUGIN_INVALID_RESULT" });
|
|
133
|
+
const plain = await resolveMarkdownRenderer(PipelineConfigSchema.parse({ plugins: { markdownRenderer: await fixture("return null") } }));
|
|
134
|
+
expect(plain.studio).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
118
137
|
test("reports thrown errors, process exits, and invalid image results without escaping the parent", async () => {
|
|
119
138
|
for (const [body, code] of [
|
|
120
139
|
['throw new Error("bad template")', "PLUGIN_EXECUTION_FAILED"],
|
package/src/plugin-runtime.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { resolveBunBinary } from "./spawn";
|
|
|
10
10
|
/** 插件错误保留阶段和错误码,终端与调用方使用同一提示。 */
|
|
11
11
|
export class PluginExecutionError extends Error {
|
|
12
12
|
constructor(readonly code: string, specifier: string, action: string, detail: string) {
|
|
13
|
-
const stage = action === "describe" ? "加载" : action === "doctor" ? "检查" : "渲染";
|
|
13
|
+
const stage = action === "describe" || action === "browser" ? "加载" : action === "doctor" ? "检查" : action === "studio" ? "工作台加载" : "渲染";
|
|
14
14
|
super(`[${code}] 插件「${specifier}」${stage}失败:${detail}。请检查插件配置、运行 zzp doctor 或切换内置渲染器,修复后再重试;本次未自动重试。`);
|
|
15
15
|
this.name = "PluginExecutionError";
|
|
16
16
|
}
|
|
@@ -20,6 +20,15 @@ export const PluginMetadataSchema = z.object({
|
|
|
20
20
|
name: z.string().trim().min(1),
|
|
21
21
|
version: z.string().optional(),
|
|
22
22
|
hasDoctor: z.boolean(),
|
|
23
|
+
hasStudio: z.boolean().default(false),
|
|
24
|
+
hasBrowser: z.boolean().default(false),
|
|
25
|
+
});
|
|
26
|
+
export const PluginBrowserSchema = z.object({
|
|
27
|
+
scriptPath: z.string().refine(isAbsolute, "浏览器入口必须是绝对路径"),
|
|
28
|
+
});
|
|
29
|
+
export const PluginStudioSchema = z.object({
|
|
30
|
+
root: z.string().refine(isAbsolute, "Studio root 必须是绝对路径"),
|
|
31
|
+
entry: z.string().min(1).refine(value => !isAbsolute(value) && !value.split(/[\\/]/).includes(".."), "Studio entry 必须位于资源目录内"),
|
|
23
32
|
});
|
|
24
33
|
export const PluginDoctorSchema = z.array(z.object({
|
|
25
34
|
name: z.string().min(1), ok: z.boolean(), message: z.string().optional(), detail: z.unknown().optional(),
|
|
@@ -48,7 +57,7 @@ const ProgressSchema = z.object({
|
|
|
48
57
|
/** 外部插件独立执行;父进程可终止同步死循环、异常退出和永不结束的 Promise。 */
|
|
49
58
|
export async function runExternalPlugin<T>(
|
|
50
59
|
specifier: string,
|
|
51
|
-
action: "describe" | "doctor" | "render",
|
|
60
|
+
action: "describe" | "doctor" | "render" | "studio" | "browser",
|
|
52
61
|
schema: z.ZodType<T>,
|
|
53
62
|
input?: Record<string, unknown>,
|
|
54
63
|
configuration: { options?: Record<string, unknown>; environment?: Record<string, string> } = {},
|
package/src/providers/index.ts
CHANGED
|
@@ -84,15 +84,17 @@ async function publishWechatArticleRoute({
|
|
|
84
84
|
try {
|
|
85
85
|
const markdownRenderer = await resolveMarkdownRenderer(config);
|
|
86
86
|
const wxAccount = config.wx.accounts[account] ?? config.wx.accounts[config.wx.defaultAccount];
|
|
87
|
+
// 独立正文配置与发布账号的凭据、默认 CSS 分离。
|
|
88
|
+
const standalone = articleTheme?.manifest.css !== undefined;
|
|
87
89
|
const rendered = await markdownRenderer.render({
|
|
88
90
|
onProgress: reportProgress,
|
|
89
91
|
markdownPath: exportPostPath,
|
|
90
92
|
outPath: workspacePaths.zotepadExportHtml,
|
|
91
93
|
account,
|
|
92
94
|
title: state.metadata.title,
|
|
93
|
-
customCss: resolveConfigRelativePath(wxAccount?.customCss),
|
|
95
|
+
customCss: standalone ? null : resolveConfigRelativePath(wxAccount?.customCss),
|
|
94
96
|
articleThemePath: articleTheme?.path,
|
|
95
|
-
themeOverrides: wxAccount?.theme,
|
|
97
|
+
themeOverrides: standalone ? undefined : wxAccount?.theme,
|
|
96
98
|
});
|
|
97
99
|
const { html } = rendered;
|
|
98
100
|
|
package/src/runtime-package.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function validateStudioDefaults(text: string, templateIds: Set<string>):
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** 准备账号主题文件,只导出样式白名单,不复制凭据或本机插件入口。 */
|
|
64
|
-
export async function prepareMarkdownRuntime(config: PipelineConfig, accountMap: Record<string, string> = {}): Promise<{ files: Map<string, Uint8Array>; accounts: Record<string, unknown
|
|
64
|
+
export async function prepareMarkdownRuntime(config: PipelineConfig, accountMap: Record<string, string> = {}): Promise<{ files: Map<string, Uint8Array>; accounts: Record<string, unknown>; markdownRenderer: PipelineConfig["plugins"]["markdownRenderer"] }> {
|
|
65
65
|
const files = new Map<string, Uint8Array>();
|
|
66
66
|
const accounts: Record<string, unknown> = {};
|
|
67
67
|
for (const [localAccount, value] of Object.entries(config.wx.accounts)) {
|
|
@@ -69,13 +69,14 @@ export async function prepareMarkdownRuntime(config: PipelineConfig, accountMap:
|
|
|
69
69
|
if (!/^[a-zA-Z0-9_.-]{1,80}$/.test(account) || [".", "..", "__proto__", "constructor", "prototype"].includes(account)) throw new Error(`无效的样式账号标识:${account}`);
|
|
70
70
|
if (Object.hasOwn(accounts, account)) throw new Error(`账号映射重复:${account}`);
|
|
71
71
|
const next: Record<string, unknown> = { theme: value.theme, customCss: null, articleTheme: null };
|
|
72
|
-
|
|
72
|
+
const theme = value.articleTheme ? await readArticleTheme(resolveConfigRelativePath(value.articleTheme)!) : null;
|
|
73
|
+
// 独立配置已内嵌 CSS,部署时不依赖被替代的账号样式文件。
|
|
74
|
+
if (value.customCss && theme?.manifest.css === undefined) {
|
|
73
75
|
const name = `markdown/custom-css-${account}.css`;
|
|
74
76
|
files.set(name, await readRuntimeSource(resolveConfigRelativePath(value.customCss)!));
|
|
75
77
|
next.customCss = `${RUNTIME_ROOT}/${name}`;
|
|
76
78
|
}
|
|
77
|
-
if (
|
|
78
|
-
const theme = await readArticleTheme(resolveConfigRelativePath(value.articleTheme)!);
|
|
79
|
+
if (theme) {
|
|
79
80
|
const prefix = `markdown/article-themes/${account}`;
|
|
80
81
|
for (const [name, bytes] of theme.files) files.set(`${prefix}/${name}`, bytes);
|
|
81
82
|
files.set(`${prefix}/manifest.json`, Buffer.from(JSON.stringify(theme.manifest, null, 2)));
|
|
@@ -83,7 +84,12 @@ export async function prepareMarkdownRuntime(config: PipelineConfig, accountMap:
|
|
|
83
84
|
}
|
|
84
85
|
accounts[account] = next;
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
+
const entry = config.plugins.markdownRenderer;
|
|
88
|
+
const module = typeof entry === "string" ? entry : entry?.module;
|
|
89
|
+
// 部署只接受可复现的 npm 入口,禁止把本机绝对路径带到服务器。
|
|
90
|
+
if (module && module !== "@akairos/myno/adapter") throw new Error("运行时同步仅支持 @akairos/myno/adapter 或内置 Markdown 渲染器");
|
|
91
|
+
if (typeof entry === "object" && entry && Object.keys(entry.options).length) throw new Error("myno 运行时同步暂不接受专属参数");
|
|
92
|
+
return { files, accounts, markdownRenderer: module ? { module, options: {} } : null };
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
/** 写入带文件哈希的独立配置包,调用者在全部成功后切换目录。 */
|
|
@@ -100,8 +106,9 @@ export async function writeRuntimePackage(destination: string, templates: Map<st
|
|
|
100
106
|
files.set("typeset-studio/default-template.json", Buffer.from(defaults));
|
|
101
107
|
// 防止渲染时重新播种内置模板,使部署目录与文件清单保持一致。
|
|
102
108
|
files.set("typeset-studio/templates/.initialized", Buffer.from("1"));
|
|
109
|
+
files.set("typeset-studio/templates/.canvas-presets-v1", Buffer.from("1"));
|
|
103
110
|
const patch = {
|
|
104
|
-
plugins: { imageRenderer: { module: "@akairos/typeset-studio/adapter", options: { dataDir: `${RUNTIME_ROOT}/typeset-studio` } }, markdownRenderer:
|
|
111
|
+
plugins: { imageRenderer: { module: "@akairos/typeset-studio/adapter", options: { dataDir: `${RUNTIME_ROOT}/typeset-studio` } }, markdownRenderer: markdown.markdownRenderer },
|
|
105
112
|
wx: { accounts: markdown.accounts },
|
|
106
113
|
};
|
|
107
114
|
files.set("pipeline.patch.json", Buffer.from(JSON.stringify(patch, null, 2) + "\n"));
|