npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dsh-plugin-gzh-studio

v0.2.0

Published

新媒体内容工作台:21 条内容类型的合规检查、逐张出图与版本留存、手机宽度预览、推公众号草稿、视频成片预览;抖音 / 小红书 / 快手 的发布包在规划中。

Readme


id: GZS-PLUGIN-README title: dsh-plugin-gzh-studio — 新媒体工作台 updated: 2026-11-04 status: active

新媒体工作台 · dsh-plugin-gzh-studio

挂在 DeepSeek Harness(DSH)主区面板上的新媒体内容工作台:中间是原生对话,右边是实时看板。 从选题、分内容类型写稿、逐段出图,到推平台草稿、导出 HTML、看视频成片——一条线走完,每一步都留痕。

  • 21 条内容类型(赛道):医疗、母婴、金融、教育、法律、美妆医美、心理、保健品、政策、品牌推广、 科学、历史、AI、财经、职场、美食、家居、运动、宠物、旅行、情感。每条带禁用词、必须声明、 结构骨架、标题公式与出图风格;不选也能用(通用模式)。
  • 实时:对话里改稿、出图,面板自己刷新(SSE),不用点刷新。
  • 推草稿:封面走永久素材、正文图走 uploadimg,直接进公众号草稿箱;推的是草稿,不是发表。
  • 视频期:成片能在面板里播(Range 流式)、字幕能叠、发布包能复制。
  • 跟随宿主外观:面板采样自己背后的背景色,按亮度选深 / 浅两套调色板;纸色在宿主背景上压一层很轻的倾向色(彩色主题自动减半),卡片再亮一档。宿主切主题或换背景时自动重采,不用重启。
  • 一稿多投:顶栏「发布包」按公众号 / 小红书 / 抖音 / 快手各裁一版;平台标题、正文、话题可以改,只存和原稿不同的部分,原稿仍是一份。

平台支持(写清「现在」和「还没」)

| 平台 | 现在能做到 | 还没做 | |---|---|---| | 公众号 | 推草稿(直接进草稿箱)、导出 HTML / Markdown / 图片清单 | 群发 / 发表(人工点) | | 视频号 | 成片预览、字幕叠加、发布文案复制 | 自动上传(无开放接口) | | 抖音 / 小红书 / 快手 | 发布包:同一篇稿按各平台字数 / 封面比例裁一遍、逐条列规格问题、一键复制(顶栏「发布包」) | 一键发布(小红书无公开发布 API;抖音 / 快手需企业开发者资质与权限申请) |

所以「新媒体」指的是内容形态与投放目标,不是「已经能一键发所有平台」。当前唯一能真推的是公众号草稿。

形态与「版本管理 + 可观测」的内核搬自 Deckline(dsh-deck-studio,MIT)—— 它把 image deck 的这套做进了工作台;图文这条线是同一个病,单位从「一页一张图」换成「一篇:封面 + 每段配图」。

它是什么,不是什么

  • 是:多赛道写稿与合规检查、逐张出图与版本管理、改稿提案(先看对照再确认)、手机宽度预览、 推公众号草稿、导出 HTML / Markdown / 图片清单、视频期成片预览与发布包。
  • 不是:群发 / 发表器(推的是草稿,发表仍要人工在后台点),也不是视频号上传器 (视频号没有面向普通账号的开放上传接口)。
  • 不是:素材库同步器。走导出 HTML 手工粘时,正文图仍需你在后台替换占位符;走「推草稿」不用。

三个视图

打开面板先进首页,不是直接被塞进某一篇的段落列表。

| 视图 | 内容 | 什么时候用 | |---|---|---| | 首页 | 全部文章的卡片网格:封面缩略图 / 标题 / 副标题 / 段数 / 已出图数 / 模板 / 相对时间。最后一张是「+ 新建一篇」。右栏是最近动作(见下)。 | 打开面板、想换一篇、想知道哪篇卡住了 | | 单篇 | 三栏编辑(见下节),顶栏有「← 全部」和「治理」。 | 写稿、改段、出图、切版本 | | 治理 | 一屏三块:检查结果(按必改 / 提醒 / 提示分组,点一条跳到那段)、提案(含逐段对照与确认/驳回)、账本(最近 40 条动作)。 | 交付前过一遍、看有哪些提案等着确认 |

治理视图只做集中呈现,不另算一套口径——数据全部来自 /article 已有的 qa / proposals / ledger,与右栏检查器显示的内容完全一致。

首页卡片不跑 QA:那会让首屏随文章数量变慢。卡片只读 /state 里已有的字段(段数、已出图数、封面版本、更新时间),要看具体哪条问题去治理视图。

最近动作(首页右栏)

回答的是「我刚才干了什么」——跨文章的账本尾巴,点一条直接进那篇。

  • 数据由 /state 的 activity 一次给全(服务端 store.recentActivity,每篇取最后 60 条原始账本、合并后按时间倒序、最多 14 条)。 不让面板按文章数发 N 个请求,文章一多就现原形。
  • 原始窗口必须远大于展示条数。取 12 条时踩过:一次验收跑连点导出,整栏被压成一行「导出 html ×13」, 出图、切版这些真事全被挤出窗口。readLedger 本来就读完整个文件再切片,放宽到 60 不增加 I/O。
  • 连续的同种动作折叠成一条并记次数(导出 html ×11)。不折的话,连点几次导出就把这栏刷成十几行一样的记录, 真正有价值的「出图」「改稿」被挤到看不见。只在相邻发生时合并——中间夹了别的事就是两段不同的操作。 判定签名含 type/slot/vid/format/pid/fields/verbatim:改的是不同字段就是两件事,不能算重复。
  • 文案复用 ledgerText,但走 brief 口径:只留「谁出的 · 多大」,把尺寸吸附说明和 prompt 摘要留给治理视图的账本 ——那两样在 300px 宽的动态栏里只会被截断。
  • 空库、或一条动作都没有时,这一栏整个不渲染,首页回到单栏。

