Skip to content
gaoweiblue-sourcePublic

About

No description, website, or topics provided.

Resources

Stars

55 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

文案工坊 · 多用户自媒体文案生成系统

两个板块:创作(题材 → 方向 → 成稿 → 编辑 → 学语气)和热点(抓榜单 → 和账号比对 → 找可蹭的点)。

账号设定 → 给一个题材 → 自动生成 3 个差异化话题方向 → 选中一个 → 流式生成完整成稿 → 编辑器里划词改 / 唤起 AI → 确认后喂回账号学语气。

多用户:每个用户独立注册登录,账号设定与创作记录互相隔离。

快速开始

npm install
cp .env.example .env   # 填入模型密钥;不填也能跑(演示模式)
npm start              # http://localhost:5177

首次打开先注册一个账号即可。

模型通道

server/llm.js 支持三种通道,按环境变量自动选择:

通道 配置 说明
anthropic ANTHROPIC_API_KEY 官方 SDK,默认 claude-opus-5;选题走结构化输出(output_config.format),正文走流式
openai OPENAI_API_KEY + OPENAI_BASE_URL 任意 OpenAI 兼容接口:DeepSeek / Kimi / 通义 / vLLM / Ollama
mock 无需配置 无密钥时的本地模板,保证全流程可跑通,便于演示和开发

也可用 LLM_PROVIDER 强制指定。

账号设定

创作之上有一层「账号」——先给这个号定位,之后每次创作都自动带上这份语境:

字段 作用
账号名称 标识,创作记录按它归档
发布平台 / 风格调性 新建创作时的默认值,可单篇覆盖
主要内容 这个号写什么,约束选题不跑偏
目标用户 决定称呼、举例和知识密度
解决的问题 三个话题方向都要回应它
备注 / 禁忌 固定要求与不能写的东西

个人人设

账号设定里还有一组「个人人设」,描述博主本人(全部选填):

字段 在文案里怎么起作用
年龄 自称与生活处境的年龄感(可填「32」也可填「30 岁出头」)
性别 第一人称叙述中自然的自我指代
所在行业 / 岗位 举例和类比优先取自这里的真实工作场景
性格特征 语气节奏、态度和用词习惯

提示词里明确要求:让人设体现在选材、措辞和态度里,而不是被直白地写出来,同时禁止虚构与人设冲突的经历(岗位对不上的专业细节、年龄段不符的生活状态)。选题阶段还会优先选「凭这个人的年龄、行业、岗位和性格真的写得出来、别人写不出来」的角度。

实测效果(DeepSeek,账号「下班后的厨房」+ 人设「32 岁 / 女 / 互联网 / 产品经理 / 说话直接爱吐槽」,题材「周末备菜」):三个方向里出现了「用产品经理思维拆解备菜流程」,成稿里自然带出「冰箱乱得像没人 review 的 backlog」「多线程工作」「像预发布上线一样把风险前置」——行业和性格落在了具体措辞上,而不是喊出来。

怎么进的提示词

账号设定拼成「—— 账号设定 ——」块、个人人设拼成「—— 博主本人 ——」块,一起放在提示词最前面(见 server/prompts.js 的 personaBlock / creatorBlock),选题和成稿两步都带,system 提示词里也有对应约束。留空的字段不会写进提示词。

一个用户可以建多个账号(上限 20),侧栏切换即切换创作语境和历史视图;选「全部创作(不限账号)」可以跨账号浏览,此时新建的创作不绑定账号。

快照与生效时机:每条创作会存下当时的账号设定快照,所以历史稿件永远能看到它是基于什么定位写的。改了账号设定后,对旧稿点「换一批」会用最新设定重新出方向(并刷新快照),保证方向与随后的成稿一致。删除账号不会删创作——它们会保留在「全部创作」里,快照仍在。

成稿编辑器

成稿卡片有「阅读 / 编辑」两个模式,正文的唯一真相是 Markdown 原文(state.draft.content)。

划词改写 —— 在任一模式下选中文字,就地弹出菜单:扩写 / 专业点 / 轻松点 / 缩写 / 举个例子 / 更好懂。 结果流式出现在浮层里,可以「替换原文」「插入到后面」或「重来一次」,确认之后才会写进正文。 阅读模式下选中的是渲染后的文本,系统会把它反查回 Markdown 原文的偏移(同一句出现多次时按相对位置就近匹配); 遇到跨格式的选区会提示切到编辑模式。

/ 唤起 AI —— 编辑模式下打 /(半角全角都认),光标处弹出菜单:续写这一段 / 写个开头 / 加个小标题 / 写个结尾, 也可以直接敲一句自定义指令回车。生成同样先进浮层,确认后才插入(自动补空行,不会和上下文粘住)。 触发规则按中文写作习惯设计:句号、逗号、括号、汉字后面都能直接唤起;只有紧跟在字母、数字、冒号或另一个斜杠后面时才不触发, 以免误伤 https://、and/or、3/4 这类写法。

改写请求会带上选区前后各 600 字的上下文,加上账号设定、个人人设、语气档案和平台规范,所以改出来的东西接得住上下文、也不跑调。 提示词里明确约束了边界:输出逐字替换选区,不许把上下文里已有的内容(尤其小标题)抄进来,选区多长输出就多长。

编辑内容 1.2 秒防抖自动保存,右下角显示「未保存 / 保存中 / 已保存」,也可以手动点保存;有未保存内容时关页面会拦一下。

成稿检查

出稿后自动跑一遍,编辑器工具栏也有「检查」按钮随时手动跑。分两层:

第一层:语言错误(可一键采纳)

只报五类:错字 / 语句不通 / 重复啰嗦 / 前后矛盾 / 标点格式。提示词里明确写了不该报什么——口语、短句、省略句、网络用语、emoji、话题标签都是自媒体的正常写法,风格和详略不归校对管。这条很重要,否则校对会变成把稿子改成八股。

每条问题给出「原文片段 → 改后片段 + 一句话原因」,可以逐条采纳或忽略,也可以一键全部采纳。两个安全设计:

  • quote 必须能在原文里一字不差地找到,服务端对不上的直接丢掉(并在界面上如实告诉你丢了几条),这样替换才是安全的。
  • 同一片段出现多次时只替换第一处,并在那条上标出「原文出现 N 次」。

整体判定分三档:ok 可直接发 / minor 有几处小毛病 / bad 大面积不通顺。判成 bad 时会直接建议重新生成,而不是逐句去补——跑飞的稿子修不回来。

第二层:调性与风险(只提示,不自动改)

检查时会把账号设定、个人人设、语气档案、平台写作规范、借势的热点一起交给模型,所以谈得上"符合度"。六个维度:

维度 查什么
账号调性 跑到定位之外、面向的不是目标用户、没回应它要解决的问题、违反写明的禁忌、语气不像这个人
平台调性 结构、篇幅、开头方式、排版、结尾是否明显不合该平台规范
公序良俗 地域/性别/年龄/职业/学历歧视、贬低人群、煽动对立、极端贩卖焦虑、低俗擦边、美化违法
法律风险 广告法绝对化用语、医疗保健疗效承诺、投资收益承诺与荐股、未标明的广告、点名贬损企业或个人、未授权使用他人作品肖像
热点使用 只在借势时查:正文关于这条热点的事实陈述有没有超出概要范围;概要为空却写了具体细节的一律报高
事实存疑 具体数字、比例、机构名、研究结论、他人引语被当成事实但没有可核查出处

每条给「维度 + 级别(高/中)+ 触发片段 + 问题 + 建议」,不做自动替换——这类问题该由作者判断。综合风险取所有提示里的最高级别。

