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,不用黑阴影 |
三条具体做法,都是为了「看起来是编辑工具而不是后台表单」:
- 中间那栏当画布:手机预览 375px 是真机宽度,不改它;窗口不够宽时用
zoom等比缩小整块, 宁可图小也不横向裁掉右半边(裁掉的话滚动条在几千像素高的底部,够不着)。 - 层级靠字号 + 衬线 + 眉标,不靠加边框:
结构/检查器是 10px 大写字距的青眉标,版本(5)同款, 标题用衬线,正文 12.5px。原来全是 11.5px 灰字,一片平。 - 空态与加载态都给人话:加载是骨架条(不是「读取中…」),空列表是虚线卡 + 一句「下一步做什么」。
六个能力
逐张出图:封面(900×383,2.35:1)与每段配图(1024×576)分开出。改哪段出哪段,不重做整套。 节点可有多个;没指定节点时按「默认 → 其余可用」依次试,一个额度不够自动切下一个(跳过谁、为什么跳过,都进账本)。
改稿提案:Agent 改稿走
/propose,只落提案、不动正文;面板逐段给旧/新对照(删的行红底、加的行绿底), 人点「确认」才写入,点「驳回」只记一行账。提案记着它基于哪一版正文的内容哈希——中间被人改过就判过期, 拒绝确认,不会拿一份过时的对照去盖别人的改动。 提案分四类性质,决定该退到哪一步(判据是「改了什么」,不是「改了多少字」):性质 退到 依据 改词句 polish07-deai 只改词句:不许动标题、摘要、配图,也不增删段落 改结构 structure04-write 动了段落增删或整段重写(小标题+正文同时改也算) 改标题 title03-angles 标题在方案阶段就定了;或整篇只在第 11 步改一次并留痕 改摘要 summary11-digest-draft 摘要的正式修订点在推草稿前 混着改时按退得最远的那一步定(摘要+结构 → 退 04,不是 11)。提出方可以声明性质, 但声明不能当免责声明用:声明「改词句」却动了标题/摘要/段落增删,服务端当场 400, 并把撞了哪条红线说出来。
记忆账本:
ledger.jsonlappend-only。第 1 段出过 v001、v002、v003,每版当时的 prompt、节点、模型、尺寸、大小都在账本里,改完 prompt 也能回查当时写的是什么。导出:公众号 HTML(全内联样式、没有一个 class,后台不会剥掉)+ Markdown(带 front matter)+ 图片上传清单(哪张、对应哪个占位符
__IMG_COVER__/__IMG_01__)。手机预览:见上表中间那栏。
六套模板:商务简报 / 杂志编辑 / 日系清简 / 科技蓝图 / 手账纸质 / 夜间深色。换模板只改长相,不动文字。
流水线状态:这篇文章卡在哪一步。见下一节。
去 AI 味:两层判据,都落在可核对的东西上。
- 句子层(30 条词表):命中哪句、命中什么词、属于什么类(编辑腔 / 行业黑话 / 论文连接词 / 模糊限定…)、 为什么像 AI、怎么改。按规则归并理由(不是按词——「此外」和「然而」属同一条规则, 按词拼会把同一条 why 拼两遍,读起来像复读机)。同一句出现在多段时合并成一行并标出在哪几段, 否则模板稿会把同一句列三遍,把结论挤出屏幕。
- 节奏层(4 条判据):长句连排(连续 ≥3 句 ≥25 字)、段落长度太整齐(≥3 段、均长 ≥60 字、 极差 < 均长 35%)、全篇无短句(≥6 句且没有一句 ≤12 字)、每段都以中心句开头。 词表抓不到「读起来像一条直线」,只有这一层能抓。
- 改得动:可机器改的(删过渡词、去「所谓的」)标「可采纳」、可逐句勾;句式类的标「手」, 不许可点——机器硬凑只会把 AI 腔换成另一种 AI 腔。采纳产出提案,不直接改稿,人确认才写入。
- 干净也报分母:结论里写明「扫了 N 句」。只报「没问题」不给分母,用户分不清 「稿子干净」和「功能没跑」。
05/06 两份清单逐项确认:编辑审与读者审各 8 条验收项(文案逐字取自 SKILL.md 的「验收」小节, 不自造条目),面板上逐项勾,勾完这道门才算过。 三种依据必须能区分:
disk(盘上两份清单文件都在)/fallback(靠多轮审稿记录兜底)/panel(靠面板逐项勾)。勾满但盘上没有文件时判panel并在 detail 里写明依据, 同时对账给出 warn:「勾选只是我看过了的记录,不能替代清单本身」。 勾选只写插件自己的reviews.json,一个字节都不往真实仓里写——那个仓是内容生产的事实源, 插件往里塞面板状态会让「盘上有什么」说不清。出图前确认(一篇文章只问一次):说明会消耗额度、这一版会带上几条反馈 (带上之后那几条就从「待带上」变「已带上」)。不用弹窗——弹窗会挡住 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 步流水线一排 chips,
- 侧栏只放够判断的信息:状态词 + 卡在哪 + 下一步 + 一条 11 段进度概览。 完整节点条与事实核对在治理视图(748px 宽)。11 个 chips 塞进 352px 会被压成三行,密得看不清 ——信息量决定放哪,不是「相关的放一起」。
安装
dsh plugin --profile web add dsh-plugin-gzh-studio@0.2.0
# 装完按提示重启 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
装好后:主区面板列表里出现「新媒体工作台」,点开就是工作台。数据全在本机。
首次打开要做的三件事
- 在左侧对话里说「写一篇关于 X 的稿子」;也可以点首页的「新建一篇」手动建(空库时它也是第一步);
- 出图节点:默认是「占位图」(不联网、出 SVG),足够把流程点一遍;
要出真图就在
settings.json里加一个 OpenAI 兼容节点(见下); - 公众号(可选):顶栏「推草稿」里填 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 分钟——首页不能每篇都跑一次。
五条口径
- 旧版永不覆盖。
nextVid()只往后加号;「现在用哪版」是current指针,切版本只改指针,不删文件。 - 账本 append-only。改稿、出图、切版本、反馈、导出、提案都进账本;面板上看到的就是宿主里存的那份,不做本地乐观更新。
- Agent 改稿先进提案。
/propose只写提案不写正文,人看过对照点确认才落盘; 提案记着baseUpdatedAt,正文中间被改过就判过期并拒绝确认——不拿旧对照盖新正文。 确认那一步复用writeScript,不另开一条写入路径,避免「确认过了但正文没按确认的内容写」。 - 图片两条路:走「推草稿」时正文图由插件走
uploadimg换成 mmbiz URL、封面走永久素材, 不用手工;走导出 HTML 手工粘时,本地文件贴不进去,所以图位置写成__IMG_01__这类占位符, 另给清单说明「哪张对应哪个占位符」,上传完在后台替换。 - 空段落在读者视角里不存在。没小标题、没正文、没配图的一段,手机预览里整段跳过——
以前它会留一个「第 N 段还没有配图 · 点这里出图」的虚线框在正文最末尾,那是编辑器控件,
混进预览里像是文章没写完。同理,QA 的「N/M 段还没有配图」也不把空段算进分母:
空段已经单独报了一条
section-empty,再数一遍就是指向一个还不存在的段落。 段号仍按原始位置编号,跳过中间的空段不会让后面「第 3 段」变成「第 2 段」。
Agent 怎么接
不需要在插件里内置 LLM,三步走:
- 取约束:
GET /api/gzh-studio.brief?id=<id>拿到一段纯文本——讲给谁、想让人带走什么、 禁忌词、分段现状(段号 + 小标题 + 字数 + 有无图)。这段直接贴进对话,Agent 起草时就有边界。 否则audience / takeaway / forbidden只是躺在article.json里的三个字符串,没人看得见。 - 提提案:
POST /api/gzh-studio.propose,body 与writeScript同形 ({id, note, title?, subtitle?, summary?, sections:[{id,heading,body}]})。 只写提案,正文不动。 返回{pid, status:"waiting-user", summary}。 - 等人裁决:人在面板上点确认才会真写入;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),
所以它能抓到单测抓不到的两类问题:夹取公式算错(拖不动)、宽度没落盘(刷新就丢)。
它跑完会把对话列恢复成开着——测试不改用户的习惯。
左栏原生对话与分隔条:两个不能退回去的设计
- 原生对话是会话内嵌,不是 iframe。 左栏通过
ctx.sessions.retain/create绑定本工作台 自己的会话,再用 session 作用域的conversation.content渲染。公众号与视频号各一条;没有就新建 空白会话。不要改回同源/?gzhChat=1:完整 Web 应用会按dsh.sessions.current恢复宿主上次看的 会话,结果就是工作台里出现别的任务。会话 cwd 必须用pipelineRoot,不能用插件数据目录root。 - 中栏有两档下限。
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。
- 改
version(semver)与 CHANGELOG; npm run verify—— 含pack-check(包边界 / 体积 / 私有路径)与install-check(从真打出来的 tarball 解包,跑一遍宿主半);npm publish --access public;- 市场索引加一条:
versions[].publishTimestamp必须是真实的 npm 发布时间戳(不能留 0),source.type用npm; - 用户侧:
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,本项目在此基础上重写了类型宽度与超长退化处理。
No comments yet. Be the first to write one.