库存:有多少篇、各在哪、搜得到吗

打开工作台先回答的是「我手上有多少活」,不是「这一篇卡在哪」。所以首页顶部是一排可点的数字:

| 数字 | 含义 | | ---- | ---- | | 全部 / 草稿 / 草稿箱 / 已发布 | 生命周期三档,点一下就筛到那一档 | | 空壳 | 建了但正文还是空的——最该被提醒的那批 | | 未绑期号 | 没绑期号,流水线状态判不出来(进度只能靠猜) |

生命周期与流水线状态是两个维度,不能混:流水线答「卡在第几步」(要台账、要审批、要哈希, 只有绑了期号才算得出);生命周期答「这篇在作者手里到哪了」(所有文章都有)。一篇可以流水线全通 但还没发布,也可以已发布但后来改了字要重发。

  • published 只能由人标,机器无从知晓(没接公众号 API)。所以每个值都带 source (manual / inferred / default),卡片上人标的标一个「·手」字——否则用户会以为插件真的知道发布状态。
  • 不猜 published:有推草稿回执只推到 draftbox。没有证据的「已发布」会让待发的那几篇消失。
  • dry-run 回执不算推过(persisted=false 只是模拟),拿它当证据会让「草稿箱」混进没推的篇。
  • 空串能取消手标。只能设不能撤的开关,用户第一次标错就会讨厌它。

没绑期号也有进度:按内容完整度推五档(有标题 → 有正文 → 有封面 → 配图齐 → 导出过), 并明确标注「粗略估计」。期号与根目录收进「接上产线」的进阶区——小白写一篇文章不该先被要求填绝对路径。

标签与月份是目录检索的两个入口:标签自由填(去重、大小写不敏感、单个 ≤20 字、最多 12 个), 点一下即筛;月份按更新时间归档。两者都进检索串,也都能单独作为筛选条件。

搜索走 /state?state=&q=&tag=&month=,服务端过滤:检索文本(标题 + 小标题 + 正文拼串)可能几十 KB, 不下发;而且前端过滤会让「总数」和「列表」对不上。计数始终基于全量——改了筛选条件, 顶上总数不该跟着跳。

状态词笔误不能把库筛成空:传进来 ?state=draf 若直接比对会筛出 0 篇,用户看到的是 「文章全没了」而不是「条件写错了」。非法值当作不筛,并回 filters.invalid=true。

可读性基线:不是「风格」,是底线

六套模板之前各自定字号行高(正文 15–17px、行高 1.8–2.1),等于让用户挑一个「读起来最不吃力的」 ——那是设计缺陷,不是选项。现在把可读性抽成全局基线 READABILITY,模板只能在其上微调:

| 项 | 值 | 依据 | | -- | -- | ---- | | 正文字号 | 15px(模板可 ±1px) | 135 编辑器与腾讯 wiki 两份独立指南的区间中位 | | 行高 | 1.75(1.65–1.9) | 同上,两份都指向 1.75 为「万能值」 | | 段间距 | 20px(16–26) | 同上 | | 左右留白 | 10px(8–16) | 同上;之前导出是 padding:2px 0,手机上正文直接贴边 | | 正文色 | #3f3f3f 深灰 | 两份都明确说不要用纯黑 |

templateOf() 是唯一出口,出口处统一 clampTokens()——手机预览与导出 HTML 都从这里取 token, 所以一处 clamp 就全面生效,不靠每个模板手写合规值(手写的迟早会漂)。

单篇视图:三个栏位

| 栏位 | 内容 | |---|---| | 左:结构 / 对话 | 上部是文章结构;开启对话后,左侧嵌入 DSH 原生对话,负责自然语言讨论、理解需求和生成修改提案。工作台不另造第二个聊天框。 | | 中:手机预览 | 375px 微信正文预览,套当前模板的 token。改一个字当场看到手机上什么样,不用导出、不用开微信。点段落也能选中它。 | | 右:检查器 / 看板 | 只剩三栏:证据(QA / 流水线 / 十道门 / 账本)、图(槽位与版本)、元信息。讨论、改稿和提案确认都在左侧对话与顶栏「检查」抽屉里,右侧不承担交流。 |

视觉:与 Deckline 同一套语言

界面不是「能用就行」的表格堆叠。口径和 Deckline 对齐,为的是两件事:同一个工作区里不像两个产品、 读长文本不累。

| 项 | 取值 | |---|---| | 底色 | 暖纸 #f4f1e9 → 卡片 #fffdf8 → 中栏画布 #f1ece1→#e9e2d4(带极淡横纹) | | 墨色 | 主字 #0c4651(墨绿)/ 正文 #3c4c50 / 次 #728086 / 弱 #8a9599 | | 强调 | 青 #1496a8(选中、聚焦)/ 深青 #0f6975(主按钮)/ 金 #c5882f(版本号、当前版标记) | | 边框 | 一律暖灰 #d6d0c4,不用冷蓝灰——冷灰配暖底会显脏 | | 字体 | 正文 Inter / 系统无衬线 12.5px,行高 1.65;标题用 Noto Serif SC(本机已装,Deckline 同款) | | 顶栏 | 墨绿实底 + 米白字,一眼区分「这是应用,不是弹窗」 | | 选中态 | 薄荷底 #e2f1ef + 左侧 3px 青条(Deckline 的 .deck-mini.active 同做法) | | 形状 | 圆角 6–8px,阴影只用暖调 #b9ae9b,不用黑阴影 |