两道去凑数的闸(实测模型会为了显得认真硬找茬):

  1. 丢掉「低」这一整档——模型在这档放的基本是"没问题但可以更好"的废话。schema 里仍保留这档,只是服务端不展示,这样它排序时还是诚实的。
  2. 丢掉「先承认符合再转折」的条目——内容符合账号定位,但…、未涉及歧视,但… 这种句式直接过滤(正则带 (?<![不未没]) 负向断言,不符合平台规范,但… 是真问题不会被误杀)。

被丢掉几条会在总结里如实写出来,不是偷偷吞掉。

实测:一篇故意植入五类违规的稿子,六个维度全部命中、quote 全部可定位;一篇正常成稿从 6 条提示降到 2 条,剩下的两条都指向同一个真问题(正文里编的 GitHub star 数没有出处)。

顺带调了采样参数

实测 temperature: 1.3 在中文长文里会跑出语无伦次的句子(词语拼贴、成分残缺)。现在默认 1.0(DeepSeek 的默认值)+ top_p: 0.95,结构化输出用 0.6。语气靠账号设定和语气档案撑,不靠高温度堆"创意"。三个值都可以用环境变量覆盖:LLM_TEMPERATURE / LLM_TEMPERATURE_JSON / LLM_TOP_P。

口播提示

成稿卡片的第三个模式「口播」:给要念出来的稿子标语气 / 重读 / 停顿 / 表情 / 动作。适合抖音口播、B 站视频这类要出镜的内容。

不往正文里塞标记 —— 成稿保持干净可直接发,提示是单独一层。顶部给整体基调、语速节奏和一句出镜提醒;下面按「一口气念完的一句或几句」切段,每段:正文里重读的词直接下划线标出来,配语气/重读/停顿/表情/动作的标签。「复制口播稿」导出成可以照着念的格式。

提示词里的关键约束是克制:

不要每条都填满。一篇稿子里真正需要专门标表情或动作的地方通常不超过三分之一。为了显得认真而每句都配一个动作,念出来会像提线木偶——留空是完全正确的答案。

还有两条:表情动作要真人出镜的自然幅度(挑眉、抿嘴,不是舞台剧);给了人设的要符合这个人,性格内敛的不要让他挥手跳跃。

服务端校验:quote 必须能在正文里逐字找到(对不上的丢掉),重读词必须真的出现在那段里(否则剔除)。正文一改,已生成的提示自动作废——位置全对不上了,留着是误导。

标志

原来三个入口各挂一个 emoji(✍️ / 🛠️),在不同系统上长得不一样,也和整套视觉没关系。 换成 public/mark.svg:一块"正在写的文本" + 一个信号色光标—— 三条白色横线(第三条短),紧跟一个比行高略高的红色光标块,底是主色的斜向渐变圆角方块。

一个画出来才发现的问题:光标和第三行之间留了 1.7 个单位的空,放大看就是"列表旁边一个红点", 读不出"文字后面的光标"。把第三行加长到 7.3、间距收到 0.9 之后才立住。小图形上这种间距差一点就全变味。

一个文件供三处用:主界面、管理后台、提示词说明书,同时当 favicon(SVG 图标现代浏览器都认)。 尺寸靠 CSS 调(顶栏 22px / 登录页 28px / 说明书 26px),圆角和投影跟着尺寸走。

视觉:科技 + 时尚媒体

改的主要是设计令牌,不是到处调样式——所以整站(含管理后台、提示词说明书)一起变。

  • 主色换成电光靛 #4b39ef,比原来那支通用蓝更锐。主按钮给了很浅的纵深渐变和同色投影。
  • 加了一支信号色 #ff3d2e,只用在"正在发生"的东西上:录制中的呼吸点、生成中的任务、正在播的段落。 别的地方一律不用——信号色到处用就不是信号了。
  • 中性色偏冷、对比拉开,正文 14.5px/1.68,标题字距收到 -0.018em。标题和正文差别越明显,页面越像刊物而不是表单。
  • 背景一层极淡的网格(56px,顶部径向遮罩渐隐),淡到几乎看不见,但大面积留白不再显得空。
  • 顶栏改成毛玻璃,底下压一条主色渐变细线——整页唯一的"发光"元素。
  • 板块标题左边一道 3px 主色短线;数字统一 tabular-nums,跳动的计时不左右晃;代码和提示词一律等宽字体。
  • 圆角收小(12→10),chip 从药丸改成方角标签,更像技术标注。

顺带修了一个一直存在的 bug:后来加的样式(账号设定页、提示词说明书这些后来的面板) 一直在用 --line / --card / --ink 这三个从没定义过的变量,边框和底色其实一直没生效。 现在在 :root 里补了别名指向 --border / --panel / --text,两套命名都能用。

成稿的标题行和工具条吸顶

正文动辄一两千字,往下读的时候模式切换和那几个动作会滚出视野。现在标题行 + 工具条 position: sticky 吸在顶栏下面,用负边距把卡片自身的内边距铺满——不然吸住时两侧会露出下面的正文。

改工具条时把 .head-stick 的闭合标签一起替换掉了,浏览器自动补在了父节点末尾—— 于是它包住了整篇正文,position: sticky 就失效了(一个和容器一样高的元素没有可吸的余量)。 HTML 少一个闭合标签不会报错,只会静默变形,这类改动之后值得回头看一眼嵌套。

贴住之后才给分隔线和投影,没贴住时保持干净。判断用 IntersectionObserver 观察一个哨兵元素, 而不是监听 scroll——后者每帧都要算,这里只需要知道"越界了没有"。

顺带修了一个既有的错位:顶栏高度在 .sidebar 里写死成 57px,而视觉改版把顶栏 padding 调大之后 实际是 66px——侧栏一直比顶栏低 9px 起。现在统一成 --topbar-h 令牌, 顶栏自己用 min-height 锁住,侧栏、吸顶工具条、以及 JS 里的 rootMargin 都引它, 不再在几个地方各写一个数。实测顶栏底边和吸顶块顶边都是 66,重叠 0。

口播工具条:自己念

口播面板的工具条只有一组动作,和成稿那边的「确认 / 导出」是同一套组件:

自己念  提词器 · 复制口播稿                              生成口播提示

「复制口播稿」把整体基调、每段正文和标注导出成能直接照着念的纯文本; 「提词器」全屏滚动播放,见下面的提词器一节。

成稿只有三个动作:修改 / 确认 / 导出

原来一排八个按钮加一个三选模式条,等于把八个决定同时摆在人面前。现在只问一件事: 这稿你是要改、认了、还是拿走? 认了之后再问"接下来做什么",拿走之前再问"要什么格式"——一次一个决定。

  • 修改 → 进编辑模式。进去之后这个按钮换成「← 返回阅读」,旁边一个「编辑中」指示器。 离开编辑会自动落盘(原来这个保存挂在模式条的点击处理上,模式条撤了就得挪进 setMode)。
  • 确认 ▾ → 生成口播提示 / 出多平台版本 / 配图 / 喂给账号学习 ——— 再检查一遍 / 归档。 每项底下一行说明它到底干什么。看非原文版本时,前面几项自动置灰并说明"先切回原文"。
  • 导出 ▾ → 复制 / 下载 Markdown / 下载图文(.html,图嵌在里面)/ 导出 PDF。

PDF 走浏览器打印,不是自己生成字节流——那要嵌中文字体,体积和复杂度都不划算。 交给浏览器排版,中文断行和图片都是对的,用户在弹窗里选「另存为 PDF」。 配了一份 print 样式表:界面元素全藏掉、只留正文,插图 break-inside: avoid 不跨页断开。

