@openschool_01/skills 0.1.1 → 0.1.2

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.
@@ -11,6 +11,7 @@ const tar = require("tar");
11
11
  const CLI_ROOT = path.resolve(__dirname, "..");
12
12
  const REPO_ROOT = path.resolve(CLI_ROOT, "..", "..");
13
13
  const BUNDLED_REGISTRY_PATH = path.join(CLI_ROOT, "registry", "skills-registry.json");
14
+ const BUNDLED_SKILLS_ROOT = path.join(CLI_ROOT, "bundled-skills");
14
15
  const WORKSPACE_REGISTRY_PATH = path.join(REPO_ROOT, "registry", "skills-registry.json");
15
16
  const NPM_EXECUTABLE = process.platform === "win32" ? "npm.cmd" : "npm";
16
17
 
@@ -116,6 +117,12 @@ function writeSourceMeta(targetDir, meta) {
116
117
  }
117
118
 
118
119
  function getLocalSkillDirectory(entry) {
120
+ const bundledSkillDir = path.join(BUNDLED_SKILLS_ROOT, entry.slug, "skill");
121
+
122
+ if (fs.existsSync(bundledSkillDir)) {
123
+ return bundledSkillDir;
124
+ }
125
+
119
126
  if (!entry.localPackageDir) {
120
127
  return null;
121
128
  }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@openschool_01/skill-wechat-assistant",
3
+ "version": "0.1.0",
4
+ "description": "OpenSchool curated skill: wechat assistant.",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "files": [
10
+ "skill"
11
+ ]
12
+ }
@@ -0,0 +1,19 @@
1
+ # OpenSchool 收录说明
2
+
3
+ - 收录名称:公众号助手
4
+ - 收录方式:OpenSchool 严选整合
5
+ - 当前版本:纯内容版
6
+
7
+ 当前版本整合了公众号写作流程里最常用的三块能力:
8
+
9
+ 1. 标题方向整理
10
+ 2. 正文写作约束
11
+ 3. 微信 HTML 渲染与质量检查
12
+
13
+ 本版本刻意不包含:
14
+
15
+ - 生图
16
+ - 封面生成
17
+ - 微信 API 发布
18
+
19
+ 这样可以先保证“写得出、改得动、落得下、能预览”这条链路稳定。
@@ -0,0 +1,109 @@
1
+ # 公众号助手
2
+
3
+ 适用于把一个选题整理成可发布到微信公众号的完整内容稿。
4
+
5
+ 当前版本专注 4 件事:
6
+
7
+ 1. 明确选题和读者视角
8
+ 2. 生成更像公众号风格的标题方向
9
+ 3. 写出 Markdown 正文
10
+ 4. 渲染成微信公众号可用的 HTML
11
+
12
+ 这不是一个“全自动发布器”。当前版本不负责:
13
+
14
+ - 生图
15
+ - 封面生成
16
+ - 微信草稿箱发布
17
+
18
+ ## 什么时候用
19
+
20
+ 当用户要做这些事时使用:
21
+
22
+ - 写公众号文章
23
+ - 把直播内容整理成公众号稿
24
+ - 生成公众号标题
25
+ - 输出微信公众号排版版式的 HTML
26
+ - 检查文章是否适合公众号发布
27
+
28
+ ## 推荐工作流
29
+
30
+ ### 1. 先收集输入
31
+
32
+ 至少确认这 4 个信息:
33
+
34
+ - 文章主题
35
+ - 目标读者
36
+ - 读者当前最常见的问题
37
+ - 这篇文章承诺解决什么
38
+
39
+ ### 2. 先给标题方向
40
+
41
+ 优先输出 3 到 5 个标题,并解释它们分别适合:
42
+
43
+ - 偏方法拆解
44
+ - 偏观点判断
45
+ - 偏案例复盘
46
+
47
+ 标题规则见:
48
+
49
+ - `references/title-guidelines.md`
50
+
51
+ ### 3. 再写正文
52
+
53
+ 正文默认要求:
54
+
55
+ - 使用 4 到 6 个二级标题
56
+ - 先给判断,再展开解释
57
+ - 至少包含 1 个真实场景
58
+ - 至少包含 1 个常见误区
59
+ - 至少包含 1 组可执行动作
60
+ - 结尾自然引向下一篇或下一步动作
61
+
62
+ 写作约束见:
63
+
64
+ - `references/writing-rules.md`
65
+
66
+ ### 4. 渲染 HTML
67
+
68
+ 正文 Markdown 完成后,运行:
69
+
70
+ ```bash
71
+ python scripts/render_wechat_html.py --input "<article.md>" --output "<article>.html"
72
+ ```
73
+
74
+ ### 5. 做一次质量检查
75
+
76
+ 如果用户在意发布质量,再运行:
77
+
78
+ ```bash
79
+ python scripts/audit_article_quality.py --input "<article.md>"
80
+ ```
81
+
82
+ ## 目录说明
83
+
84
+ - `references/title-guidelines.md`
85
+ 标题风格和筛选规则
86
+ - `references/writing-rules.md`
87
+ 正文写作规则和结构要求
88
+ - `assets/wechat-article-template.html`
89
+ 微信 HTML 模板
90
+ - `config/wechat-style-config.json`
91
+ 样式配置
92
+ - `scripts/render_wechat_html.py`
93
+ Markdown 转微信 HTML
94
+ - `scripts/audit_article_quality.py`
95
+ 质量检查脚本
96
+
97
+ ## 交付要求
98
+
99
+ 默认交付:
100
+
101
+ - 3 到 5 个标题备选
102
+ - 1 篇 Markdown 正文
103
+ - 1 个 HTML 文件
104
+
105
+ 如果脚本已经执行,还要明确告诉用户:
106
+
107
+ - Markdown 文件路径
108
+ - HTML 文件路径
109
+ - 质量检查结果
@@ -0,0 +1,26 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{TITLE}}</title>
7
+ <style>
8
+ {{STYLE}}
9
+ </style>
10
+ </head>
11
+ <body>
12
+ <main class="wx-wrap">
13
+ <article class="wx-article">
14
+ <header class="wx-header">
15
+ <h1 class="wx-title">{{TITLE}}</h1>
16
+ {{SUBTITLE_BLOCK}}
17
+ </header>
18
+ <section class="wx-content">
19
+ {{CONTENT}}
20
+ </section>
21
+ {{CTA_BLOCK}}
22
+ {{FOOTER_BLOCK}}
23
+ </article>
24
+ </main>
25
+ </body>
26
+ </html>
@@ -0,0 +1,7 @@
1
+ {
2
+ "bodyFontSize": 16,
3
+ "bodyLineHeight": 1.8,
4
+ "contentWidth": 720,
5
+ "accentColor": "rgb(0,128,255)",
6
+ "textColor": "#111111"
7
+ }
@@ -0,0 +1,48 @@
1
+ # 标题规则
2
+
3
+ 公众号标题优先追求“清楚、可信、能打开”,不是追求夸张。
4
+
5
+ ## 默认输出 3 类标题
6
+
7
+ ### 1. 观点判断型
8
+
9
+ 适合表达一个明确判断。
10
+
11
+ 示例结构:
12
+
13
+ - 为什么我现在更建议先做 X,再做 Y
14
+ - 真正拉开差距的,不是 X,而是 Y
15
+
16
+ ### 2. 方法拆解型
17
+
18
+ 适合教程、流程、经验总结。
19
+
20
+ 示例结构:
21
+
22
+ - 我现在做公众号内容,基本只用这 4 步
23
+ - 从选题到成稿,我现在固定这样写公众号
24
+
25
+ ### 3. 复盘案例型
26
+
27
+ 适合直播复盘、项目拆解、过程型内容。
28
+
29
+ 示例结构:
30
+
31
+ - 这场直播做完后,我把整个流程又重搭了一遍
32
+ - 我复盘了今天的内容链路,发现问题不在写作本身
33
+
34
+ ## 标题筛选规则
35
+
36
+ - 不要震惊体
37
+ - 不要故意制造焦虑
38
+ - 不要只有概念没有对象
39
+ - 要让读者一眼知道这篇内容大概讲什么
40
+ - 最好能带一个具体场景、对象或判断
41
+
42
+ ## 输出建议
43
+
44
+ 默认输出 3 到 5 个标题,并补一句:
45
+
46
+ - 哪个更适合当前主题
47
+ - 哪个更适合偏增长
48
+ - 哪个更适合偏专业感
@@ -0,0 +1,40 @@
1
+ # 写作规则
2
+
3
+ ## 默认结构
4
+
5
+ 1. 开头先给判断
6
+ 2. 解释为什么今天值得讲这件事
7
+ 3. 讲清楚这件事到底是什么
8
+ 4. 讲清楚为什么大家容易做错
9
+ 5. 给出一套能直接执行的方法
10
+ 6. 用边界和下一步收尾
11
+
12
+ ## 文字要求
13
+
14
+ - 用连续叙述,不写成培训提纲
15
+ - 默认 1600 到 2800 字
16
+ - 使用 4 到 6 个 `##` 二级标题
17
+ - 每节至少两段自然段
18
+ - 列表尽量少,只在确实需要步骤时使用
19
+
20
+ ## 内容要求
21
+
22
+ - 至少 1 个真实场景
23
+ - 至少 1 个常见误区
24
+ - 至少 1 组可执行动作
25
+ - 至少 1 个明确判断
26
+ - 结尾不能只剩 CTA
27
+
28
+ ## 风格要求
29
+
30
+ - 专业,但不要端着
31
+ - 有判断,但不要喊口号
32
+ - 可以有第一人称经验
33
+ - 不要低质口癖
34
+ - 不要泛泛而谈“提效、闭环、赋能”
35
+
36
+ ## 输出约束
37
+
38
+ - 正文用 Markdown
39
+ - 重要判断句用 `**加粗**`
40
+ - 如果有命令、配置、提示词,放进 fenced code block
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env python3
2
+ """Heuristic quality gate for WeChat article markdown."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ from pathlib import Path
10
+
11
+
12
+ BAD_SLANG = [
13
+ "家人们",
14
+ "绝绝子",
15
+ "冲就完了",
16
+ "yyds",
17
+ "哈哈哈",
18
+ ]
19
+
20
+ TRANSITIONS = [
21
+ "先说结论",
22
+ "更关键的是",
23
+ "换句话说",
24
+ "问题在于",
25
+ "所以",
26
+ "接下来",
27
+ "最后",
28
+ "这意味着",
29
+ ]
30
+
31
+ SCENE_MARKERS = [
32
+ "比如",
33
+ "例如",
34
+ "我自己",
35
+ "我实测",
36
+ "这周",
37
+ "上周",
38
+ "项目里",
39
+ "直播间",
40
+ ]
41
+
42
+ ACTION_MARKERS = [
43
+ "可以先",
44
+ "建议你",
45
+ "下一步",
46
+ "按这三步",
47
+ "立刻",
48
+ "先做",
49
+ ]
50
+
51
+
52
+ def normalize(markdown: str) -> str:
53
+ markdown = re.sub(r"```[\s\S]*?```", "", markdown)
54
+ markdown = re.sub(r"!\[[^\]]*]\([^)]+\)", "", markdown)
55
+ return markdown
56
+
57
+
58
+ def split_paragraphs(markdown: str) -> list[str]:
59
+ chunks = re.split(r"\n\s*\n", markdown)
60
+ paragraphs = []
61
+ for chunk in chunks:
62
+ text = chunk.strip()
63
+ if not text:
64
+ continue
65
+ if text.startswith("#") or text.startswith(">") or text == "---":
66
+ continue
67
+ if re.match(r"^\s*(?:[-*]|\d+\.)\s+", text):
68
+ continue
69
+ paragraphs.append(text)
70
+ return paragraphs
71
+
72
+
73
+ def count_chinese_chars(text: str) -> int:
74
+ return len(re.findall(r"[\u4e00-\u9fff]", text))
75
+
76
+
77
+ def build_report(markdown: str) -> dict:
78
+ text = normalize(markdown)
79
+ lines = text.splitlines()
80
+ non_empty = [line for line in lines if line.strip()]
81
+ list_lines = [line for line in lines if re.match(r"^\s*(?:[-*]|\d+\.)\s+", line)]
82
+ h2_headings = [line for line in lines if re.match(r"^\s*##\s+", line)]
83
+ paragraphs = split_paragraphs(text)
84
+
85
+ zh_chars = count_chinese_chars(text)
86
+ list_ratio = (len(list_lines) / len(non_empty)) if non_empty else 0
87
+ short_paragraphs = [paragraph for paragraph in paragraphs if count_chinese_chars(re.sub(r"\s+", "", paragraph)) < 60]
88
+
89
+ transition_hits = sum(text.count(item) for item in TRANSITIONS)
90
+ scene_hits = sum(text.count(item) for item in SCENE_MARKERS)
91
+ action_hits = sum(text.count(item) for item in ACTION_MARKERS)
92
+ slang_hits = [item for item in BAD_SLANG if item in text]
93
+
94
+ has_what = ("是什么" in text) or ("这件事是" in text) or ("你可以把它理解成" in text)
95
+ has_why = ("为什么" in text) or ("原因" in text) or ("问题在于" in text)
96
+ has_how = ("怎么做" in text) or ("可以先" in text) or ("建议你" in text) or ("下一步" in text)
97
+ has_next = ("下一篇" in text) or ("下篇" in text) or ("下一步" in text)
98
+ punctuation_burst = bool(re.search(r"[!?!?]{3,}", text))
99
+
100
+ suggestions: list[str] = []
101
+ if zh_chars < 1200:
102
+ suggestions.append("正文偏短,建议补到至少 1200 个中文字符。")
103
+ if list_ratio > 0.2:
104
+ suggestions.append("列表密度偏高,建议改写成更多自然段。")
105
+ if len(h2_headings) < 4:
106
+ suggestions.append("结构层级偏少,建议补足 4 到 6 个二级标题。")
107
+ if transition_hits < 4:
108
+ suggestions.append("章节承接偏弱,可以增加“先说结论、问题在于、换句话说”这类过渡句。")
109
+ if scene_hits < 1:
110
+ suggestions.append("缺少真实场景,建议补一个直播、项目或实测片段。")
111
+ if action_hits < 1:
112
+ suggestions.append("缺少可执行动作,建议补一组能直接照做的步骤。")
113
+ if slang_hits:
114
+ suggestions.append(f"存在口语化过强表达:{', '.join(slang_hits)}。")
115
+ if not has_next:
116
+ suggestions.append("结尾缺少下一步引导,建议加一句自然收束。")
117
+
118
+ return {
119
+ "metrics": {
120
+ "zh_chars": zh_chars,
121
+ "h2_count": len(h2_headings),
122
+ "paragraph_count": len(paragraphs),
123
+ "short_paragraph_count": len(short_paragraphs),
124
+ "list_line_count": len(list_lines),
125
+ "list_ratio": round(list_ratio, 3),
126
+ "transition_hits": transition_hits,
127
+ "scene_hits": scene_hits,
128
+ "action_hits": action_hits,
129
+ "slang_hits": slang_hits,
130
+ "punctuation_burst": punctuation_burst
131
+ },
132
+ "gates": {
133
+ "logic": "PASS" if has_what and has_why and has_how and len(h2_headings) >= 4 else "FAIL",
134
+ "humanity": "PASS" if scene_hits >= 1 and action_hits >= 1 else "FAIL",
135
+ "language": "PASS" if not slang_hits and not punctuation_burst else "FAIL",
136
+ "publish_ready": "PASS" if has_next and zh_chars >= 1200 and list_ratio <= 0.2 else "FAIL"
137
+ },
138
+ "suggestions": suggestions
139
+ }
140
+
141
+
142
+ def main() -> None:
143
+ parser = argparse.ArgumentParser()
144
+ parser.add_argument("--input", required=True)
145
+ parser.add_argument("--output", default="")
146
+ args = parser.parse_args()
147
+
148
+ markdown = Path(args.input).read_text(encoding="utf-8")
149
+ report = build_report(markdown)
150
+ output = json.dumps(report, ensure_ascii=False, indent=2)
151
+ print(output)
152
+
153
+ if args.output:
154
+ output_path = Path(args.output)
155
+ output_path.parent.mkdir(parents=True, exist_ok=True)
156
+ output_path.write_text(output, encoding="utf-8")
157
+
158
+
159
+ if __name__ == "__main__":
160
+ main()
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env python3
2
+ """Render markdown into WeChat-friendly article HTML."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import html
8
+ import json
9
+ import re
10
+ from pathlib import Path
11
+
12
+
13
+ DEFAULT_STYLE = """
14
+ body { margin: 0; background: #ffffff; color: #111111; font-family: "PingFang SC", "Microsoft YaHei", sans-serif; }
15
+ .wx-wrap { max-width: 720px; margin: 0 auto; padding: 24px 18px 48px; box-sizing: border-box; }
16
+ .wx-title { margin: 0 0 12px; font-size: 28px; line-height: 1.35; font-weight: 700; color: #111111; }
17
+ .wx-subtitle { margin: 0 0 20px; font-size: 15px; line-height: 1.8; color: #666666; }
18
+ .wx-content p, .wx-content li { margin: 0 0 14px; font-size: 16px; line-height: 1.8; color: #111111; text-align: justify; text-justify: inter-ideograph; word-break: break-word; }
19
+ .wx-content h2, .wx-content h3 { margin: 26px 0 10px; font-size: 20px; line-height: 1.6; font-weight: 700; color: rgb(0,128,255); }
20
+ .wx-content strong { color: rgb(0,128,255); font-weight: 700; }
21
+ .wx-content ul, .wx-content ol { margin: 0 0 14px 1.4em; padding: 0; }
22
+ .wx-content pre { margin: 0 0 18px; padding: 14px 16px; background: #f7f8fa; border-left: 3px solid rgba(0,128,255,.45); overflow-x: auto; border-radius: 6px; }
23
+ .wx-content code { font-size: 14px; line-height: 1.7; color: #333333; font-family: "Consolas", "Menlo", monospace; white-space: pre-wrap; }
24
+ .wx-sep { margin: 12px 0 16px; font-size: 16px; line-height: 1.8; color: #999999; text-align: center; }
25
+ .wx-cta { margin-top: 28px; padding-top: 16px; border-top: 1px solid #ececec; }
26
+ .wx-cta p { margin: 0; font-size: 15px; line-height: 1.8; color: #444444; }
27
+ .wx-footer { margin-top: 44px; padding-top: 18px; border-top: 1px solid #f0f0f0; }
28
+ .wx-footer p { margin: 0; font-size: 13px; line-height: 1.9; color: #a0a0a0; text-align: center; }
29
+ """
30
+
31
+
32
+ def load_style(config_path: Path | None) -> str:
33
+ if not config_path or not config_path.exists():
34
+ return DEFAULT_STYLE
35
+
36
+ config = json.loads(config_path.read_text(encoding="utf-8"))
37
+ body_font_size = int(config.get("bodyFontSize", 16))
38
+ body_line_height = float(config.get("bodyLineHeight", 1.8))
39
+ content_width = int(config.get("contentWidth", 720))
40
+ accent = str(config.get("accentColor", "rgb(0,128,255)"))
41
+ text = str(config.get("textColor", "#111111"))
42
+
43
+ return DEFAULT_STYLE.replace("720px", f"{content_width}px").replace(
44
+ "rgb(0,128,255)", accent
45
+ ).replace("#111111", text).replace("font-size: 16px; line-height: 1.8;", f"font-size: {body_font_size}px; line-height: {body_line_height};")
46
+
47
+
48
+ def convert_inline(text: str) -> str:
49
+ text = re.sub(r"!\[\[[^\]]+\]\]", "", text)
50
+ text = html.escape(text)
51
+ text = re.sub(r"`([^`]+)`", r"<code>\1</code>", text)
52
+ text = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", text)
53
+ text = re.sub(r"\*([^*]+)\*", r"<em>\1</em>", text)
54
+ text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', text)
55
+ return text
56
+
57
+
58
+ def markdown_to_blocks(content: str) -> tuple[str, str, bool]:
59
+ lines = content.splitlines()
60
+ title = ""
61
+ blocks: list[str] = []
62
+ in_code = False
63
+ code_lines: list[str] = []
64
+ paragraph_lines: list[str] = []
65
+ has_cta = False
66
+
67
+ def flush_paragraph() -> None:
68
+ nonlocal paragraph_lines, has_cta
69
+ if not paragraph_lines:
70
+ return
71
+ text = re.sub(r"\s+", " ", " ".join(line.strip() for line in paragraph_lines if line.strip())).strip()
72
+ paragraph_lines = []
73
+ if not text:
74
+ return
75
+ if any(keyword in text for keyword in ["留言", "评论", "下一篇", "下篇", "领取"]):
76
+ has_cta = True
77
+ blocks.append(f"<p>{convert_inline(text)}</p>")
78
+
79
+ for raw_line in lines:
80
+ line = raw_line.rstrip()
81
+ stripped = line.strip()
82
+
83
+ if stripped.startswith("```"):
84
+ flush_paragraph()
85
+ if in_code:
86
+ blocks.append("<pre><code>" + html.escape("\n".join(code_lines)) + "</code></pre>")
87
+ code_lines = []
88
+ in_code = False
89
+ else:
90
+ in_code = True
91
+ continue
92
+
93
+ if in_code:
94
+ code_lines.append(line)
95
+ continue
96
+
97
+ if not stripped:
98
+ flush_paragraph()
99
+ continue
100
+
101
+ if not title and stripped.startswith("# "):
102
+ title = stripped[2:].strip()
103
+ continue
104
+
105
+ if stripped.startswith("## "):
106
+ flush_paragraph()
107
+ blocks.append(f"<h2>{convert_inline(stripped[3:])}</h2>")
108
+ continue
109
+
110
+ if stripped.startswith("### "):
111
+ flush_paragraph()
112
+ blocks.append(f"<h3>{convert_inline(stripped[4:])}</h3>")
113
+ continue
114
+
115
+ if re.match(r"^\d+\.\s+", stripped):
116
+ flush_paragraph()
117
+ blocks.append("<ol><li>" + convert_inline(re.sub(r"^\d+\.\s+", "", stripped)) + "</li></ol>")
118
+ continue
119
+
120
+ if re.match(r"^[-*]\s+", stripped):
121
+ flush_paragraph()
122
+ blocks.append("<ul><li>" + convert_inline(re.sub(r"^[-*]\s+", "", stripped)) + "</li></ul>")
123
+ continue
124
+
125
+ if stripped == "---":
126
+ flush_paragraph()
127
+ blocks.append('<p class="wx-sep">···</p>')
128
+ continue
129
+
130
+ paragraph_lines.append(stripped)
131
+
132
+ flush_paragraph()
133
+ return title, "\n".join(blocks), has_cta
134
+
135
+
136
+ def render_html(markdown: str, title_override: str, style_config_path: Path | None) -> str:
137
+ title, body, has_cta = markdown_to_blocks(markdown)
138
+ title = title_override or title or "公众号文章"
139
+ template_path = Path(__file__).resolve().parent.parent / "assets" / "wechat-article-template.html"
140
+ template = template_path.read_text(encoding="utf-8")
141
+ cta = "" if has_cta else '<footer class="wx-cta"><p>如果这篇内容对你有帮助,下一步可以继续把同主题素材整理成系列文章。</p></footer>'
142
+ footer = '<footer class="wx-footer"><p>由 OpenSchool 严选技能整理输出</p></footer>'
143
+ subtitle_block = ""
144
+
145
+ output = template.replace("{{TITLE}}", html.escape(title))
146
+ output = output.replace("{{SUBTITLE_BLOCK}}", subtitle_block)
147
+ output = output.replace("{{STYLE}}", load_style(style_config_path).strip())
148
+ output = output.replace("{{CONTENT}}", body)
149
+ output = output.replace("{{CTA_BLOCK}}", cta)
150
+ output = output.replace("{{FOOTER_BLOCK}}", footer)
151
+ return output
152
+
153
+
154
+ def main() -> None:
155
+ parser = argparse.ArgumentParser()
156
+ parser.add_argument("--input", required=True)
157
+ parser.add_argument("--output", required=True)
158
+ parser.add_argument("--title", default="")
159
+ parser.add_argument("--style-config", default="")
160
+ args = parser.parse_args()
161
+
162
+ style_config_path = Path(args.style_config) if args.style_config else Path(__file__).resolve().parent.parent / "config" / "wechat-style-config.json"
163
+ markdown = Path(args.input).read_text(encoding="utf-8")
164
+ output = render_html(markdown, args.title, style_config_path)
165
+ output_path = Path(args.output)
166
+ output_path.parent.mkdir(parents=True, exist_ok=True)
167
+ output_path.write_text(output, encoding="utf-8")
168
+
169
+
170
+ if __name__ == "__main__":
171
+ main()
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@openschool_01/skill-xiaohongshu-assistant",
3
+ "version": "0.1.0",
4
+ "description": "OpenSchool curated skill: xiaohongshu assistant.",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "files": [
10
+ "skill"
11
+ ]
12
+ }
@@ -0,0 +1,14 @@
1
+ # OpenSchool 收录说明
2
+
3
+ - 收录名称:小红书助手
4
+ - 收录方式:OpenSchool 严选整合
5
+ - 当前版本:纯文案版
6
+
7
+ 当前版本重点解决的是“小红书内容怎么写得更像平台原生表达”,不先做图片和发布。
8
+
9
+ 主要保留的能力:
10
+
11
+ 1. 切角选择
12
+ 2. 标题方向生成
13
+ 3. 正文重写
14
+ 4. 标签与评论区引导
@@ -0,0 +1,100 @@
1
+ # 小红书助手
2
+
3
+ 适用于把一个主题、直播片段、公众号原稿或观点素材,整理成更适合小红书发布的内容。
4
+
5
+ 当前版本专注 4 件事:
6
+
7
+ 1. 明确小红书切角
8
+ 2. 生成更像平台风格的标题方向
9
+ 3. 输出可直接发布的正文
10
+ 4. 补齐标签、封面文案和评论区引导
11
+
12
+ 当前版本不负责:
13
+
14
+ - 生图
15
+ - 封面出图
16
+ - 自动发布
17
+
18
+ ## 什么时候用
19
+
20
+ 当用户要做这些事时使用:
21
+
22
+ - 把公众号内容改成小红书
23
+ - 写小红书标题
24
+ - 生成小红书正文
25
+ - 做一版更适合平台传播的内容重写
26
+
27
+ ## 推荐工作流
28
+
29
+ ### 1. 先收集输入
30
+
31
+ 至少确认:
32
+
33
+ - 原始主题或素材
34
+ - 目标读者
35
+ - 这次最想强调的结论
36
+ - 希望走哪种内容逻辑
37
+
38
+ ### 2. 先定传播逻辑
39
+
40
+ 默认优先在这几类里选一个主逻辑:
41
+
42
+ - 误区避坑
43
+ - 保姆级步骤
44
+ - 强观点判断
45
+ - 案例复盘
46
+ - 清单速看
47
+
48
+ 一次只保留一个主逻辑,不要把一篇小红书写成什么都想讲的说明文。
49
+
50
+ ### 3. 再出标题
51
+
52
+ 默认输出 5 个标题备选,并覆盖:
53
+
54
+ - 警告拦截型
55
+ - 数字步骤型
56
+ - 观点判断型
57
+
58
+ 标题规则见:
59
+
60
+ - `references/title-guidelines.md`
61
+
62
+ ### 4. 再写正文
63
+
64
+ 正文默认要求:
65
+
66
+ - 开头前两句就给钩子和结论
67
+ - 用短段落,控制阅读压力
68
+ - 优先写误区、建议、步骤和判断
69
+ - 结尾补评论区互动问题
70
+
71
+ 写作规则见:
72
+
73
+ - `references/writing-rules.md`
74
+
75
+ ### 5. 最终补齐发布要素
76
+
77
+ 默认交付:
78
+
79
+ - 5 个标题备选
80
+ - 1 篇可直接发布的正文
81
+ - 3 条封面文案
82
+ - 8 到 12 个标签
83
+ - 3 条评论区引导
84
+
85
+ ## 目录说明
86
+
87
+ - `references/title-guidelines.md`
88
+ 标题方向规则
89
+ - `references/writing-rules.md`
90
+ 正文改写规则
91
+
92
+ ## 交付要求
93
+
94
+ 如果没有特别说明,默认用中文直接输出:
95
+
96
+ - 标题备选
97
+ - 正文
98
+ - 封面文案
99
+ - 标签建议
100
+ - 评论区引导
@@ -0,0 +1,36 @@
1
+ # 标题规则
2
+
3
+ 小红书标题优先追求“停下来、看下去、愿意点开”,不是写成说明书标题。
4
+
5
+ ## 默认输出 5 个标题
6
+
7
+ 至少覆盖这三类:
8
+
9
+ ### 1. 警告拦截型
10
+
11
+ 示例:
12
+
13
+ - 别一上来就这样做
14
+ - 新手最容易踩坑的,其实是这一步
15
+
16
+ ### 2. 数字步骤型
17
+
18
+ 示例:
19
+
20
+ - 我现在只用这 3 步写小红书
21
+ - 1 分钟看懂这件事到底怎么做
22
+
23
+ ### 3. 观点判断型
24
+
25
+ 示例:
26
+
27
+ - 真正拉开差距的,不是会不会写,而是会不会切角
28
+ - 很多人不是不会做内容,是一开始就写偏了
29
+
30
+ ## 标题筛选规则
31
+
32
+ - 优先 14 到 20 字
33
+ - 一眼能看懂在讲什么
34
+ - 不要空泛“干货、分享、教程”
35
+ - 可以有轻度情绪,但不要过度夸张
36
+ - 优先带对象、场景、结果或判断
@@ -0,0 +1,33 @@
1
+ # 写作规则
2
+
3
+ ## 默认结构
4
+
5
+ 1. 开头两句完成钩子和结论
6
+ 2. 中间用 3 到 5 个短段落展开
7
+ 3. 优先写误区、建议、步骤、判断
8
+ 4. 结尾用一句总结加一个互动问题收尾
9
+
10
+ ## 内容要求
11
+
12
+ - 正文尽量控制在 280 到 520 字
13
+ - 复杂主题也尽量不要超过 1000 字
14
+ - 每段 1 到 3 行
15
+ - 一次只保留一个主逻辑
16
+ - 至少有一个可直接执行动作
17
+
18
+ ## 风格要求
19
+
20
+ - 更像真实用户分享
21
+ - 少用说明文腔调
22
+ - 少铺背景,多给结论
23
+ - 少讲大道理,多给具体判断
24
+
25
+ ## 输出要求
26
+
27
+ 默认输出:
28
+
29
+ - 5 个标题
30
+ - 1 版正文
31
+ - 3 条封面文案
32
+ - 8 到 12 个标签
33
+ - 3 条评论区互动引导
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openschool_01/skills",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "OpenSchool curated skills installer CLI.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "bin",
11
+ "bundled-skills",
11
12
  "registry",
12
13
  "README.md"
13
14
  ],