三条具体做法,都是为了「看起来是编辑工具而不是后台表单」:

  1. 中间那栏当画布:手机预览 375px 是真机宽度,不改它;窗口不够宽时用 zoom 等比缩小整块, 宁可图小也不横向裁掉右半边(裁掉的话滚动条在几千像素高的底部,够不着)。
  2. 层级靠字号 + 衬线 + 眉标,不靠加边框:结构 / 检查器 是 10px 大写字距的青眉标,版本(5) 同款, 标题用衬线,正文 12.5px。原来全是 11.5px 灰字,一片平。
  3. 空态与加载态都给人话:加载是骨架条(不是「读取中…」),空列表是虚线卡 + 一句「下一步做什么」。

六个能力

  1. 逐张出图:封面(900×383,2.35:1)与每段配图(1024×576)分开出。改哪段出哪段,不重做整套。 节点可有多个;没指定节点时按「默认 → 其余可用」依次试,一个额度不够自动切下一个(跳过谁、为什么跳过,都进账本)。

  2. 改稿提案:Agent 改稿走 /propose,只落提案、不动正文;面板逐段给旧/新对照(删的行红底、加的行绿底), 人点「确认」才写入,点「驳回」只记一行账。提案记着它基于哪一版正文的内容哈希——中间被人改过就判过期, 拒绝确认,不会拿一份过时的对照去盖别人的改动。 提案分四类性质,决定该退到哪一步(判据是「改了什么」,不是「改了多少字」):

    | 性质 | 退到 | 依据 | | ---- | ---- | ---- | | 改词句 polish | 07-deai | 只改词句:不许动标题、摘要、配图,也不增删段落 | | 改结构 structure | 04-write | 动了段落增删或整段重写(小标题+正文同时改也算) | | 改标题 title | 03-angles | 标题在方案阶段就定了;或整篇只在第 11 步改一次并留痕 | | 改摘要 summary | 11-digest-draft | 摘要的正式修订点在推草稿前 |

    混着改时按退得最远的那一步定(摘要+结构 → 退 04,不是 11)。提出方可以声明性质, 但声明不能当免责声明用:声明「改词句」却动了标题/摘要/段落增删,服务端当场 400, 并把撞了哪条红线说出来。

  3. 记忆账本:ledger.jsonl append-only。第 1 段出过 v001、v002、v003,每版当时的 prompt、节点、模型、尺寸、大小都在账本里,改完 prompt 也能回查当时写的是什么。

  4. 导出:公众号 HTML(全内联样式、没有一个 class,后台不会剥掉)+ Markdown(带 front matter)+ 图片上传清单(哪张、对应哪个占位符 __IMG_COVER__ / __IMG_01__)。

  5. 手机预览:见上表中间那栏。

  6. 六套模板:商务简报 / 杂志编辑 / 日系清简 / 科技蓝图 / 手账纸质 / 夜间深色。换模板只改长相,不动文字。

  7. 流水线状态:这篇文章卡在哪一步。见下一节。

  8. 去 AI 味:两层判据,都落在可核对的东西上。

    • 句子层(30 条词表):命中哪句、命中什么词、属于什么类(编辑腔 / 行业黑话 / 论文连接词 / 模糊限定…)、 为什么像 AI、怎么改。按规则归并理由(不是按词——「此外」和「然而」属同一条规则, 按词拼会把同一条 why 拼两遍,读起来像复读机)。同一句出现在多段时合并成一行并标出在哪几段, 否则模板稿会把同一句列三遍,把结论挤出屏幕。
    • 节奏层(4 条判据):长句连排(连续 ≥3 句 ≥25 字)、段落长度太整齐(≥3 段、均长 ≥60 字、 极差 < 均长 35%)、全篇无短句(≥6 句且没有一句 ≤12 字)、每段都以中心句开头。 词表抓不到「读起来像一条直线」,只有这一层能抓。
    • 改得动:可机器改的(删过渡词、去「所谓的」)标「可采纳」、可逐句勾;句式类的标「手」, 不许可点——机器硬凑只会把 AI 腔换成另一种 AI 腔。采纳产出提案,不直接改稿,人确认才写入。
    • 干净也报分母:结论里写明「扫了 N 句」。只报「没问题」不给分母,用户分不清 「稿子干净」和「功能没跑」。
  9. 05/06 两份清单逐项确认:编辑审与读者审各 8 条验收项(文案逐字取自 SKILL.md 的「验收」小节, 不自造条目),面板上逐项勾,勾完这道门才算过。 三种依据必须能区分:disk(盘上两份清单文件都在)/ fallback(靠多轮审稿记录兜底)/ panel(靠面板逐项勾)。勾满但盘上没有文件时判 panel 并在 detail 里写明依据, 同时对账给出 warn:「勾选只是我看过了的记录,不能替代清单本身」。 勾选只写插件自己的 reviews.json,一个字节都不往真实仓里写——那个仓是内容生产的事实源, 插件往里塞面板状态会让「盘上有什么」说不清。

  10. 出图前确认(一篇文章只问一次):说明会消耗额度、这一版会带上几条反馈 (带上之后那几条就从「待带上」变「已带上」)。不用弹窗——弹窗会挡住 prompt, 而人确认时最想再看一眼的恰恰是 prompt。确认卡贴在按钮下方并自动滚进视野 (踩过:卡落在视口外,用户点完看不到任何变化,以为按钮坏了)。