原来那六个按钮保留成隐藏元素,菜单转发点击到它们——几百行处理逻辑(复制的富文本剪贴板、 下载的自包含 HTML、检查、学习)一行没动,菜单只是外壳。

宽度按步骤给,不是一个全局值

.main 原来全局卡在 900px,1536 宽的屏上选方向那一步左右各空掉近 200px。 但直接放宽会伤正文——长文一行超过 ~800px,眼睛回扫就费劲。一个全局宽度伺候不了这两种内容。

改成 .main[data-step]:第 2 步(选方向)放到 1280,第 1、3 步保持 900。 goStep() 里顺手写 dataset.step,加了 .18s 过渡免得切换时突然跳。

实测 1536 视口:第 2 步 main 1276px、方向卡 1182px(原来 806px),第 1、3 步仍是 900px。

让三个方向的差异一眼可见

原来卡片上写的是「方向 1 / 2 / 3」——纯编号,没有信息量;而真正的差异说明(angle 字段) 是整张卡里最小最淡的一行。等于把最该看的埋了,把没用的放大了。

两处一起改:

  • schema 加 label:2-6 字给这个方向定性(投资逻辑 / 机会清单 / 避坑指南), 提示词里写明"label 就是这个差异维度的名字,三个必须互不相同"。 代码里再兜一层:标签重复的直接清空回落到「方向 N」——重了就等于没分类。
  • angle 的描述改成"说清它写什么、另外两个不写什么"。改完模型给的是 「从产业投资视角拆解…而不是讲个人炒股经历或技术原理」这种带对照的句子, 第三条甚至会写「区别于投资逻辑和机会清单」。

视觉上把 angle 提成卡里最重的一块:白底描边、13.5px、正文色,前面一个「差异」小标; 标签变成带序号徽章的主色药丸。其余(钩子、结构、适合谁)保持弱化。

老草稿没有 label 字段,回落到「方向 N」,不会报错。

方向卡横排

三个方向原来是并排的窄长条(270 宽 × 900 高),要上下扫三趟,而且没法横着比。 改成一个方向一横条:左边"是什么"(标签 / 标题 / 切入点 / 钩子 / 适合谁 / 按钮), 右边"怎么写"(结构要点),中间一条竖分隔线。实测每条 806×288,扫读成本低很多。

窄屏(<860px)下左右两栏自动堆成上下,分隔线从竖线变成虚横线。

创作分成三步,一次只显示一步

原来三张卡(创作简报 / 三个话题方向 / 完整文案)堆着往下滚,到第三步时前两步还杵在上面—— 页面又长,又分不清现在在哪一步。现在一次只显示一张卡,由 goStep() 统一切换。

步骤条兼做导航:走过的步骤可以点回去,往前只能走到已经有内容的那一步 (第 2 步要有 topics,第 3 步要有 content),够不到的步骤不给点击态。 第 2、3 步的卡头还各有一个「← 改题材」「← 换方向」。

来回切不丢状态——题材、方向、正文都还在,因为切的只是显示,state.draft 没动。 从历史打开一篇旧稿会直接落到它已经走到的那一步:有正文去第 3 步,只有方向去第 2 步。 生成过程中(state.streaming)禁止跳步。

侧栏账号列表

多账号时一行一个直接列出来,不用下拉——下拉的问题是"得先点开才知道有什么", 而账号是长期语境,值得一直摆在那儿。当前那行展开成卡片(平台标签 + 内容定位 + 齿轮 + 可折叠的完整设定), 其余是单行(头像 + 名称 + 平台标签),末尾一行「全部创作」。

  • 头像:拿名字第一个字做字母标
  • 齿轮只在当前账号那行出现,点进账号设定整页

刷新后停在原来的账号。 存在 localStorage 里,注意三种情况要分开: 空串是"全部创作"(和"没存过"不是一回事);存的账号可能已经被删了,删了就回落到第一个。 不记的话每次回来都跳回第一个账号,多账号的人一刷新就得重新选,还容易在错的账号下开始写。

改这块时踩了一个:把独立的「编辑」按钮和下拉一起删了,但 el.editPersonaBtn.addEventListener 还留着,对着 null 绑事件 → 模块初始化中断 → 整页白屏。 后面几十行的 const 全部停在 TDZ,控制台报的却是 Cannot access 'toastTimer' before initialization—— 真正的原因在更前面。删 DOM 元素时得回头搜一遍引用。

账号设定是一整页,不是浮层

原来几个板块(基本设定 / 个人人设 / 素材库 / 语气样本)挤在一个浮层里共用一根滚动条, 加新的就没地方放了。现在拆成整页 + 左侧导航,右边一次只显示一个板块。

加一个新板块 = ACCOUNT_TABS 里加一条 + HTML 里加一个 data-panel 块,导航是从配置渲染的。

几处顺带处理掉的体验问题:

  • 依赖账号 id 的板块会锁住。素材库、语气样本都挂在 persona id 上,新建时这几项在导航里是灰的, 写明"先保存账号",而不是点进去看到一个空面板。
  • 新建保存后留在页面上,不再直接关掉——顺手解锁那几个板块并提示"现在可以设了"。 之前的浮层会在保存时关闭,等于逼人再点开一次,左边这几个板块也就白解锁了。
  • 换账号回到基本设定,同一个账号记住停在哪。打开另一个号直接落在「语气样本」上会以为走错地方。

采纳之后自动重查

采纳一条修改,正文就变了——原来那份检查结果立刻过期,但界面上没有任何表示, 人会继续对着一份已经不准的清单往下点。

现在采纳后自动重查,但不是每采纳一条查一次:一条一条点会连着发好几次请求, 每次都是一次模型调用。攒 2.5 秒,停手了才查,中间再点就重新计时。 实测连点两条只发了 1 次请求。

等待期间把结果区压暗并禁用点击,提示「正文已改,正在重新检测…」—— stale 要等结果回来才解除,请求还在飞的时候列表里还是旧数据。 重查前先 flushSave() 落盘,否则查的是改之前的正文。

提示词 A/B 与 eval(管理后台)

A/B 的意义全在"记下每次用了哪个变体"上——没有这一列,换提示词就只是"感觉好像好点"。 prompt_variants 挂多份 system,按权重分流,usage_events.variant 记下每次调用用的哪一份。 内置那份也参与分流,否则一开 A/B 就等于全量换了新提示词。目前接了「选题方向」和「成稿」两个功能—— 每加一个都要在调用处接上分流,不是所有提示词都值得这个复杂度。

