markdown-ai-creator
v0.3.1
Published
AI-driven original content creation from one or more Markdown sources. Designed for higher-tier users who want fresh writing instead of paraphrasing.
Maintainers
Readme
markdown-ai-creator
AI-driven original content creation from one or more Markdown sources. Designed for higher-tier users who want fresh writing instead of paraphrasing.
markdown-ai-creator 是 markdown-ai-rewriter 的「上一档」—— 不做改写,直接基于素材重新创作一篇有判断、有立场、能溯源的博客草稿。
何时用 creator vs rewriter
| 场景 | 推荐包 |
|---|---|
| 想保留原文结构,只改表达(去 AI 味、去版权痕迹) | markdown-ai-rewriter |
| 想从 N 份素材里合成一篇新文章,有自己的角度和判断 | markdown-ai-creator |
定位上,creator 适合 pro / ultra / admin 等付费档;rewriter 留给免费 / 入门档。
安装
npm install markdown-ai-creator要求 Node ≥ 18(用了 native fetch 和 ESM)。
最小调用
import { generateOriginalDraft } from 'markdown-ai-creator';
const result = await generateOriginalDraft({
sources: [
{
url: 'https://example.com/article',
fullMarkdown: '# 原文标题\n\n原文正文...'
}
],
provider: 'deepseek',
apiKey: process.env.DEEPSEEK_API_KEY!
});
console.log(result.contentMarkdown);
console.log(`tokens=${result.totalTokens} images=${result.imagesRestored}/${result.imagesDetected}`);完整参数
interface CreatorInput {
// 必填
sources: SourceMaterial[]; // 至少 1 个,每个有 fullMarkdown
provider: 'minimax' | 'deepseek' | 'openai' | 'anthropic' | 'gemini';
apiKey: string;
// 可选
title?: string; // 不传则让 LLM 自行拟题
/**
* v0.3.0 新增。默认 false(title 当 brief,LLM 可润色)。
* true 时强制 LLM 用原 title,并在 sanitize 后做一次保险替换 H1。
* 必须搭配非空 title,否则抛 INVALID_INPUT。
*/
lockTitle?: boolean;
angle?: WritingAngle; // 6 选 1,默认 'custom'
persona?: PersonaInput | null; // null = 中立模式
customInstruction?: string; // 本次额外要求
targetLength?: { min: number; max: number }; // 不传按素材总长动态推断
model?: string; // 不传走 provider 默认值
temperature?: number; // 默认 0.7
maxTokens?: number; // 默认 4000
preserveImages?: boolean; // 默认 true
baseUrl?: string; // OpenAI 兼容自定义网关
verbose?: boolean;
imagePolicy?: ImagePolicy; // v0.2.0+ 图片策略,详见下方
/**
* v0.3.0 新增。'auto'(默认) | 'always' | 'never':
* - auto:LLM 一份 source URL 都没引用时,文末自动追加 "原文:[…](url)" 列表
* - always:永远追加
* - never:完全关闭
*/
appendSourceFooter?: 'auto' | 'always' | 'never';
}v0.3.0 关键能力
lockTitle —— 锁死最终标题
热搜稿、SEO 稿、命题作文场景,标题不能被 LLM 改:
const result = await generateOriginalDraft({
sources: [...],
provider: 'minimax',
apiKey,
title: '小米 SU7 Ultra 明天发布,售价 81.49 万', // 必填
lockTitle: true // 锁死
});
// 第一行 H1 一定是 '# 小米 SU7 Ultra 明天发布,售价 81.49 万'
// 即使 LLM 加了副标题 / 改了写法,包内会强制改回去
// 改写时会冒一条 warning:'lockTitle: H1 mismatch was rewritten to "..."'实现是双保险:
- prompt 改成"必须完全照抄"硬约束
- sanitize 后扫第一行 H1,不一致就强制改写(标点差异容忍)
appendSourceFooter —— 防止 LLM 漏链接
LLM 偶尔会"忘记"在文中引用 source URL。auto 模式(默认)会扫一遍最终 markdown,
所有 source URL 都没出现时,在文末自动追加:
---
原文:
- [素材的 briefingTitle](https://...)
- [...](...)并冒一条 warning 给调用方知道。always / never 让你完全控制。
Source truncation warnings
当某份 source 的 fullMarkdown 超过 12000 字(prompt 内置阈值)会被截断。
v0.3.0 起会把这条信息冒到 result.warnings:
source[0] (https://example.com/long): truncated from 25430 to 12000 chars调用方可以据此提示用户"素材太长,可能丢失尾部信息"。
Provider 默认模型
| Provider | Model | Base URL | 备注 |
|---|---|---|---|
| minimax | MiniMax-M2.7-highspeed | https://api.minimaxi.com/v1 | M2.7 系列的低延迟变体(同写作档位、首 token 更快,CLI 阻塞调用最划算);要极限质量传 model: 'MiniMax-M2.7',要压成本传 'abab6.5s-chat' |
| deepseek | deepseek-chat | https://api.deepseek.com/v1 | 性价比首选 |
| openai | gpt-4o-mini | https://api.openai.com/v1 | 平衡成本 + 中文质量 |
| anthropic | claude-3-5-sonnet-20241022 | (Anthropic 原生 API) | 长文写作最强 |
| gemini | gemini-1.5-flash | OpenAI-compatible 网关 | 免费额度大 |
minimax 走 MiniMax 官方的 OpenAI-compatible endpoint,和 markdown-ai-rewriter 共用同一个 MINIMAX_API_KEY——服务端只需配置一次。Creator 引擎的目标是"原创合成"而不是"逐段改写",所以默认选了 MiniMax-M2.7-highspeed(M2.7 系列的低延迟变体)。如果你的 minimax 账户没有 M-2 系列权限或想压成本,传 model: 'abab6.5s-chat'(老 abab 系列);想极限质量传 model: 'MiniMax-M2.7'。海外节点请通过 baseUrl 覆盖到 https://api.minimax.io/v1。
任意 OpenAI 兼容的网关(自建中转 / openrouter 等)传 provider: 'openai' + baseUrl 即可。
写作角度(angle)
| Angle | 说明 |
|---|---|
| fact_judgment | 先讲事实,再给"我的判断" |
| comparison | 横向对比多个 source |
| timeline | 用时间线 / 事件链重组信息 |
| translate | 把英文 / 论文翻译并给中文工程师听 |
| hands_on | "我装了一遍 / 跑了一遍"的口吻 |
| custom | 兜底 —— 综合素材写完整起承转合(默认) |
Persona(人设)
不传 / 传 null → 走中立模式:克制、有判断、面向中文工程师/产品读者。
传 PersonaInput 时支持注入:
name/positioning/styleDescriptionmustHaveElements(每篇必带要素)bannedWords(禁用词)preferredKeywords(自然处可提到的关键词)openingSignatureMarkdown/signatureMarkdown(文首/文末签名 —— LLM 偶尔会忘,包内会兜底拼一次)
长度推断(targetLength)
不传时按素材总字符数(去除空白)动态推断:
| 素材总字符 | 输出字数区间 | |---|---| | <800 | 300–600 | | 800–3000 | 600–1200 | | 3000–8000 | 1000–1800 | | >8000 | 1500–2500 |
并且输出永远 ≤ 素材的 60%,强制 LLM「提炼」而不是「注水」。
图片占位符保护
包内默认 preserveImages: true,会:
- 把素材里每张
替换成{{IMAGE_ANCHOR_001}}、{{IMAGE_ANCHOR_002}}... - 在 prompt 里硬约束 LLM「占位符必须原样保留」
- 拿回结果后还原占位符为原始图片语法
- 如果 LLM 漏了占位符,启用 4 级修复策略(关键词 → 位置比例 → 相邻锚 → 文末兜底)
返回值的 imagesDetected / imagesRestored / warnings 反映保护效果。如果你不想保护图片(比如想让 LLM 自己决定要不要带图),传 preserveImages: false。
图片处理策略(v0.2.0+)
把图片"原样保留"之外,本包还可以为每张图执行三种动作之一:
| Strategy | 行为 | 何时用 |
|---|---|---|
| preserve | 原样保留 | 默认;图片来源可信、不会过期 |
| caption-only | 替换成 *图:{alt}* 注脚 | 图片 404 / 反爬挡 / 被识别为水印图、文字截图 |
| regenerate | 调 image API 重新生成(text-to-image) | 想替换成无版权风险、风格统一的原创图 |
import { generateOriginalDraft } from 'markdown-ai-creator';
const result = await generateOriginalDraft({
sources: [...],
provider: 'minimax',
apiKey: process.env.MINIMAX_API_KEY!,
imagePolicy: {
// 默认对所有图采取的动作
default: 'preserve',
// 给特定 url 的精确指示,优先级高于 default
perImage: {
'https://watermarked.example.com/img1.jpg': 'caption-only',
'https://watermarked.example.com/img2.jpg': 'regenerate'
},
// regenerate / generateCover 必填
provider: 'minimax',
apiKey: process.env.MINIMAX_API_KEY!,
imageModel: 'image-01', // 默认即此值,可省
bodyAspectRatio: '16:9',
// 单独再生成一张封面图
generateCover: true,
coverAspectRatio: '16:9',
coverStyle: '现代简洁、信息密度低、视觉冲击力强'
}
});
console.log(result.contentMarkdown);
console.log(result.coverImageUrl); // 24h 过期的临时 URL(minimax)
console.log(result.imageActions); // 每张图的处理审计关于决策来源:本包不做 OCR / 水印检测 / 视觉判断 —— 那些会让包从 26KB 撑到几 MB,且需要原生依赖。语义判别留给上层(比如 web-publisher 的 api 服务里有独立的 image-classifier),把分类结果通过 imagePolicy.perImage 喂进来即可。
关于 URL 寿命:MiniMax image-01 返回的 URL 24h 过期,OpenAI DALL-E 3 返回的 URL 1h 过期。caller 务必下载 + 转存到稳定图床(比如微信公众号素材库),不要直接把这个 URL 长期保存到 DB。
关于 regenerate 失败的兜底:默认 fallbackOnRegenerateFail: true —— 单图 regenerate 失败会自动退化为 caption-only,不阻断整篇文章生成。把这个值设成 false 才会保留原图 + 写一条 warning。
关于支持的 image provider:
| Provider | Endpoint | 默认 model | aspect_ratio |
|---|---|---|---|
| minimax | https://api.minimaxi.com/v1/image_generation | image-01 | 1:1 / 16:9 / 4:3 / 3:2 / 2:3 / 3:4 / 9:16 / 21:9 原生支持 |
| openai | https://api.openai.com/v1/images/generations | dall-e-3 | 内部映射到 1024×1024 / 1792×1024 / 1024×1792 |
错误处理
所有错误都是 CreatorError,按 code 路由:
import { CreatorError } from 'markdown-ai-creator';
try {
await generateOriginalDraft(input);
} catch (err) {
if (err instanceof CreatorError) {
switch (err.code) {
case 'INVALID_INPUT': /* → 400 */ break;
case 'MISSING_API_KEY': /* → 401 / 配置错 */ break;
case 'LLM_FAILED': /* → 502 */ break;
case 'LLM_EMPTY_RESPONSE': /* → 502 */ break;
case 'IMAGE_PROTECTION_FAILED': /* → 500 */ break;
}
}
}不做内置 retry —— 重试责任在 caller(前端「重新生成」按钮 / pipeline 重试逻辑)。
开发
npm install
npm run typecheck
npm test # node --test,纯函数单测,不调真 LLM
npm run build # 输出到 dist/设计取舍
为什么不直接用 markdown-ai-rewriter 的 full mode?因为 rewriter 的 system prompt 是写死的「改写不改本意」,对原创合成场景反而是约束。creator 把 system prompt 调成「你是博主,请基于素材写一篇」,温度更高,鼓励有判断有立场的输出。
~~为什么不引 minimax?~~ v0.1.1 加了 minimax 支持。MiniMax 官方提供 OpenAI-compatible chat completions endpoint,可以直接走现有 OpenAI-compatible 调用路径,不增加额外的依赖;同时让 markdown-ai-rewriter 和 markdown-ai-creator 复用同一份 MINIMAX_API_KEY。
为什么 v0.1 不做流式?一篇博客几秒生成完,前端 toast loading 即可;流式有 token 拼接 + 错误恢复成本,留到后续 PR 看是否真有需求。
License
MIT © Ping Si