流水线状态:卡在哪一步

回答的是公众号 13 步流水线里的位置,不是「编辑器里改到哪」。判据全部来自盘上的真实文件, 不是自报进度。

  • 状态词不出自造:只允许 PIPELINE.md §4 那九个(ready / needs-research / needs-browser-audit / needs-user-approval / needs-draft-approval / needs-review / dry-run …)。lib/pipeline.js 里有一张 全集表,测试会对每个返回值断言「必须在词表内」——防止哪天随手加一个 needs-something。
  • 十道门逐条给证据:人物清单 → frontmatter 四项 → 证据台账 → 方案已挑定 → 编辑审+读者审 → Chrome 终核 → 制作确认 → 排版已体检 → 排版校验留痕 → 推草稿回执。每道门给 ok / fail / stale / unknown / skip 之一,附一句能核对的具体内容(「绑的是 6f35a431,现稿 27bee65f」),不是只说「不通过」。
  • unknown 不等于做不到:判据不可用时标 unknown 而不是 fail。把「这台机器查不出」说成「没做到」, 会让人去补一份本来不该存在的文件(027 期的 chrome-fetch-027.json 只绑了《史记》页面,不是没核过)。
  • 哈希说了算:确认记录里绑的是 sha256,正文/排版/封面任何一个字节变了旧确认就作废。 时间戳挡不住「改完又改回来」。但首尾空白不算改稿——否则改个空格就判全篇作废,人不敢动键盘。
  • 过期确认可被重签覆盖:027 期的制作确认早过期了,后面的草稿授权重签过且哈希匹配,所以判 ok 并注明「后被 …-wechat-draft.json 重签覆盖」。直接判 stale 会让人白重核一遍。
  • 绑定是显式动作:article.pipeline = {episodeId, root, articleHash, boundAt},绑定那刻钉下正文哈希; 之后在面板改过字,状态里 drift=true 会明说「面板里改过正文,按规矩要重核、重新交稿」。
  • 绑定/解绑进账本但不进「最近动作」也不进账本视图:那是配置杂事,不是编辑动作。 留在 ledger.jsonl 里做审计,但混进视图会挤掉真正的内容工作(验收脚本每跑一次就多一对绑定记录)。 视图里会注明「略去 N 条配置」,不偷偷藏。
  • 状态词之外还有三样东西(借同事「编辑部观测台」的做法):
    • 节点条:11 步流水线一排 chips,✓立项 ✓台账 … –去AI味,一眼看出走到哪。 没有机器判据的步骤标「无判据」并列名——假装知道比不知道更糟,但也不能因此把整篇判成未知。
    • 时间线:从交付件 mtime 推出来。账本记「谁为什么改」,时间线记「什么时候动过」,不另建事件表。 必须给绝对时刻(10-22 17:23),给「4 天前」的话八个文件全一样,排不出先后。
    • 事实核对:台账 / 证据 / 核验 / 审稿 / 素材包 分开列。混在一张表里会让人误以为「审稿过了 = 事实对」。
  • 侧栏只放够判断的信息:状态词 + 卡在哪 + 下一步 + 一条 11 段进度概览。 完整节点条与事实核对在治理视图(748px 宽)。11 个 chips 塞进 352px 会被压成三行,密得看不清 ——信息量决定放哪,不是「相关的放一起」。

安装

dsh plugin --profile web add [email protected]
# 装完按提示重启 dsh web(profile 开了 patchReload: live 时,改 manifest 会热重载)

本机开发(把目录链进 profile):

New-Item -ItemType Junction -Path "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-plugin-gzh-studio" `
  -Target "<插件目录>"
# 然后 package.json 两处:dependencies 加 "dsh-plugin-gzh-studio": "link:<插件目录>",
# dsh.profile.bundles 加 dsh-plugin-gzh-studio

装好后:主区面板列表里出现「新媒体工作台」,点开就是工作台。数据全在本机。

首次打开要做的三件事

  1. 在左侧对话里说「写一篇关于 X 的稿子」;也可以点首页的「新建一篇」手动建(空库时它也是第一步);
  2. 出图节点:默认是「占位图」(不联网、出 SVG),足够把流程点一遍; 要出真图就在 settings.json 里加一个 OpenAI 兼容节点(见下);
  3. 公众号(可选):顶栏「推草稿」里填 AppID / AppSecret,第一次推大概率遇到 40164,把面板给出的 IP 加进白名单即可。

真源仓库(往期与视频期)默认不配:要用就往环境变量 GZH_STUDIO_REPO 指到 social-media-management 那一级,或在 settings.json 里写 pipelineRoot。

配置(可选)

loader 行 config 或 ~/.dsh/gzh-studio/settings.json:

| 键 | 默认 | 说明 | |---|---|---| | root | ~/.dsh/gzh-studio | 数据根目录 | | ledgerLimit | 200 | 面板一次回读的账本行数 | | nodes[] | 一个 fake 节点 | 出图节点:{id,name,baseURL,apiKeyEnv,model,size,fake} | | defaultNode | 第一个 | 默认节点 id | | defaultTemplate | business | 新建文章的默认模板 |

key 只存环境变量名(apiKeyEnv),插件从不把 key 写进任何文件、也不进日志与错误消息。

公众号推送配置

推草稿的凭据放在另一个文件(~/.dsh/gzh-studio/wechat.json),跟出图节点分开—— 那里面有 AppSecret,不该混进会被到处复制的那份设置里。面板上的「推草稿」对话框会写它。

| 键 | 说明 | |---|---| | appId / appSecret | 公众号后台「设置与开发 → 基本配置」。也可用环境变量 GZH_WECHAT_APPID / GZH_WECHAT_SECRET 覆盖(不想落盘时用) | | author | 草稿作者名(≤16 字,可空) | | mode | direct(默认,插件直连微信)/ gateway(预留,密钥与出口 IP 都留在网关) | | gateway.baseURL / gateway.token | 网关模式的地址与令牌 |

直连的硬门槛是出口 IP 白名单:第一次推大概率回 40164,面板会把微信报的那个 IP 抠出来给你一键复制。 token 走 stable_token 缓存 7200 秒(提前 5 分钟失效),只有 40001/42001/40014 才会强制刷新一次。

fake 节点(默认)不出网,出的是 SVG 占位图:尺寸、槽位名、prompt 摘要、版本号都画在上面。 装好就能把「出图 → 看版本 → 改 → 重出 → 导出」整条流程点一遍,不用先配 key。

真节点是 OpenAI 兼容接口:POST {baseURL}/v1/images/generations, 吃 b64_json 或 url 两种返回;尺寸以返回的图片本身为准(节点常忽略 size 参数),探不到才回填请求值并在版本上标 note。

⚠️ 尺寸必须是 64 的倍数。这是实测出来的坑:网关对非倍数尺寸不报错, 直接把请求改成一对方图(900x383 → 1254x1254、900x384 → 同样方图; 1792x768、1216x512、1344x576 则按请求返回)。所以 generate.js 在发请求前 会先做一次吸附(snapSize):保比例不保数字——封面目标 2.35:1 落到 1664x704 (2.36:1,漂移 0.6%),正文目标 16:9 的 1024x576 本身就是 64 的倍数,原样发出。 吸附过就在版本的 note 里写清楚改了哪一档,面板版本条下方能看到这句话。

赛道包:合规口径挂在内容方向上,不挂在模板上

模板回答「长什么样」,赛道回答「写给谁、按什么结构写、哪些话不能说、缺什么声明会出事」。 医疗和母婴可以共用一套排版,但「治愈率 98%」「零风险」在医疗稿里是红线、在职场上只是夸张。

内置 21 条

| 档 | 赛道 | |---|---| | A 强合规(写错会出事) | 医疗健康、母婴育儿、金融理财、教育升学、法律、美妆医美、心理、保健品营养、政策解读、品牌推广 | | B 事实核验(写错会失真) | 科学科普、历史人文、科技 / AI、财经商业 | | C 生活通用(结构与口吻) | 职场成长、美食菜谱、家居生活、运动健身、宠物养护、旅行、情感关系 |

21 条覆盖市面主流内容方向;覆盖不到的方向由用户自己加(见下)。

规则怎么判

| 判据 | 级别 | 为什么 | |---|---|---| | 命中禁用词 | warn | 字面匹配分不清「不根治」和「根治」,判 error 会逼人改一句本来没错的话 | | 缺「必须声明」 | error | 那是真的没写,不是猜的(医疗要「科普不构成诊疗建议」、金融要「不构成投资建议」) | | 有数字 / 年份但没写来源 | info | 提醒补出处,不拦发布 | | 科技 / AI 稿没写版本或日期 | warn | 能力、价格、配额都会过期 |

赛道条目并进普通 QA(同一套 {level, item, fix}),面板不为它另做一块 UI—— 两块并排时人只会看其中一块。

自定义赛道

往 <root>/tracks/*.json 丢一份包就能加,形状不对的包会在日志里报一句、然后被跳过 (不会把面板带崩):

{
  "id": "pet-care", "name": "宠物养护", "tier": "C",
  "audience": "养猫养狗的人", "tone": "具体、不吓人",
  "structure": ["场景", "怎么做", "何时就医"],
  "titleFormulas": ["…到底要不要紧"],
  "imageStyle": "自然光下的宠物抓拍,避免拟人化",
  "defaultTemplate": "paper",
  "compliance": {
    "forbidden": ["包治", "绝对安全"],
    "required": [{ "any": ["兽医", "就医"], "message": "宠物稿要提示就医", "fix": "补一句就医指征" }]
  }
}

撞了内置 id 的自定义包不覆盖内置:内置那套是兜底,被顶掉会让规则来源说不清。

数据落点

~/.dsh/gzh-studio/
  settings.json                        节点 / 默认模板
  articles/<gzh-YYYYMMDD-xxxx>/
    article.json                       当前状态:标题、段落、每个槽位的 current 指针
    ledger.jsonl                       记忆账本(append-only,只加不改)
    proposals/<p-xxxxxxxx>.json        改稿提案(含逐段对照与 baseHash)
    reviews.json                       05/06 清单逐项确认(16 项;**只在这里,不写真实仓**)
    images/cover/v001.svg              封面各版本(旧版永不覆盖)
    images/sec-s3/v002.png             第 3 段配图各版本

article.json 是当前状态,ledger.jsonl 是历史。状态可以覆盖,历史不行 —— 所以「哪一版说过什么、为什么改」只能从账本查。

提案放在文章目录里而不是全局目录:拷走一个目录就带走了正文、账本、图、提案,不会漏。

路由(全部在 Connection 鉴权栅栏之下)

| 路由 | 方法 | 说明 | |---|---|---| | /api/gzh-studio.status | GET | 就绪状态 | | /api/gzh-studio.state | GET | 文章列表 + 节点(含可用性)+ 六套模板 | | /api/gzh-studio.article?id= | GET | 单篇全文 + QA + 账本 + 图片清单 | | /api/gzh-studio.create | POST | 新建:{title,audience,takeaway,sections:number\|array,template} | | /api/gzh-studio.save | POST | 改稿:{id,patch}(白名单字段,未知字段忽略) | | /api/gzh-studio.script | POST | Agent 起草入口:整篇讲稿一次写入 | | /api/gzh-studio.generate | POST | 出图:{id,slot,prompt?,negative?,node?,size?} | | /api/gzh-studio.pick | POST | 切版本:{id,slot,vid} | | /api/gzh-studio.feedback | POST | 反馈原话进账本:{id,slot?,vid?,verbatim} | | /api/gzh-studio.export?id=&format=html\|md\|checklist | GET | 导出(纯文本响应) | | /api/gzh-studio.image?id=&slot=&v= | GET | 图片字节(no-cache + ETag) | | /api/gzh-studio.node | POST | 节点增改 / 切默认 | | /api/gzh-studio.brief?id= | GET | 纯文本 brief(讲给谁 / 带走什么 / 禁忌 / 分段现状),可直接贴进对话 | | /api/gzh-studio.propose | POST | Agent 改稿入口:{id,note,title?,summary?,sections?} → 只落提案,不动正文 | | /api/gzh-studio.proposal?id=&pid= | GET | 单份提案 + 逐段对照(只给变化行与上下文) | | /api/gzh-studio.decide | POST | 裁决:{id,pid,confirmed};确认走 writeScript,驳回只记账 | | /api/gzh-studio.deai?id= | GET | 去 AI 味诊断:句子层(词表)+ 节奏层(4 条判据) | | /api/gzh-studio.adoptDeai | POST | 采纳几句建议:{id,accepted:[{slot,start,end,after}]} → 产出提案,不改正文 | | /api/gzh-studio.review | POST | 清单勾一项:{id,kind,item,checked};勾满即视为这道门过 | | /api/gzh-studio.repo | GET | 真源仓库(只读):03-scripts 下稿件列表 {root,lines,articles}(只 stat,不读正文) | | /api/gzh-studio.repoArticle?dir= | GET | 单篇:intent/evidence/review/html + sections(H2/H3 切节;html > 200 KB 截断并标 htmlTruncated) | | /api/gzh-studio.video | GET | 视频号各期:{id,dir,status,files},status 形如 分段 · 1/8 | | /api/gzh-studio.videoEpisode?id= | GET | 单期:口播稿 / 分镜 / 分段 / 字幕 + title / outline / dirPath / media(成片、封面、分镜图、字幕文件);缺件回空数组 + note,不报错 | | /api/gzh-studio.media?id=&rel= | GET | 视频期里的媒体文件:支持 Range(<video> 才能拖进度条),路径三道闸校验,类型白名单(音频/视频/图片/字幕),未知扩展名 415 | | /api/gzh-studio.gate?dir= | GET | 质量门:真跑 gate status(20 秒超时、同目录 5 分钟缓存),中文键映射成 no/name/verdict/round/pass |

真源仓库(只读,与工作台自己的库是两份数据)

  • 根目录没有默认值:必须用环境变量 GZH_STUDIO_REPO,或用 settings.json 的 pipelineRoot 指定 (指向 social-media-management 那一级)。没配时 /repo 回 configured:false、列表为空,面板显示「未配置」而不是报错;
  • 这些路由一个字节都不写真源仓:扫描只看 readdir / stat,正文只在单篇请求时读;
  • dir / id 一律做包含校验:..、盘符、UNC、绝对路径、符号链接逃逸都回 400,找不到回 404;
  • gate 是真起子进程(node <workspace>/wechat-article-pipeline/gate/index.mjs status <稿件>), 结果按 dir + 稿件目录 mtime 缓存 5 分钟——首页不能每篇都跑一次。

五条口径

  1. 旧版永不覆盖。nextVid() 只往后加号;「现在用哪版」是 current 指针,切版本只改指针,不删文件。
  2. 账本 append-only。改稿、出图、切版本、反馈、导出、提案都进账本;面板上看到的就是宿主里存的那份,不做本地乐观更新。
  3. Agent 改稿先进提案。/propose 只写提案不写正文,人看过对照点确认才落盘; 提案记着 baseUpdatedAt,正文中间被改过就判过期并拒绝确认——不拿旧对照盖新正文。 确认那一步复用 writeScript,不另开一条写入路径,避免「确认过了但正文没按确认的内容写」。
  4. 图片两条路:走「推草稿」时正文图由插件走 uploadimg 换成 mmbiz URL、封面走永久素材, 不用手工;走导出 HTML 手工粘时,本地文件贴不进去,所以图位置写成 __IMG_01__ 这类占位符, 另给清单说明「哪张对应哪个占位符」,上传完在后台替换。
  5. 空段落在读者视角里不存在。没小标题、没正文、没配图的一段,手机预览里整段跳过—— 以前它会留一个「第 N 段还没有配图 · 点这里出图」的虚线框在正文最末尾,那是编辑器控件, 混进预览里像是文章没写完。同理,QA 的「N/M 段还没有配图」也不把空段算进分母: 空段已经单独报了一条 section-empty,再数一遍就是指向一个还不存在的段落。 段号仍按原始位置编号,跳过中间的空段不会让后面「第 3 段」变成「第 2 段」。

Agent 怎么接

不需要在插件里内置 LLM,三步走:

  1. 取约束:GET /api/gzh-studio.brief?id=<id> 拿到一段纯文本——讲给谁、想让人带走什么、 禁忌词、分段现状(段号 + 小标题 + 字数 + 有无图)。这段直接贴进对话,Agent 起草时就有边界。 否则 audience / takeaway / forbidden 只是躺在 article.json 里的三个字符串,没人看得见。
  2. 提提案:POST /api/gzh-studio.propose,body 与 writeScript 同形 ({id, note, title?, subtitle?, summary?, sections:[{id,heading,body}]})。 只写提案,正文不动。 返回 {pid, status:"waiting-user", summary}。
  3. 等人裁决:人在面板上点确认才会真写入;Agent 可以轮询 GET /api/gzh-studio.proposal?id=&pid= 看 proposal.status 与 blocked。

/save 与 /script 仍然保留,但它们是直接覆盖——人工在面板上改字走的就是这条路。 Agent 改稿请走 /propose,别再直接调那两个。

出图 prompt 的「聪明」也不在插件里:它靠人写、靠 Agent 写、靠 seeds/ 里的样张。这部分本来就是可以被公开改超越的(Deckline 的自述同理)。

验证(不需要浏览器、不需要网络)

npm run verify                 # 全套:11 套单测 + 打包边界 + 干净安装验收
npm run verify:fast            # 只跑不落盘的那三套(路由/类名/布局)
npm run pack:check             # 只验包:文件清单、体积、有没有作者私有路径

| 脚本 | 盯什么 | |---|---| | qa-unit | 纯函数:QA(含禁忌词)/ 模板 / 导出器 / 占位图 / 尺寸探测 / 文件清单 | | generate-unit | 出图:尺寸吸附到 64 的倍数、封面比例漂移 <2%(假 fetch,不联网) | | pipeline-unit | 流水线状态机:造假文件树逐个打破门禁,验状态词指得对 | | host-smoke | 假 ctx + 真落盘:建→改→出图→切版→读图→导出→提案闭环→节点 failover→越权探针 | | client-harness | 假 ModuleLoader/React:apply 注册面 + 三栏组件真渲染 + 原生会话绑定/重开/递话 | | stream-unit | SSE:hello 帧 / POST 写事件(带 id)/ 盘上外部改动 / 断开清理 | | push-unit | 推草稿:本地假微信服务器跑全链路(token 缓存 / 封面走素材 / 正文图走 uploadimg / 占位符全替换 / 40164 带 IP / SVG 拦下) | | video-media-unit | 视频:成片/封面/分镜/字幕识别、Range 206、越界与未知类型拒 | | tracks-unit | 赛道:12 条内置、坏包被拒、命中只 warn / 缺声明判 error、自定义包生效、出图风格进 prompt | | layout-split | 两档中栏下限 + 左右栏夹取 + 分隔条指针/键盘/复位 | | class-check | 组件里用到的 gzs-* 类名是否都有 CSS 定义 | | pack-check | 包边界:22 个文件 / 解开 <1.5 MB / 不含 verify 截图与作者私有路径 | | install-check | 从真打出来的 tarball 解包、加载宿主半、挂路由、真调三条读接口 + 建一篇 |

真界面验收(需要 3080 在跑,用无头 Chrome + CDP 真点真截图):

node verify/live-ui.mjs <token>   # 78 项:首屏三栏/首页/单篇/治理/新建/导出 + 原生对话独立会话/空白态/收起重开 + 窄窗口与几何
node verify/live-entry.mjs <token>  # 10 项:入口可达性——新媒体工作台是侧栏常驻行、一步点开、「更多」里不重复出现
node verify/live-interact.mjs <token>  # 22 项:真鼠标拖分隔条(含极限与夹取)→ 刷新记不记得住 → 双击复位 → 视频号点开单期
node verify/live-pipeline.mjs <token> [期号] [根目录]  # 29 项:绑定→徽标可见性→侧栏概览→观测台→解绑还原
node verify/probe-app.mjs <token> # 只读探针:把首屏 DOM 里能点的东西列出来,排查「入口点不到」时用
node verify/probe-node.mjs <token> # 只读探针:出图节点到底能不能用(缺 key 会静默降级成占位图,先问再按)

本机 3080 只认 dsh-auth-* cookie,URL 的 ?token= 不能代替登录。verify/cdp.mjs 会从 D:/work/skill/output/dsh-cookie.txt 读取 Netscape 格式 cookie,灌进无头 Chrome 后刷新一次; 可用环境变量 DSH_COOKIE_FILE 换路径。cookie 缺失或过期时,页面会是 401 白页,症状通常是所有 live 套件都报「等不到侧栏按钮」——先查登录,不要误判成插件没注册。

live-pipeline.mjs 用演示文章绑真实期号,只为验界面能不能正确显示状态;结尾一定解绑, 不留错配。它查徽标时不止查「存在」,还查尺寸、是否在卡片内、是否在视口内—— 徽标是 absolute 定位,父容器少一句 position:relative 就会跑到视口左上角被侧栏盖住, 而 querySelector 照样找得到。这类「在 DOM 里但看不见」的错,只查存在性永远发现不了。

批量配图与看成品(会花出图额度,按需跑):

node verify/fill-section-images.mjs <token> --dry   # 只打印每段的 prompt,不出图
node verify/fill-section-images.mjs <token>         # s1–s5 逐张出真图;已是真图的槽位自动跳过(--force 重出)
node verify/shot-sections.mjs <token>              # 逐屏截手机预览,人眼过一遍每段配图

fill-section-images.mjs 的约定:prompt 全写在脚本里(STYLE 是统一风格尾,三处「字迹/内容虚化」 是为了让模型不去渲染中文小字——渲染出来就是乱码)。空段落不配图,s6 这种没正文的段直接跳过。 每张图都是一条新版本,旧版本不动,所以重跑可回退。

shot-sections.mjs 为什么不用导出 HTML 截图:导出里的图位是 __IMG_01__ 占位符(公众号后台不认外部图, 得手工往占位符上传),渲染出来看不到真图。要看图只能看面板里的手机预览。

live-ui.mjs 的两个坑记在这里,省得下次重踩:

  • .click() 在这套面板里是无效的:React 的委托监听收得到,但点了不跳转。必须派发 pointerdown → mousedown → pointerup → mouseup → click 的完整序列(verify/cdp.mjs 里已封装)。
  • 挂载 ≠ 数据到了:等工作台要等 .gzs-card / .gzs-body / .gzs-gov 出现,不能只等 .gzs-root, 否则会读到骨架屏并误判成「首屏空白」。
  • 别用 innerText 等侧栏:这一版 GUI 把面板列表默认收成 display:none,隐藏文本不进 innerText,等待会一直假失败(按钮其实早在 DOM 里)。按 button.textContent 找。

live-interact.mjs 是唯一会真派发鼠标事件的套件(Input.dispatchMouseEvent), 所以它能抓到单测抓不到的两类问题:夹取公式算错(拖不动)、宽度没落盘(刷新就丢)。 它跑完会把对话列恢复成开着——测试不改用户的习惯。

左栏原生对话与分隔条:两个不能退回去的设计

  1. 原生对话是会话内嵌,不是 iframe。 左栏通过 ctx.sessions.retain/create 绑定本工作台 自己的会话,再用 session 作用域的 conversation.content 渲染。公众号与视频号各一条;没有就新建 空白会话。不要改回同源 /?gzhChat=1:完整 Web 应用会按 dsh.sessions.current 恢复宿主上次看的 会话,结果就是工作台里出现别的任务。会话 cwd 必须用 pipelineRoot,不能用插件数据目录 root。
  2. 中栏有两档下限。 MID_NEED=427 是自动布局的「够用宽度」(375 手机 + 两侧内边距), 由 CSS 守住;MID_FLOOR=300 是用户拖过左右栏后的「硬下限」,由内联 min-width 覆盖。 全用 427,真实窗口里只剩约 36px 可拖,看起来是「假动作」;全用 300,纯自动窄窗会把手机 预览压小并偏出中线。两档不能合并。

发布

包名 dsh-plugin-gzh-studio。package.json 的 files 白名单只带运行需要的东西 (22 个文件、解开约 0.76 MB):verify/、seeds/、*.bak 和历史截图都不进包—— 早期的包有 31 MB,就是因为没有 files。

  1. 改 version(semver)与 CHANGELOG;
  2. npm run verify —— 含 pack-check(包边界 / 体积 / 私有路径)与 install-check(从真打出来的 tarball 解包,跑一遍宿主半);
  3. npm publish --access public;
  4. 市场索引加一条:versions[].publishTimestamp 必须是真实的 npm 发布时间戳(不能留 0), source.type 用 npm;
  5. 用户侧:dsh plugin --profile web add dsh-plugin-gzh-studio@<version>。

市场索引入口格式见发布仓库里的 market-index.template.json(publishTimestamp 必须是真实发布时间戳)。

已知边界(没做的部分,写清楚不假装完整)

  • 提案是「约定」,不是「强制」:插件分不出一次 /save 请求来自人还是 Agent。所以实际规则是 ——人工在面板改字走 /save 直通不弹窗,Agent 改稿约定走 /propose。这个约定要靠 Agent 侧 的规则(skill / 提示词)约束。想强制,就得把 /save 也关进提案,那样改一个字也要点确认,很难用。
  • 只推草稿,不发布:已接公众号 draft/add —— 封面走永久素材拿 thumb_media_id、正文图走 uploadimg 换成 mmbiz URL、token 用 stable_token 缓存 7200 秒。仍然不做的两件: 群发/发表(要认证 + 群发权限,而且是不可撤销动作)和定时群发。 直连要把出口 IP 加进白名单(40164),家庭宽带的 IP 会变,所以错误里会把微信报的 IP 抠出来给你复制; 网关模式(wechat.json 里 mode: "gateway")留了位置,但还没接真实网关。
  • 推之前不压缩图片:正文图上限 2 MB、封面 10 MB,超了会明确报出来,需要自己压。
  • 推的是草稿不是发布:推完在「内容管理 → 草稿箱」,群发/发表仍然人工在后台点。
  • 占位图不能直接发微信:SVG 公众号不认。fake 节点只用于跑通流程与对齐版式。
  • 不做富文本编辑:正文只支持 **加粗** 和空行分段。要表格、代码块、卡片排版,去后台改。
  • 不做多作者协作:一个目录一个写者。多人同时改同一篇会互相覆盖(article.json 是整文件原子写)。
  • 禁忌词检查是字面匹配:切成词后按字面找(中文 includes、英文加词边界避免 AI 命中 SAID)。 它不懂语义——「裁员」会命中「不裁员」。宁可多一点假警,也不让人真写进去才发现。
  • 模板只有 6 套,且只覆盖正文:没有做封面套版(首图上不加字),因为中文封面字在生图模型上极不稳定。

致谢

形态与「版本管理 + 可观测」的内核直接搬自 Deckline / dsh-deck-studio(MIT): 三栏布局、记忆账本、旧版不覆盖、提案-确认后再写、多出图节点,都是它先做出来的。 lib/diff.js 的 LCS 行级对照同样源自它的 lib/diff.js,本项目在此基础上重写了类型宽度与超长退化处理。