eval 的打分分两层,而且刻意不用大模型当裁判。 这个项目一路的结论是模型在"给点评价"的压力下一定会顺着说 (成稿检查那里实测过:不加代码过滤就会凑一堆"符合…但…")。让它评自己的输出,得到的是和气不是信号。

  • 确定性指标:客观、可复现、不花钱。文本类看字数偏离、格式痕迹(把口播的【画面】标注或 小红书的 #话题 带到公众号)、段落结构;结构化输出看约束有没有满足——标签是否互不相同、 差异说明里有没有真的做对照(而不是/区别于)、必填字段有没有空的。
  • 盲测人工对比:两栏并排、随机左右、隐去变体名。知道哪边是新的,判断就已经偏了。

汇总用中位数,和复盘那边同一个理由。

第一次实跑就抓到了东西:我写了个"更狠的差异要求"变体(把「互相错开」改成「必须能一句话说清 另外两个不写什么」),结果它在**「有对照」指标上是 0,内置是 1**——要求写得更死,模型反而不照做了。 这正是 A/B 该拦下来的东西。

落地页

/ 按登录状态分流:没登录看落地页,登录了直接进应用,应用固定在 /app。 用 cookie 在服务端判断而不是前端跳转——前端跳会先闪一下落地页, 每天用的人每天看一次那个闪烁。

内容上刻意避开两件事:

  • 不写"AI 帮你一键生成"。那是这个赛道的通用话术,说了等于没说。 首屏说的是"AI 写的东西一眼假,不是因为它不会写"——先承认问题,再说为什么。
  • 不编造社会证明。没有用户数、没有推荐语、没有 logo 墙。这个阶段还没有真实用户, 编出来一戳就破。用来替代的是项目里的真实实测数字: 素材库里的具体数字被原样写进稿子;提示词写了"宁可少插"但 281 字照样给 3 张。

还专门放了一节「这个产品现在什么状态」,如实写支付没接通、自动发布做不了。

价格从 /api/pricing 取(公开接口,不回任何用户数据),不写死在 HTML 里—— 写死迟早和后台对不上,那种不一致比不显示价格更伤信任。

样式沿用主产品的设计令牌。落地页和产品长得不一样的话,进来之后会有落差, 那种落差本身就是不信任的来源。

营销落地页(独立入口 /start)

/ 上那版是说明型的:解释机制,读者是"用过 AI 写作、嫌它假"的人。 投放需要的是另一种东西,所以单开了 /start,两页卖点和动线完全不重叠, 互相不改:

/(说明型) /start(营销型)
卖点 机制——不像 AI、上下文、素材库 结果——产能,一条选题够发一天
受众 嫌 AI 写得假的人 一个人扛一个号、更新不动的人
动线 解释 → 原则 → 价格 → 现状 钩子 → 产物展开 → 三步 → 步骤账 → 顾虑 → CTA
CTA 两个(开始 / 看怎么工作) 单一 CTA 重复三次,二级链接尽量少
价格 完整价目表 不放——多一个价格表就多一个先犹豫的理由

主证据是「一条选题扇出四份产物」:公众号 1810 字、小红书 653 字、微博 281 字、 口播稿,左边选题卡在桌面端 sticky 跟着滚,视觉上强调"同一个源头"。

营销页最容易失真的地方是数字,这里守了两条:

  • 只比步骤,不比时间。 对比那节写的是 6 步 → 4 步,其中 3 步是重复劳动。 步骤数是能数的,"从 3 小时降到 20 分钟"是编的——没测过就不写。
  • 免费档的说法回代码核对过。 "150 点够写几十篇稿子":按一篇约 5k token、 0.4 点/1000 token 算是 75 篇,"几十篇"是保守说法。

同样不编造社会证明和稀缺感。FAQ 里「能直接发吗」如实写了自动发布做不了—— 平台开放接口要企业主体,个人号申请不下来。营销页承认这个反而比绕过去可信。

promo.js 会把 utm_source / utm_medium / utm_campaign / from 透传到 /app 链接上,不然不知道哪条渠道真的带来了注册。类名统一 pm- 前缀, 和 landing.css 的 lp- 完全隔开,两页各改各的互不影响。

支付凭据在后台管,但管得比别的严

改个商户号不该要 SSH。但商户私钥能以你的名义签名收款,泄露的后果比 API key 严重得多, 所以这一块的存取规则和别的配置不一样:

  • 加密入库(AES-256-GCM),密钥留在 SECRET_KEY 环境变量里。 只拿到数据库文件(备份、开发副本、误传的 dump)是解不开的——而那恰好是最常见的泄露方式。 没设 SECRET_KEY 时会用一个派生的弱密钥并在日志里吵,因为那等于没加密。
  • 只写不读。接口永远不返回原文,只回「PEM 私钥 · 86 字符」或「****3456」。 实测:存完私钥后整个接口响应里搜不到 PEM 内容。
  • 环境变量优先且锁定。env 里配了的,后台改会被拒(「支付通道 由环境变量提供,请在服务器上改」)—— 生产可以完全锁死在环境变量里,那样即使后台被攻破也改不动收款配置。
  • 换过 SECRET_KEY 之后旧数据解不开,decrypt 返回空而不是抛—— 让"这项没配置"的路径接住它,而不是把整个服务打挂。

连通性自检:填完不试一下,第一次真实支付才发现配错,那时候是用户在等着付钱。 自检查回调地址是不是公网 https、各渠道凭据齐不齐、加密密钥有没有设。

这挡不住能读进程环境的人——它挡的是数据库泄露这一类。

支付:微信 + 支付宝

两家都需要企业主体 + 对公账户 + 已备案域名,个人开发者申请不下来(个体工商户可以,但仍要执照)。 所以做成 provider 抽象(和 llm.js / images.js 一个套路),拿到商户号只是换实现; PAY_PROVIDER=mock 能把下单→支付→发货整条流程在本地跑通。

支付这块和别的接入层不一样,有几条错了就会丢钱的规则,都落在代码里:

  • 订单是唯一真相。用户套餐由订单驱动,回调只负责把订单标成已付,开通是"订单变 paid"的副作用。 不允许回调直接改用户套餐。
  • 价格从服务端查表。前端只传买什么。信任前端传来的金额 = 一分钱买 Max。
  • 金额落库并比对。回调带来的数字不可信,必须和自己存的订单金额比。
  • 发货幂等。回调会重复推送(微信最多 15 次),靠 UPDATE … WHERE status='pending' 去重——第二次 changes 为 0 就跳过发货。
  • 验签失败一律丢弃,且不回 200,免得对方以为收下了。
  • 回调可能丢。前端 3 秒轮询订单状态,服务端在被问到时也会主动查单补偿——不能只等推送。
  • 金额用整数分。浮点算钱迟早出事。

实测的攻击面:伪造签名 → 400;把金额篡改成 1 分 → 400;不存在的订单 → 400; 三次都没能改动用户套餐。正常回调后开通成功;同一个加购包连推三次,点数只加了一次 (用加购包测幂等而不是套餐——套餐是幂等赋值,看不出重复发货)。

回调路由要走原始字节,不能先被 JSON.parse 吃掉——验签算的是原文。

套餐、配额与用量界面

托管密钥的 SaaS 里配额不是商业功能,是安全底线——没有它,一个用户能把平台的 key 无限跑爆 (配图 0.14 元/张)。

统一点数制(1 点 ≈ 0.01 元成本),不做分项配额。两种成本单位(token / 张) 单价差几十倍,分项限额(「每月 100 张图」)看着直观,但用户不用的那项就浪费了, 而且每次调价要重设好几个数。界面上再换算成"≈ 多少篇稿子"给人看。

套餐 月费 点数 封顶成本 毛利 屏蔽
免费 0 150 1.5 元 — 配图
Pro 49 2000 20 元 59% —
Max 199 9000 90 元 55% —

一条定价上的判断:free 不开放配图——那是真金白银按张计费的,免费送必被薅; 写稿的额度足够让人试出价值。

校验在调用之前,事后统计拦不住任何东西。文案类的拦截放在 llm.js 的 tracked() 里统一做—— 十几个调用点各写一遍一定会漏。拿不到真实用量时按保守预估判断,调完按实际扣。

用量界面:顶栏常驻余额,低于 15% 变红——撞到 402 才提醒就晚了。 面板说三件事:还剩多少、花在哪了、升级能多什么。明细按功能分组并折算成点数, 一眼能看出出图是大头(一张 14 点,4 次文案调用一共才 5 点)。 撞到 402 时自动把面板顶出来——普通 toast 说一句就消失,而这一刻正是最愿意付费的时候。

顺带修的一个:withRetry 原来会重试配额错误并包成 502「生成失败」。 额度不够重试一百次也不会好,而伪装成服务故障会让人反复点重试,每点一次都是一次真实调用。 现在客户端错误不重试也不包装。

还没接支付。 改套餐的接口默认拒绝(ALLOW_SELF_UPGRADE),上线前要换成 "下单 → 支付回调 → 改状态",否则谁都能把自己改成 Max。

提示词说明书(独立入口 /prompts)

十二条提示词各一页说明,读者设定是AI 产品经理——不是罗列提示词正文,而是按三个维度写: 产品定位(这一步在产品里扮演什么角色)、价值(拆成对用户和对产品)、 功能逻辑(上下文怎么拼、约束怎么下、冲突时听谁的)。

外加一节其它文档不会写的:代码兜底。每条都标出"提示词之外还加了哪一层硬规则"—— 枚举、正则、白名单、二次校验。十二条全都有,这本身就是结论。页头把这条原则单独立了出来: 看得见的失败,一律不靠模型自律。

页头第二块是 user 消息怎么拼。这件事比任何单条提示词都更能解释这个产品: system 负责"怎么做",八个上下文块(账号定位 / 个人人设 / 栏目要点 / 栏目素材 / 素材库召回 / 语气档案 / 平台规范 / 热点信息)负责"这一次的语境是什么"。同一份 system 配不同的块, 出来的东西完全不同——调提示词之前,先确认该拼的块拼上了没有。

按产品流程分六段(选题 4 / 成稿 2 / 打磨 2 / 发布 2 / 口播 1 / 视频 1),左侧导航跟随滚动高亮。 挂了 A/B 变体的标 A/B。 每条底部可展开 system 原文和 JSON Schema——从 server/prompts.js 直接 import, 不是复制的副本,改了提示词说明书跟着变。

/prompts 是和 /admin 一样的独立入口,但不需要登录——它讲的是产品怎么设计的,不含用户数据。

应用内浮层,不用浏览器弹窗

原来 19 处确认/输入用的是原生 confirm / prompt。换掉不只是为了好看:

  • 原生弹窗顶着 "localhost:5177 显示" 的标题,样式完全不受控;
  • 有些浏览器会给出"阻止此页面再弹出对话框",勾了之后功能就静默失效;
  • 它是同步阻塞的,弹着的时候页面上别的东西全停了。

public/dialog.js 是一份两个页面共用的组件,自己建 DOM,不依赖任何 HTML 结构。 接口做成 await 的,调用处基本原样:

if (!await ask.confirm({ title: '删掉这条素材?', ok: '删除', danger: true })) return;
const v = await ask.form({ fields: [...] });   // 取消返回 null

几个细节:删除类的确认给 danger 红按钮;ESC 和点遮罩都是取消; 纯确认时焦点落在主按钮上(回车直接过),有输入框时落在第一个框并全选; 多行输入里回车是换行不是提交;必填留空不关窗,标红并聚焦——不然人会以为提交成功了。

顺带把后台"新增 eval 用例"从连着弹两次 prompt 改成一个浮层收完四个字段—— 中途取消不会留下半条记录,平台和调性还能给默认值。

管理后台的左侧分区

八张卡堆在一列里往下滚,找一个东西要滚半天。改成左侧导航,一次只显示一个分区: 概览 / 成本 / 提示词 A/B / Eval / 功能用量 / 提示词 / 用户 / 管理员。 选中的分区记在 localStorage 里,刷新回到原处。 加分区 = ADMIN_SECTIONS 里加一条 + HTML 上打个 data-sec。

多平台版本

一份账号设定、一篇成稿,出多个平台的版本。工具栏「多平台」→ 勾平台 → 一个一个生成。

关键决定是改写,不是各平台各生成一遍。 后者会让同一件事在小红书和公众号变成两个故事—— 事实、例子、数字各说各的,读者一对照就露馅,而且和"栏目素材是唯一可信事实来源"那条直接冲突。 所以 ADAPT_SYSTEM 里第一条就是事实层不许动:篇幅短就整块删,不要把每句话压成缩写体; 篇幅长可以展开,但只能用原文已有的信息。

另一条实测踩出来的:不许把原平台的格式痕迹带过来——口播脚本的【口播】【画面】标注、 小红书的 #话题标签,改到公众号里要全部去掉。

实测 1810 字的公众号原文 → 微博 281 字(目标 300)、小红书 653 字(目标 600)。

版本切换条把原文和各平台版本平级放,不做主次——发哪个平台哪个就是正文。 正文后来被编辑过的话,旧版本会标「需重做」(比对改写时记下的 fromChars)。 编辑、口播、检查这些始终针对原文,切到别的版本时会禁用——否则"编辑的是哪一版"会变成一笔糊涂账。

图文配图

工具栏「配图」给当前这一版排插图位并出图,图直接插进阅读区的正文里。

一篇 2-5 张,而且提示词里明确要求不画具体的人——需要表现"人"就用手部特写、背影剪影、 空的工位。公众号插图本来就该是概念图和场景图,绕开人物,一致性问题就不存在。 (逐句配图的"图文成片"在早期版本里试过:保不住人物一致性,这个上限修不了,已经整个拿掉。)

两条代码兜底:

  • anchor 必须能在正文里逐字找到,找不到的丢掉(和口播提示的 quote 一个道理),同一个位置只插一张。
  • 张数按字数硬卡,大致每 450 字一张、上限 5 张。提示词里写了"宁可少插", 但实测 281 字的微博也给了 3 张——插图是给长文换气的,短文里每隔两段一张只会打断阅读。

配图走横图 1280×720——竖图放进正文会把一屏占满。实测真实通道出图 8.6 秒、802KB, 场景类提示词(空工位、深夜的显示器、对放的椅子)出来的质量可以直接用,没有乱码文字。

复制要带图,就得写富文本剪贴板。 剪贴板里放图片链接没用——链接指向 localhost 且要登录态, 粘到公众号编辑器那边根本取不到。所以「复制」用 ClipboardItem 同时写两种格式: text/html(图转成 data URI 内嵌)给富文本编辑器,text/plain(markdown)给纯文本处。

下载在有图时给自包含的 .html 而不是 .md——.md 存不了图。单个文件、图嵌在里面, 浏览器打开全选复制就能粘进任何编辑器,也是剪贴板那条路走不通时的兜底。

按钮文案会跟着变(「下载图文」/「下载 .md」),而且要在三个时机都刷新: 切版本、首次渲染正文、出完图之后。少刷一个,就会出现"图出好了按钮还写着下载 .md"。

配图挂在版本上:公众号要配图、微博通常不需要,两边该插几张也不一样,所以切版本 = 换一套配图。 插图位是在纯文本上算的再分段渲染——在渲染后的 HTML 里找插入点不可靠,markdown 会改结构。

素材库

产品里最硬的一条规则是「栏目素材是全篇唯一可信的事实来源,不许编造」。 但素材原来每篇现填、填完就丢,不沉淀。素材库让它变成复利。

存的是作者的真实经历、数据、案例、金句、观察(五类),带标签。 写作时按题材召回最相关的几条,拼进 user 消息,措辞和栏目素材一致——模型不需要区分这两个来源。

召回刻意不做向量检索:素材是几十到几百条的量级,字面重合已经够用,而且结果可解释。 中文没有空格分词,按 2 字滑窗取词;标签命中权重加倍(标签是作者自己标的,比正文里的偶然重合可信); 得分低于 3 的不给——宁可不召回,也不塞噪声。上限 6 条,再多会挤占正文该占的上下文。

实测三个查询:「AI 让团队变小了吗」召回带团队/AI数据/AI案例三条,「30分钟家常菜」只召回做菜那条, 「区块链跨境支付」一条都不召回。真跑一篇成稿,三条素材里的具体数字(4.2 小时→47 分钟、8 人走了 4 个、 隔壁部门周报)全都写进了正文。被写进稿子的素材会记一次「用过」,界面上能看出哪些素材真的在干活。

发布数据回填与复盘

整条链路原来到「导出」就断了——哪篇火了、哪个方向表现好,一概不知。 语气档案学的是"你怎么写",这个学的是"哪种写法数据好"。

回填做得很轻:发完手填阅读/点赞/评论/转发/涨粉几个数字加一句备注。 不指望自动抓平台数据——没有开放接口,爬取违反条款,而几个数字的手工成本远低于它带来的信息。 全部留空并保存 = 撤销这次回填(存 null 而不是空对象,复盘时才好过滤)。

复盘按平台 / 栏目 / 方向类型分组。两条统计上的选择:

  • 用中位数不用平均数。自媒体数据长尾极重。实测样例里公众号有一条 18 万的爆款, 中位数 3850,平均数会是 3.2 万——完全误导。
  • 样本少于 3 篇的维度不给结论,只报数量。两三条数据得出的"规律"是噪声。

选题池与排期

热点里看到的机会、推荐里想留的题材,不当场写就丢了。推荐题材卡右上角一个「+」存进池子, 来源会标上(热点 / 推荐 / 手动)。plan_date 空 = 只在池子里;有值 = 排到那天,到期的行标红。 「写这条」把题材填进简报并标记已用,不自动生成——生成要花钱,让人自己按。

刻意不做完整日历:自媒体的排期粒度就是"这周写哪几条"。日期只收 YYYY-MM-DD, 别的一律当空——排期字段被塞进奇怪的值,排序就乱了。

成本看板

付费通道(文案 / 配图)原来没地方看花了多少钱。

先补的是记录:usage_events 原来只记 tokens,配图的张数传的是 0, 计费量根本没存。现在按各自的单位记(units + unit)。

单价表在 server/pricing.js,可以用 PRICING_JSON 环境变量覆盖。 几条克制的处理:算不出来就显示"未计价",不编数字;演示模式的调用不计钱(它照样会记模型名, 因为环境变量里配着);每行标出用了哪个单价、是不是精确匹配到型号,并把免费额度一起列出来。 界面上一律写"估算"——单价会调、有阶梯、有免费额度,真实账单以服务商控制台为准。

提词器

口播提示生成后,旁边多一个「提词器」按钮:全屏深色、大字号、自动滚动,可以直接对着念。

  • 段落用的就是口播提示切好的段,标题、【画面】这类不念的内容自动不出现。
  • 重读的词在正文里高亮成琥珀色;语气/停顿/表情/动作在每段下方用暗色小字跟着走,扫一眼就能看到,不抢正文。
  • 固定阅读线 + 百分比进度,知道念到哪了。
  • 镜像模式给外接提词器玻璃用;提示开关可以只留纯文本。
  • 速度 10–200 px/s、字号 20–96px 都能调,存在 localStorage,下次打开还是你的习惯设置。
  • 上下键直接挪画面(一次一行),念快了往回一点、念慢了往前赶一点,播放中也能挪,挪完接着按新位置滚。滚轮同效;PgUp/PgDn 翻页,Home/End 跳首尾。
  • 其余快捷键:空格 播放/暂停 [ ] 速度 + − 字号 C 提示 M 镜像 R 重来 Esc 退出;点画面任意处也能播放/暂停,录制时不用够按钮。
  • 打开时申请 Wake Lock,录制过程中屏幕不会自己睡过去(浏览器不支持就静默跳过)。

实测(DeepSeek,抖音口播 + 32 岁女产品经理人设):

段落 标注
「那一刻我的心情,比需求被产品驳回还绝望」 语气:带点自嘲的无奈 重读:只剩、绝望 表情:苦笑一下
「30分钟,3个方案」 停顿:说完"快速交付"停半拍 动作:比三根手指
「不然炒出来黏成一坨,那就不叫炒饭,叫浆糊」 表情:说到浆糊时撇嘴(精确到句内位置)
结尾互动句 动作:指一下镜头

6 段里只有 2 段标了动作,重读词全部可在原文定位。

语气学习

成稿页的「喂给账号学习」把这篇作为语气样本交给当前账号(会先弹确认框,只有用户确认才学)。

服务端拿到样本后重新蒸馏出一份语气档案:从最近 6 篇样本里总结 8-14 条可复制的写作习惯—— 句式长度、段落节奏、开头结尾套路、高频词与口头禅、比喻取材偏好、标点用法、刻意回避的表达。 之后这个账号的选题和成稿都会带上这份档案,外加最近 2 篇样本的开头片段做语感锚点(只模仿语感,明确禁止复用内容)。

档案在账号设定弹窗里可见可管:列出所有样本、可单篇删除、可「重新学习」。删样本会用剩下的重新学一遍,删光则档案清空。 样本上限 30 篇,单篇 100–20000 字。

实测(DeepSeek,喂了一篇备菜笔记后)学出来的档案包括「开头习惯用具体场景切入,直接交代时间、事件和感官细节」 「结尾常以『今晚就去……』收束」「比喻取材自日常烹饪,爱把抽象感受具象化」——都是能照着写的具体特征。

热点板块

抓全网榜单,和当前账号的定位逐条比对,只留下这个号真能接得住的,每条给出原文章 / 蹭热点的点 / 选题建议,一键把选题带回创作简报。

榜单来源

server/hotspots.js 里每个源独立抓取、独立超时、独立失败,UI 上逐个显示状态:

源 类型 实测
微博热搜 社会热点 ✅ 需带 Referer
百度热搜 社会热点 ✅ HTML 内嵌 JSON
抖音热榜 短视频 ✅
今日头条 资讯 ✅
36氪热榜 科技商业 ✅ 对科技/商业类账号最有用
B 站排行 视频内容 ⚠️ 常被风控限流,失败会明确显示原因

这些都是各平台未公开承诺的接口,随时可能变或被限流。 所以:结果 20 分钟内存缓存、不做轮询、一个源挂了不影响其它源;全挂了还有「手动粘一份榜单」兜底(一行一条,自动去掉序号)。

原文概要

命中的几条会补一份原文概要,来源如实标注,绝不用模型脑补的"背景"冒充:

标注 含义 覆盖情况
榜单摘要 榜单接口自带的真实摘要 百度热搜的条目都有,内容相当完整
抓取原文后概括 真抓到了正文,再压成 2-3 句 少数不设反爬的站点
抓不到 界面直说「抓不到原文概要,动笔前请点开原文核实」 多数情况

主流平台的原文基本抓不到——36氪 是火山引擎安全检测、百家号/头条是验证页或要 JS、微博抖音的链接本来就是搜索页不是文章。所以概要是 best-effort:能拿到就给真的,拿不到就说没有,每条最多试抓 3 次、有反爬特征页直接判定失败。

不适合蹭的,代码层面直接挡掉

伤亡灾难、犯罪司法、战争冲突、疫病健康危机、点名到个人的争议、明星私生活——这六类在送进模型之前就被关键词规则过滤掉了,模型根本看不到。

这不是多此一举:实测只在提示词里写红线时,模型仍然会把「某人要求某人道歉」挑成关联度最高的一条,还会自行脑补标题里没有的背景。所以现在是三道闸:

  1. 前置过滤——risky 的条目不进候选池(server/hotspots.js 的 RISK_RULES)
  2. 提示词红线——告诉模型漏网的也要自己跳过,并且只能基于标题字面判断、不许脑补背景
  3. 后置校验——模型的选择再过一遍同样的规则,同时丢掉它自己标为「关联度弱」的(那基本是凑数)

过滤是关键词规则,不可能完美——所以界面上写明了「比对只看得到榜单标题,看不到原文,动笔前请点开原文核实」,并显示这一轮挡掉了多少条。

内容栏目

一个账号下可以分几条内容线,比如「来时路」讲这个号是怎么来的、为什么是我在做、和别人的不一样在哪;「生活日常」记录和定位相关的真实片段。

栏目就在创作简报里,题材框下面一排 chip:默认「常规创作」(不指定栏目,按账号通用定位写),点一下切到某个栏目;右侧「管理栏目」打开增删改的小弹窗。放在创作里而不是账号设定里,是因为选栏目是每篇都要做的动作,不是配置一次就不管的事。

七个起手模板(来时路 / 生活日常 / 干货教程 / 行业观察 / 答读者问 / 避坑指南 / 幕后故事),每个都带好了「讲什么」和「写作要点」,选中即预填,可以改;也可以完全自定义。单账号最多 12 个,已用过的模板不再出现在下拉里。

每个栏目要什么素材,自己说了算

不同栏目需要的信息不一样:「来时路」要你的背景经历和为什么开始做这个号,「避坑指南」要你踩过的坑和实际代价,「答读者问」要读者的原问题。所以每个栏目自带一份素材清单(最多 8 项,可标必填),在栏目编辑器里增删改;模板都预置好了。

选中该栏目创作时,简报里就会出现对应的输入框问你要这些素材。必填没填前端直接拦住(给出「先补上:你的背景经历、为什么开始做这个号」这样的提示,不发请求、不浪费调用),服务端也会再校验一次,并且只收该栏目声明过的字段。

素材会拼成「—— 作者为这个栏目提供的素材 ——」块进提示词,措辞很硬:

这些是作者本人提供的真实信息,是这篇里唯一可信的事实来源:写作时优先用它们,展开成有细节的段落而不是原样复述;不要编造它们之外的个人经历、时间、地点、数字、职位或人物关系。作者没填的字段说明他没提供这部分信息——不要脑补。

这条约束是这个功能的重点:没有素材时,模型写「来时路」只能靠编。实测(DeepSeek,喂了背景经历 / 开始做号的原因 / 差异化三项)成稿把 2013 年离职、12 年、40 多家公司、8 人到 3000 人、2024 下半年、70% 重复内容全部用上并展开成了叙事,没有多出一条编造的个人事实;留空的「想让读者记住你什么」也没有被脑补成句子。

栏目会拼成「—— 本篇所属栏目 ——」块进提示词,选题和成稿两步都带,并且写明「栏目要点和通用平台规范冲突时以栏目要点为准」。选题提示词里也加了一条:三个方向都必须是这个栏目该有的样子,「来时路」不要写成干货教程。综合检测的「账号调性」维度也会对照栏目,写跑偏了会被标出来。

实测(DeepSeek,同一个账号、同一个题材「AI 与组织这件事」):

栏目 出来的三个方向
来时路 「我做了 10 年组织咨询,AI 来了,我第一次觉得工具比人重要」「从给客户做方案到自己被 AI 逼着改,我这三年经历了什么」——全是第一人称的来历与转折
干货教程 「先看看你的组织是不是 AI-ready」「AI 落地,最先改的不是技术,是 HR 的招聘 JD」——全是可照做的清单和方法

为后续留的口子:栏目是独立的表(sections),不是塞在账号 JSON 里的字段。所以之后要给单个栏目挂自己的语气样本、自己的题材推荐、发布配额或排期,加列或加关联表即可,不用动已有结构。草稿上同时存了 section_id 和栏目快照,删栏目不会影响历史稿件。

题材推荐

选好账号进到创作简报,题材框下面默认给三条题材,点一条就填进去,也可以完全不用、自己写。

简报里的发布平台 / 风格调性 / 目标读者 / 目标字数默认按账号设定走,只展示不可改,点「修改」才展开成表单,改完还能一键「恢复账号设定」。目标读者与账号目标用户一字不差时不会重复写进提示词,只有本篇收窄了才会额外说明。

三条的构成:匹配度高的热点占前两位(按关联度强→中排序,带 🔥 标记、平台、原文标题和概要),剩下的位置由「这个号还没写过的」常规题材补齐。留一个常规位是有意的——不让热点把常青选题挤没。热点条目取自该账号 24 小时内的比对结果,过期就不再顶上来。

  • 推荐依据账号定位、目标用户、要解决的问题,以及博主本人的行业/岗位/性格——优先挑「这个号真写得出来、别人写不出来」的题目。
  • 去重:生成时会把这个账号历史创作的题材和成稿标题全部作为排除清单交给模型,要求不只是字面不同、实质主题重合的也要换掉;当前挂着的三条也算「已提过」,所以「换一批」不会换汤不换药。
  • 推荐结果缓存在 personas.subject_ideas,打开页面直接显示不必等模型;用掉一条服务端就把它划掉,列表空了会自动补一批(所以「默认给三条」始终成立)。
  • 自动补货失败过的账号,本次会话不再自动重试,避免没配密钥时反复弹错。

创作时引用热点

从热点带过来的题材会在简报里挂一条借势提示条(原文标题 + 链接 + 概要),可以随时点 × 取消借势。这条热点会随草稿一起存下来,选题和成稿两步都带上,提示词里明确要求:

  • 在开头或前三分之一处把热点当由头引出来,不要写成新闻转述,也不要通篇围着它转——它是引子,账号自己的观点才是主体。
  • 有概要时:只能用概要里给出的信息,概要没说的细节、数字、人名、时间一律不许写。
  • 没概要时:明确告诉模型「你不知道这条热点的具体内容」,只能写到「最近大家在讨论 XX」的程度,不许编造事件细节。
  • 一律不许写「据报道」「有数据显示」这类看似有出处、实则没有的表述。

实测(DeepSeek):有概要那条,成稿开头准确复述了概要里的 2万/50公里/三四平米/四次/十多名,没有多出一个数字;没概要那条,开头是「最近大家都在讨论一个话题:跳槽去机器人行业赚快钱」,没有编造原文细节,也没有伪出处措辞。

管理后台

独立入口 /admin,独立的管理员账号体系,和业务前台完全分开:

  • 前台导航里没有管理入口,前端代码也是分开的两套(admin.html + admin.js)。
  • 管理员存在自己的表 admins,用自己的 cookie(cw_admin,12 小时)。业务用户的 cookie 进不了后台,管理员的 cookie 也进不了业务接口,两边互相不认。
  • 业务接口返回的用户对象里没有任何管理员标记。

第一次进后台

打开 /admin,如果一个管理员都没有,页面会让你当场创建第一个(用户名 + 至少 8 位密码)。创建完这个初始化入口就永久关闭了,再调 POST /api/admin/setup 返回 403。之后统一走登录。

密码不落在 .env 里,也不需要预先配置任何环境变量。

这个「首次访问即设置」的引导适合自托管场景。如果要暴露到公网,请在部署完成后立刻完成初始化,或在反向代理层对 /admin 加一层访问控制。

三块内容

使用概览 —— 用户数、创作总数、模型调用次数与成功率、Token 合计(入/出分开)、平均耗时,加一条按天的调用走势。可切近 1 / 7 / 30 / 90 天。

用户表 —— 每个用户的注册时间、账号数、栏目数、创作数、已完成数、语气样本数、调用次数、Token 消耗、最近活动。下面还有一张管理员账号表(创建时间、最近登录)。

各功能用量 + 提示词 —— 按功能(选题方向 / 成稿 / 划词改写 / 续写 / 成稿检查 / 题材推荐 / 热点比对 / 原文概要 / 语气档案)统计调用次数、成功率、进出 token、平均耗时;最近的失败记录单独列出。下面折叠展示每个功能实际发给模型的 system 提示词全文,带 JSON Schema 和逐个动作的指令(扩写/专业点/轻松点…),只读。

用量是真的记的

之前什么都没记。现在每次模型调用都会落一条 usage_events:功能、provider、模型、成败、耗时、输入/输出 token、错误信息。

Token 数取自上游返回的真实用量,不是估算:非流式读 usage.prompt_tokens,流式请求加了 stream_options: {include_usage: true},DeepSeek 会在最后一个 chunk 里带上用量;Anthropic 通道读 finalMessage().usage。埋点写在 llm.js 的 tracked() 里,成功失败都记,落库函数由 index.js 注入,所以 llm.js 不依赖 db。

实测一次成稿:成稿 deepseek-chat ok 15265ms 入721 出1358。

创作记录归档

侧栏创作记录分「进行中 / 已归档」两个视图,各带条数。归档只是收起来,不删任何东西:

  • 每条记录 hover 出现 📥 归档 / × 删除;在已归档视图里 📥 变成 ↩ 恢复。
  • 底部有一键「把 N 篇已完成的收起来」,只收 status = done 的,写了一半的不会被扫走;当前是哪个账号视图就只归档哪个账号的,不会误伤别的号。没有可归档的就自动隐藏这个按钮。
  • 归档的稿子照常能打开、能编辑;它的题材仍然参与「推荐题材」的去重——写过就是写过,归档不等于没写。

数据上是 drafts.archived_at(空字符串表示进行中),不是删除标记,也不影响账号/栏目下的稿件计数。

两步生成的设计

  1. 选题(POST /api/drafts/topics)——提示词要求三个方向在切入角度上互相错开(亲历 / 方法拆解 / 反常识 / 对比 / 行业视角),而不是换标题措辞;若绑定了账号,三个方向都必须落在账号定位之内。每个方向返回:标题、切入角度、开篇钩子、3-5 条内容骨架、适配读者。Claude 通道用 JSON Schema 约束结构,其它通道用 json_object + 提示词约束,并做容错解析。
  2. 成稿(POST /api/drafts/{id}/content)——把简报 + 选定方向的骨架一起喂给模型,SSE 逐字回传;写完落库,状态置为 done。

平台规范和风格调性集中在 server/prompts.js,这是调优质量最主要的地方。内置平台:小红书 / 公众号 / 知乎 / 抖音口播 / 微博 / B 站;调性 6 种。

目录

server/
  index.js    HTTP 服务与路由(node:http,无框架)
  routes.js   API 处理函数
  db.js       node:sqlite 数据层(users / sessions / admins / admin_sessions /
              personas / sections / style_samples / drafts / usage_events)
  auth.js     scrypt 密码 + httpOnly cookie 会话(业务与管理各一套,互不相认)
  llm.js      模型接入层(anthropic / openai / mock)
  prompts.js  平台规范、调性、账号/人设/语气块、改写与续写指令、JSON Schema
  hotspots.js 榜单抓取(6 个源,独立容错)与不适合蹭的硬过滤规则
  mock.js     演示模式模板
public/       前台单页(index.html + app.js)与后台(admin.html + admin.js),共用 styles.css,无构建步骤

API

方法 路径 说明
POST /api/auth/register /api/auth/login /api/auth/logout 账号
GET /api/me /api/meta 当前用户 / 平台·调性·模型信息
GET /api/admin/session 后台登录态 + 是否需要首次设置
POST /api/admin/setup 创建第一个管理员(仅在一个都没有时开放)
POST /api/admin/login /api/admin/logout 管理员登录 / 登出
GET /api/admin/overview 用户、用量统计(?days=),需管理员会话
GET /api/admin/prompts 各功能的 system 提示词与 Schema,需管理员会话
GET POST /api/personas 账号设定列表 / 新建
PUT DELETE /api/personas/:id 修改 / 删除账号设定
GET /api/hotspots 全网榜单(20 分钟缓存,?force=1 强刷)
GET POST /api/personas/:id/hotspots 读上次比对结果 / 重新比对(可传 manual 用手动榜单)
GET POST /api/personas/:id/subjects 读推荐题材缓存 / 重新生成一批
POST /api/drafts/topics 提交简报(带 persona_id),返回 3 个方向
POST /api/drafts/:id/topics 换一批(会避开已出过的角度)
POST /api/drafts/:id/content 选定方向,SSE 流式返回成稿
PUT /api/drafts/:id/content 保存编辑器里改过的正文
POST /api/drafts/:id/assist 划词改写 / / 续写,SSE 流式返回
POST /api/drafts/:id/review 两层检查:语言错误 + 调性风险
POST /api/drafts/:id/cues 生成口播提示(语气/重读/停顿/表情/动作)
GET POST /api/personas/:id/sections 栏目列表(含模板)/ 新建
PUT DELETE /api/personas/:id/sections/:sid 修改 / 删除栏目
GET POST /api/personas/:id/samples 语气样本列表 / 新增(可传 draft_id)
DELETE /api/personas/:id/samples/:sid 删除样本并重学
POST /api/personas/:id/digest 重新蒸馏语气档案
GET /api/drafts /api/drafts/:id 历史列表(?persona_id= 按账号筛选,none 为未绑定;?archived=1 看归档)/ 详情
POST /api/drafts/:id/archive 归档 / 恢复单条
POST /api/drafts/archive-done 批量归档当前范围内已完成的
DELETE /api/drafts/:id 删除

第一版的边界

  • 会话存在 SQLite,密码用 scrypt;生产部署需再加 HTTPS、限流与注册准入。
  • 管理后台只读,没有改密码、封号、调额度这类操作;也没有在后台里增删管理员(第一个之后要加人得直接写库)。
  • 用量按调用计数和 token 记,没有换算成金额(各家单价不同,也会变)。
  • 未做协作、团队空间、定时发布与平台直发。
  • 结构化生成(三个方向、三条题材)偶发格式跑偏时会自动重试一次,两次都失败才报错;失败不会在历史里留空记录。
  • 口播提示只标不改;正文一改就作废需要重新生成。
  • 提词器是匀速滚动,没有做跟读语音识别式的自动对齐。
  • 栏目素材是每篇现填的,没有做「上次填过的自动带出来」——同一个栏目写第二篇时要重填。
  • 尚无多版本对比与历史回滚:编辑器里的改动是就地覆盖,只能靠「重来一次」重新生成。
  • 语气档案是全量重蒸馏,样本多了以后每次增删都要重算一遍(当前上限 30 篇,成本可接受)。
  • 划词菜单在阅读模式下靠文本反查定位,跨越加粗/链接等行内格式的选区需要切到编辑模式。
  • 热点抓的是非官方接口,平台改版或限流就会失效;风险过滤是关键词规则,会有漏网也会有误杀,只能算一道兜底而不是保证。
  • 成稿检查会指出没出处的数字和机构名(事实存疑维度),但不判断真假——核实还得自己来。
  • 每次出稿会多一次检查调用(输入是全文、输出是短 JSON,开销不大,但确实是一次额外请求)。
  • 热点比对时模型只看得到标题(概要是命中之后才补的),所以「蹭点」是基于标题的推断,动笔前必须点开原文核实。
  • 原文概要覆盖率有限(主流平台反爬),多数条目只有标题;这时成稿只会把热点用作一句由头。

About

No description, website, or topics provided.

Resources

Stars

55 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages