@astro-minimax/cli 0.5.0 → 0.7.1
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.
- package/README.md +69 -0
- package/dist/commands/ai.d.ts +2 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +99 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/data.d.ts +2 -0
- package/dist/commands/data.d.ts.map +1 -0
- package/dist/commands/data.js +111 -0
- package/dist/commands/data.js.map +1 -0
- package/dist/commands/hooks.d.ts +2 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +378 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +50 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/podcast.d.ts +2 -0
- package/dist/commands/podcast.d.ts.map +1 -0
- package/dist/commands/podcast.js +89 -0
- package/dist/commands/podcast.js.map +1 -0
- package/dist/commands/post.d.ts +2 -0
- package/dist/commands/post.d.ts.map +1 -0
- package/dist/commands/post.js +190 -0
- package/dist/commands/post.js.map +1 -0
- package/dist/commands/profile.d.ts +2 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +88 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/ai-process.d.ts +20 -0
- package/dist/tools/ai-process.d.ts.map +1 -0
- package/dist/tools/ai-process.js +607 -0
- package/dist/tools/ai-process.js.map +1 -0
- package/dist/tools/build-author-context.d.ts +13 -0
- package/dist/tools/build-author-context.d.ts.map +1 -0
- package/dist/tools/build-author-context.js +313 -0
- package/dist/tools/build-author-context.js.map +1 -0
- package/dist/tools/build-voice-profile.d.ts +12 -0
- package/dist/tools/build-voice-profile.d.ts.map +1 -0
- package/dist/tools/build-voice-profile.js +270 -0
- package/dist/tools/build-voice-profile.js.map +1 -0
- package/dist/tools/eval-ai-chat.d.ts +17 -0
- package/dist/tools/eval-ai-chat.d.ts.map +1 -0
- package/dist/tools/eval-ai-chat.js +362 -0
- package/dist/tools/eval-ai-chat.js.map +1 -0
- package/dist/tools/generate-author-profile.d.ts +14 -0
- package/dist/tools/generate-author-profile.d.ts.map +1 -0
- package/dist/tools/generate-author-profile.js +289 -0
- package/dist/tools/generate-author-profile.js.map +1 -0
- package/dist/tools/generate-cover.d.ts +14 -0
- package/dist/tools/generate-cover.d.ts.map +1 -0
- package/dist/tools/generate-cover.js +95 -0
- package/dist/tools/generate-cover.js.map +1 -0
- package/dist/tools/generate-og.d.ts +3 -0
- package/dist/tools/generate-og.d.ts.map +1 -0
- package/dist/tools/generate-og.js +254 -0
- package/dist/tools/generate-og.js.map +1 -0
- package/dist/tools/generate-related.d.ts +11 -0
- package/dist/tools/generate-related.d.ts.map +1 -0
- package/dist/tools/generate-related.js +124 -0
- package/dist/tools/generate-related.js.map +1 -0
- package/dist/tools/generate-tags.d.ts +14 -0
- package/dist/tools/generate-tags.d.ts.map +1 -0
- package/dist/tools/generate-tags.js +182 -0
- package/dist/tools/generate-tags.js.map +1 -0
- package/dist/tools/lib/ai-provider.d.ts +43 -0
- package/dist/tools/lib/ai-provider.d.ts.map +1 -0
- package/dist/tools/lib/ai-provider.js +146 -0
- package/dist/tools/lib/ai-provider.js.map +1 -0
- package/dist/tools/lib/audio-processor.d.ts +46 -0
- package/dist/tools/lib/audio-processor.d.ts.map +1 -0
- package/dist/tools/lib/audio-processor.js +188 -0
- package/dist/tools/lib/audio-processor.js.map +1 -0
- package/dist/tools/lib/frontmatter.d.ts +11 -0
- package/dist/tools/lib/frontmatter.d.ts.map +1 -0
- package/dist/tools/lib/frontmatter.js +80 -0
- package/dist/tools/lib/frontmatter.js.map +1 -0
- package/dist/tools/lib/index.d.ts +7 -0
- package/dist/tools/lib/index.d.ts.map +1 -0
- package/{template/tools/lib/index.ts → dist/tools/lib/index.js} +1 -0
- package/dist/tools/lib/index.js.map +1 -0
- package/dist/tools/lib/markdown.d.ts +6 -0
- package/dist/tools/lib/markdown.d.ts.map +1 -0
- package/dist/tools/lib/markdown.js +34 -0
- package/dist/tools/lib/markdown.js.map +1 -0
- package/dist/tools/lib/posts.d.ts +25 -0
- package/dist/tools/lib/posts.d.ts.map +1 -0
- package/dist/tools/lib/posts.js +63 -0
- package/dist/tools/lib/posts.js.map +1 -0
- package/dist/tools/lib/script-generator.d.ts +61 -0
- package/dist/tools/lib/script-generator.d.ts.map +1 -0
- package/dist/tools/lib/script-generator.js +182 -0
- package/dist/tools/lib/script-generator.js.map +1 -0
- package/dist/tools/lib/tts-provider.d.ts +65 -0
- package/dist/tools/lib/tts-provider.d.ts.map +1 -0
- package/dist/tools/lib/tts-provider.js +116 -0
- package/dist/tools/lib/tts-provider.js.map +1 -0
- package/dist/tools/lib/types.d.ts +129 -0
- package/dist/tools/lib/types.d.ts.map +1 -0
- package/dist/tools/lib/types.js +64 -0
- package/dist/tools/lib/types.js.map +1 -0
- package/dist/tools/lib/utils.d.ts +18 -0
- package/dist/tools/lib/utils.d.ts.map +1 -0
- package/dist/tools/lib/utils.js +121 -0
- package/dist/tools/lib/utils.js.map +1 -0
- package/dist/tools/lib/vectors.d.ts +27 -0
- package/dist/tools/lib/vectors.d.ts.map +1 -0
- package/dist/tools/lib/vectors.js +64 -0
- package/dist/tools/lib/vectors.js.map +1 -0
- package/dist/tools/podcast-feed.d.ts +6 -0
- package/dist/tools/podcast-feed.d.ts.map +1 -0
- package/dist/tools/podcast-feed.js +121 -0
- package/dist/tools/podcast-feed.js.map +1 -0
- package/dist/tools/podcast-generate.d.ts +15 -0
- package/dist/tools/podcast-generate.d.ts.map +1 -0
- package/dist/tools/podcast-generate.js +318 -0
- package/dist/tools/podcast-generate.js.map +1 -0
- package/dist/tools/podcast-list.d.ts +6 -0
- package/dist/tools/podcast-list.d.ts.map +1 -0
- package/dist/tools/podcast-list.js +66 -0
- package/dist/tools/podcast-list.js.map +1 -0
- package/dist/tools/summarize.d.ts +16 -0
- package/dist/tools/summarize.d.ts.map +1 -0
- package/dist/tools/summarize.js +108 -0
- package/dist/tools/summarize.js.map +1 -0
- package/dist/tools/translate.d.ts +13 -0
- package/dist/tools/translate.d.ts.map +1 -0
- package/dist/tools/translate.js +46 -0
- package/dist/tools/translate.js.map +1 -0
- package/dist/tools/vectorize.d.ts +13 -0
- package/dist/tools/vectorize.d.ts.map +1 -0
- package/dist/tools/vectorize.js +87 -0
- package/dist/tools/vectorize.js.map +1 -0
- package/package.json +14 -9
- package/template/astro.config.ts +8 -28
- package/template/datas/ai-seo.json +8 -0
- package/template/datas/ai-skip-list.json +1 -0
- package/template/datas/author-profile-context.json +21 -0
- package/template/datas/author-profile-report.json +21 -0
- package/template/datas/eval/gold-set.json +72 -0
- package/template/functions/README.md +82 -0
- package/template/functions/api/ai-info.ts +2 -2
- package/template/functions/api/chat.ts +4 -1
- package/template/functions/api/notify/comment.ts +140 -68
- package/template/functions/api/notify/debug.ts +41 -0
- package/template/functions/api/notify/status.ts +97 -0
- package/template/functions/api/notify/test-ai-chat.ts +67 -0
- package/template/package.json +22 -25
- package/template/src/config.ts +11 -0
- package/template/src/content.config.ts +29 -16
- package/template/src/env.d.ts +0 -5
- package/index.js +0 -36
- package/template/tools/README.md +0 -169
- package/template/tools/ai-process.ts +0 -816
- package/template/tools/build-author-context.ts +0 -405
- package/template/tools/build-voice-profile.ts +0 -322
- package/template/tools/generate-author-profile.ts +0 -369
- package/template/tools/generate-cover.ts +0 -123
- package/template/tools/generate-og.ts +0 -280
- package/template/tools/generate-related.ts +0 -146
- package/template/tools/generate-tags.ts +0 -251
- package/template/tools/lib/ai-provider.ts +0 -240
- package/template/tools/lib/frontmatter.ts +0 -94
- package/template/tools/lib/markdown.ts +0 -40
- package/template/tools/lib/posts.ts +0 -89
- package/template/tools/lib/utils.ts +0 -138
- package/template/tools/lib/vectors.ts +0 -96
- package/template/tools/summarize.ts +0 -142
- package/template/tools/translate.ts +0 -60
- package/template/tools/vectorize.ts +0 -105
|
@@ -1,322 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx tsx
|
|
2
|
-
/**
|
|
3
|
-
* 构建作者表达风格画像
|
|
4
|
-
*
|
|
5
|
-
* 从博客标题、正文中提取作者的表达风格特征。
|
|
6
|
-
* 纯本地分析,不调用 AI。
|
|
7
|
-
*
|
|
8
|
-
* 用法:
|
|
9
|
-
* pnpm voice:build 构建风格画像
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
loadEnv,
|
|
16
|
-
writeJson,
|
|
17
|
-
DATA_DIR,
|
|
18
|
-
BLOG_DIR,
|
|
19
|
-
} from "./lib/utils.js";
|
|
20
|
-
import { extractFrontmatter } from "./lib/frontmatter.js";
|
|
21
|
-
|
|
22
|
-
// ─── 常量 ─────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
const OUTPUT_FILE = join(DATA_DIR, "voice-profile.json");
|
|
25
|
-
|
|
26
|
-
// ─── 文章收集 ─────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
interface Post {
|
|
29
|
-
title: string;
|
|
30
|
-
body: string;
|
|
31
|
-
category: string;
|
|
32
|
-
lang: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function collectPosts(): Promise<Post[]> {
|
|
36
|
-
const entries = await readdir(BLOG_DIR, { withFileTypes: true });
|
|
37
|
-
const posts: Post[] = [];
|
|
38
|
-
|
|
39
|
-
for (const entry of entries) {
|
|
40
|
-
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
41
|
-
|
|
42
|
-
const subDir = join(BLOG_DIR, entry.name);
|
|
43
|
-
const subEntries = await readdir(subDir, { withFileTypes: true });
|
|
44
|
-
|
|
45
|
-
for (const subEntry of subEntries) {
|
|
46
|
-
if (!subEntry.isFile() || !subEntry.name.endsWith(".md")) continue;
|
|
47
|
-
|
|
48
|
-
const filePath = join(subDir, subEntry.name);
|
|
49
|
-
const raw = await readFile(filePath, "utf-8");
|
|
50
|
-
const fm = extractFrontmatter(raw);
|
|
51
|
-
|
|
52
|
-
if (!fm.data.title || fm.data.draft) continue;
|
|
53
|
-
|
|
54
|
-
const relativePath = filePath.replace(BLOG_DIR + "/", "");
|
|
55
|
-
const lang = relativePath.startsWith("en/") ? "en" : "zh";
|
|
56
|
-
|
|
57
|
-
posts.push({
|
|
58
|
-
title: String(fm.data.title),
|
|
59
|
-
body: fm.body,
|
|
60
|
-
category: String(fm.data.category || ""),
|
|
61
|
-
lang,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return posts;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ─── 标题风格分析 ─────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
function analyzeTitlePatterns(posts: Post[]) {
|
|
72
|
-
const patterns = {
|
|
73
|
-
usesVerticalBar: 0, // "xxx | xxx"
|
|
74
|
-
usesColon: 0, // "xxx:xxx" or "xxx:xxx"
|
|
75
|
-
usesMiddleDot: 0, // "xxx・xxx"
|
|
76
|
-
usesQuestion: 0, // 标题是问句
|
|
77
|
-
usesEmoji: 0,
|
|
78
|
-
usesEnglish: 0, // 中英混用
|
|
79
|
-
usesYear: 0, // 标题带年份
|
|
80
|
-
usesSeriesFormat: 0, // 系列文章
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
for (const post of posts) {
|
|
84
|
-
const title = post.title;
|
|
85
|
-
|
|
86
|
-
if (title.includes("|") || title.includes("|")) patterns.usesVerticalBar++;
|
|
87
|
-
if (title.includes(":") || title.includes(":")) patterns.usesColon++;
|
|
88
|
-
if (title.includes("・") || title.includes("·")) patterns.usesMiddleDot++;
|
|
89
|
-
if (/[??]/.test(title)) patterns.usesQuestion++;
|
|
90
|
-
if (/[\u{1F300}-\u{1F9FF}]/u.test(title)) patterns.usesEmoji++;
|
|
91
|
-
if (/[a-zA-Z]{3,}/.test(title)) patterns.usesEnglish++;
|
|
92
|
-
if (/20\d{2}/.test(title)) patterns.usesYear++;
|
|
93
|
-
if (
|
|
94
|
-
/[壹贰叁肆伍陆柒捌玖拾]|Day\s?\d|Part\s?\d|[((]\d[))]/i.test(title)
|
|
95
|
-
) {
|
|
96
|
-
patterns.usesSeriesFormat++;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const total = posts.length || 1;
|
|
101
|
-
|
|
102
|
-
return {
|
|
103
|
-
total: posts.length,
|
|
104
|
-
patterns,
|
|
105
|
-
style: {
|
|
106
|
-
vertical_bar_rate: Math.round(
|
|
107
|
-
(patterns.usesVerticalBar / total) * 100
|
|
108
|
-
),
|
|
109
|
-
colon_rate: Math.round((patterns.usesColon / total) * 100),
|
|
110
|
-
question_rate: Math.round((patterns.usesQuestion / total) * 100),
|
|
111
|
-
english_mix_rate: Math.round((patterns.usesEnglish / total) * 100),
|
|
112
|
-
year_in_title_rate: Math.round((patterns.usesYear / total) * 100),
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ─── 正文风格分析 ─────────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
function analyzeContentPatterns(posts: Post[]) {
|
|
120
|
-
let totalLength = 0;
|
|
121
|
-
let postsWithCode = 0;
|
|
122
|
-
let postsWithLinks = 0;
|
|
123
|
-
let postsWithImages = 0;
|
|
124
|
-
let postsWithHeadings = 0;
|
|
125
|
-
let postsWithLists = 0;
|
|
126
|
-
let postsWithQuotes = 0;
|
|
127
|
-
|
|
128
|
-
for (const post of posts) {
|
|
129
|
-
const body = post.body;
|
|
130
|
-
totalLength += body.length;
|
|
131
|
-
|
|
132
|
-
if (/```[\s\S]*?```/.test(body)) postsWithCode++;
|
|
133
|
-
if (/\[[^\]]+\]\([^)]+\)/.test(body)) postsWithLinks++;
|
|
134
|
-
if (/!\[[^\]]*\]\([^)]+\)/.test(body)) postsWithImages++;
|
|
135
|
-
if (/^#{1,6}\s/m.test(body)) postsWithHeadings++;
|
|
136
|
-
if (/^[\-\*]\s/m.test(body)) postsWithLists++;
|
|
137
|
-
if (/^>\s/m.test(body)) postsWithQuotes++;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const total = posts.length || 1;
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
avgLength: Math.round(totalLength / total),
|
|
144
|
-
codeBlockRate: Math.round((postsWithCode / total) * 100),
|
|
145
|
-
linkRate: Math.round((postsWithLinks / total) * 100),
|
|
146
|
-
imageRate: Math.round((postsWithImages / total) * 100),
|
|
147
|
-
headingRate: Math.round((postsWithHeadings / total) * 100),
|
|
148
|
-
listRate: Math.round((postsWithLists / total) * 100),
|
|
149
|
-
quoteRate: Math.round((postsWithQuotes / total) * 100),
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ─── 表达习惯提取 ─────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
function extractExpressionHabits(posts: Post[]) {
|
|
156
|
-
const connectors: Record<string, number> = {};
|
|
157
|
-
|
|
158
|
-
const connectorPatterns = [
|
|
159
|
-
"其实",
|
|
160
|
-
"说实话",
|
|
161
|
-
"不过",
|
|
162
|
-
"但是",
|
|
163
|
-
"所以",
|
|
164
|
-
"然后",
|
|
165
|
-
"感觉",
|
|
166
|
-
"突然",
|
|
167
|
-
"顺手",
|
|
168
|
-
"顺便",
|
|
169
|
-
"随手",
|
|
170
|
-
"折腾",
|
|
171
|
-
"踩坑",
|
|
172
|
-
"跳坑",
|
|
173
|
-
"体验",
|
|
174
|
-
"分享",
|
|
175
|
-
"推荐",
|
|
176
|
-
"安利",
|
|
177
|
-
"种草",
|
|
178
|
-
"注意",
|
|
179
|
-
"重要",
|
|
180
|
-
"关键",
|
|
181
|
-
"总结",
|
|
182
|
-
];
|
|
183
|
-
|
|
184
|
-
for (const post of posts) {
|
|
185
|
-
const text = `${post.title} ${post.body}`;
|
|
186
|
-
|
|
187
|
-
for (const pattern of connectorPatterns) {
|
|
188
|
-
const count = (text.match(new RegExp(pattern, "g")) || []).length;
|
|
189
|
-
if (count > 0) {
|
|
190
|
-
connectors[pattern] = (connectors[pattern] || 0) + count;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const sortedConnectors = Object.entries(connectors)
|
|
196
|
-
.sort(([, a], [, b]) => b - a)
|
|
197
|
-
.slice(0, 15)
|
|
198
|
-
.map(([word, count]) => ({ word, count }));
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
frequent_expressions: sortedConnectors,
|
|
202
|
-
style_notes: [
|
|
203
|
-
"标题常用竖线分隔格式:「主题 | 标题」",
|
|
204
|
-
"技术类标题直接用英文术语,不强行翻译",
|
|
205
|
-
"生活类标题偏感性,常用问句或感叹",
|
|
206
|
-
"喜欢用「折腾」「踩坑」等口语化表达描述技术探索",
|
|
207
|
-
"分享经验时常用「攻略」「指南」「记」等实用性标题词",
|
|
208
|
-
],
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ─── 分类风格分析 ─────────────────────────────────────────────
|
|
213
|
-
|
|
214
|
-
function analyzeCategoryStyles(posts: Post[]) {
|
|
215
|
-
const categoryMap = new Map<string, Post[]>();
|
|
216
|
-
|
|
217
|
-
for (const post of posts) {
|
|
218
|
-
const cat = post.category || "其他";
|
|
219
|
-
if (!categoryMap.has(cat)) categoryMap.set(cat, []);
|
|
220
|
-
categoryMap.get(cat)!.push(post);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const categoryStyles: Record<string, { count: number; avgLength: number }> =
|
|
224
|
-
{};
|
|
225
|
-
|
|
226
|
-
for (const [cat, catPosts] of categoryMap.entries()) {
|
|
227
|
-
const avgLength =
|
|
228
|
-
catPosts.reduce((sum, p) => sum + p.body.length, 0) / catPosts.length;
|
|
229
|
-
categoryStyles[cat] = {
|
|
230
|
-
count: catPosts.length,
|
|
231
|
-
avgLength: Math.round(avgLength),
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return categoryStyles;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// ─── 主流程 ───────────────────────────────────────────────────
|
|
239
|
-
|
|
240
|
-
async function main() {
|
|
241
|
-
await loadEnv();
|
|
242
|
-
|
|
243
|
-
console.log("🎤 构建作者表达风格画像");
|
|
244
|
-
console.log("━".repeat(50));
|
|
245
|
-
console.log("");
|
|
246
|
-
|
|
247
|
-
console.log("📂 扫描博客文章...");
|
|
248
|
-
const posts = await collectPosts();
|
|
249
|
-
console.log(` 找到 ${posts.length} 篇文章`);
|
|
250
|
-
|
|
251
|
-
const zhPosts = posts.filter((p) => p.lang === "zh");
|
|
252
|
-
const enPosts = posts.filter((p) => p.lang === "en");
|
|
253
|
-
console.log(` - 中文: ${zhPosts.length} 篇`);
|
|
254
|
-
console.log(` - 英文: ${enPosts.length} 篇`);
|
|
255
|
-
|
|
256
|
-
// 分析各维度
|
|
257
|
-
console.log("\n🔍 分析风格特征...");
|
|
258
|
-
const titleAnalysis = analyzeTitlePatterns(posts);
|
|
259
|
-
const contentAnalysis = analyzeContentPatterns(posts);
|
|
260
|
-
const expressionHabits = extractExpressionHabits(posts);
|
|
261
|
-
const categoryStyles = analyzeCategoryStyles(posts);
|
|
262
|
-
|
|
263
|
-
// 构建画像
|
|
264
|
-
const profile = {
|
|
265
|
-
$schema: "voice-profile-v1",
|
|
266
|
-
generatedAt: new Date().toISOString(),
|
|
267
|
-
overall_tone: {
|
|
268
|
-
description:
|
|
269
|
-
"技术博客风格,注重实践与可操作性,语言简洁明了",
|
|
270
|
-
primary_persona: "技术博主",
|
|
271
|
-
communication_style: "先给结论,再补细节",
|
|
272
|
-
},
|
|
273
|
-
blog_title_style: titleAnalysis,
|
|
274
|
-
content_style: contentAnalysis,
|
|
275
|
-
expression_habits: expressionHabits,
|
|
276
|
-
category_styles: categoryStyles,
|
|
277
|
-
style_modes: {
|
|
278
|
-
technical: {
|
|
279
|
-
description: "技术类文章",
|
|
280
|
-
traits: [
|
|
281
|
-
"直接给方案",
|
|
282
|
-
"会提到具体工具和版本",
|
|
283
|
-
"不回避踩过的坑",
|
|
284
|
-
"代码片段简洁",
|
|
285
|
-
],
|
|
286
|
-
},
|
|
287
|
-
tutorial: {
|
|
288
|
-
description: "教程类文章",
|
|
289
|
-
traits: [
|
|
290
|
-
"步骤清晰",
|
|
291
|
-
"配合截图或代码",
|
|
292
|
-
"注意事项突出",
|
|
293
|
-
"有总结部分",
|
|
294
|
-
],
|
|
295
|
-
},
|
|
296
|
-
lifestyle: {
|
|
297
|
-
description: "生活类文章",
|
|
298
|
-
traits: ["语气更随意", "偶尔自嘲", "不说教", "分享真实体验"],
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
};
|
|
302
|
-
|
|
303
|
-
await writeJson(OUTPUT_FILE, profile);
|
|
304
|
-
|
|
305
|
-
console.log("\n✅ 风格画像构建完成");
|
|
306
|
-
console.log(`📄 输出文件: ${OUTPUT_FILE}`);
|
|
307
|
-
console.log("\n📊 风格概览:");
|
|
308
|
-
console.log(` 标题竖线分隔率: ${titleAnalysis.style.vertical_bar_rate}%`);
|
|
309
|
-
console.log(` 标题中英混用率: ${titleAnalysis.style.english_mix_rate}%`);
|
|
310
|
-
console.log(` 平均文章长度: ${contentAnalysis.avgLength} 字符`);
|
|
311
|
-
console.log(
|
|
312
|
-
` 高频表达: ${expressionHabits.frequent_expressions
|
|
313
|
-
.slice(0, 5)
|
|
314
|
-
.map((e) => e.word)
|
|
315
|
-
.join("、")}`
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
main().catch((error) => {
|
|
320
|
-
console.error("❌ 构建失败:", error.message);
|
|
321
|
-
process.exit(1);
|
|
322
|
-
});
|
|
@@ -1,369 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env npx tsx
|
|
2
|
-
/**
|
|
3
|
-
* 生成作者画像报告
|
|
4
|
-
*
|
|
5
|
-
* 基于作者上下文数据生成用于 About 页面的结构化简介。
|
|
6
|
-
* 支持 AI 生成和规则模板两种模式。
|
|
7
|
-
*
|
|
8
|
-
* 用法:
|
|
9
|
-
* pnpm profile:generate AI 生成画像报告
|
|
10
|
-
* pnpm profile:generate --no-ai 使用规则模板
|
|
11
|
-
* pnpm profile:generate --force 强制重新生成(不回退)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
import {
|
|
17
|
-
loadEnv,
|
|
18
|
-
readJson,
|
|
19
|
-
writeJson,
|
|
20
|
-
truncate,
|
|
21
|
-
parseCliArgs,
|
|
22
|
-
DATA_DIR,
|
|
23
|
-
BLOG_DIR,
|
|
24
|
-
} from "./lib/utils.js";
|
|
25
|
-
import { stripMarkdown } from "./lib/markdown.js";
|
|
26
|
-
import { extractFrontmatter } from "./lib/frontmatter.js";
|
|
27
|
-
import { chatCompletion, hasAPIKey, getConfig } from "./lib/ai-provider.js";
|
|
28
|
-
|
|
29
|
-
// ─── 常量 ─────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
const OUTPUT_REPORT = join(DATA_DIR, "author-profile-report.json");
|
|
32
|
-
const OUTPUT_CONTEXT = join(DATA_DIR, "author-profile-context.json");
|
|
33
|
-
const DEFAULT_SITE_URL = "https://example.com";
|
|
34
|
-
|
|
35
|
-
// ─── CLI 参数 ─────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
interface CliFlags {
|
|
38
|
-
force: boolean;
|
|
39
|
-
noAI: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function parseArgs(): CliFlags {
|
|
43
|
-
return parseCliArgs({ force: false, noAI: false });
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ─── 文章收集 ─────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
interface Post {
|
|
49
|
-
title: string;
|
|
50
|
-
date: string;
|
|
51
|
-
lang: string;
|
|
52
|
-
category: string;
|
|
53
|
-
tags: string[];
|
|
54
|
-
description: string;
|
|
55
|
-
summary?: string;
|
|
56
|
-
keyPoints?: string[];
|
|
57
|
-
url: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function collectPosts(_siteUrl: string): Promise<Post[]> {
|
|
61
|
-
const entries = await readdir(BLOG_DIR, { withFileTypes: true });
|
|
62
|
-
const aiSummaries = await readJson<{
|
|
63
|
-
articles?: Record<string, { data?: { summary?: string; keyPoints?: string[] } }>
|
|
64
|
-
}>(join(DATA_DIR, "ai-summaries.json"), {
|
|
65
|
-
articles: {},
|
|
66
|
-
});
|
|
67
|
-
const posts: Post[] = [];
|
|
68
|
-
|
|
69
|
-
for (const entry of entries) {
|
|
70
|
-
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
71
|
-
|
|
72
|
-
const subDir = join(BLOG_DIR, entry.name);
|
|
73
|
-
const subEntries = await readdir(subDir, { withFileTypes: true });
|
|
74
|
-
|
|
75
|
-
for (const subEntry of subEntries) {
|
|
76
|
-
if (!subEntry.isFile() || !subEntry.name.endsWith(".md")) continue;
|
|
77
|
-
|
|
78
|
-
const filePath = join(subDir, subEntry.name);
|
|
79
|
-
const raw = await readFile(filePath, "utf-8");
|
|
80
|
-
const fm = extractFrontmatter(raw);
|
|
81
|
-
|
|
82
|
-
if (!fm.data.title || fm.data.draft) continue;
|
|
83
|
-
|
|
84
|
-
const relativePath = filePath.replace(BLOG_DIR + "/", "");
|
|
85
|
-
const id = relativePath.replace(/\.md$/, "");
|
|
86
|
-
const lang = relativePath.startsWith("en/") ? "en" : "zh";
|
|
87
|
-
const slug = id.split("/").slice(1).join("/");
|
|
88
|
-
const summaryEntry = aiSummaries.articles?.[id]?.data;
|
|
89
|
-
|
|
90
|
-
posts.push({
|
|
91
|
-
title: String(fm.data.title),
|
|
92
|
-
date: String(fm.data.pubDatetime),
|
|
93
|
-
lang,
|
|
94
|
-
category: String(fm.data.category || ""),
|
|
95
|
-
tags: Array.isArray(fm.data.tags) ? (fm.data.tags as string[]) : [],
|
|
96
|
-
description: String(fm.data.description || ""),
|
|
97
|
-
summary: summaryEntry?.summary || truncate(stripMarkdown(fm.body), 100),
|
|
98
|
-
keyPoints: summaryEntry?.keyPoints || [],
|
|
99
|
-
url: `/${lang}/posts/${slug}/`,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
105
|
-
return posts;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ─── 上下文构建 ───────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
function buildContext(posts: Post[], siteUrl: string) {
|
|
111
|
-
const selectedPosts = posts.slice(0, 12);
|
|
112
|
-
const zhPosts = posts.filter((p) => p.lang === "zh");
|
|
113
|
-
const enPosts = posts.filter((p) => p.lang === "en");
|
|
114
|
-
|
|
115
|
-
// 标签聚合
|
|
116
|
-
const tagCounts = new Map<string, number>();
|
|
117
|
-
for (const post of posts) {
|
|
118
|
-
for (const tag of post.tags) {
|
|
119
|
-
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const topTags = [...tagCounts.entries()]
|
|
123
|
-
.sort((a, b) => b[1] - a[1])
|
|
124
|
-
.slice(0, 15)
|
|
125
|
-
.map(([tag]) => tag);
|
|
126
|
-
|
|
127
|
-
// 分类聚合
|
|
128
|
-
const categoryCounts = new Map<string, number>();
|
|
129
|
-
for (const post of posts) {
|
|
130
|
-
if (post.category) {
|
|
131
|
-
categoryCounts.set(
|
|
132
|
-
post.category,
|
|
133
|
-
(categoryCounts.get(post.category) || 0) + 1
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
const topCategories = [...categoryCounts.entries()]
|
|
138
|
-
.sort((a, b) => b[1] - a[1])
|
|
139
|
-
.slice(0, 5)
|
|
140
|
-
.map(([cat]) => cat);
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
generatedAt: new Date().toISOString(),
|
|
144
|
-
siteUrl,
|
|
145
|
-
sourceInfo: {
|
|
146
|
-
totalPosts: posts.length,
|
|
147
|
-
zhPosts: zhPosts.length,
|
|
148
|
-
enPosts: enPosts.length,
|
|
149
|
-
selectedPosts: selectedPosts.length,
|
|
150
|
-
},
|
|
151
|
-
profile: {
|
|
152
|
-
name: process.env.SITE_AUTHOR || "博主",
|
|
153
|
-
siteUrl,
|
|
154
|
-
},
|
|
155
|
-
posts: selectedPosts.map((post) => ({
|
|
156
|
-
title: post.title,
|
|
157
|
-
date: post.date,
|
|
158
|
-
categories: [post.category].filter(Boolean),
|
|
159
|
-
tags: post.tags.slice(0, 5),
|
|
160
|
-
summary: post.summary,
|
|
161
|
-
keyPoints: post.keyPoints,
|
|
162
|
-
url: post.url,
|
|
163
|
-
})),
|
|
164
|
-
topTags,
|
|
165
|
-
topCategories,
|
|
166
|
-
contentStats: {
|
|
167
|
-
totalPosts: posts.length,
|
|
168
|
-
avgPostPerMonth: Math.round(
|
|
169
|
-
posts.length / Math.max(1, calculateMonthsSpan(posts))
|
|
170
|
-
),
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function calculateMonthsSpan(posts: Post[]): number {
|
|
176
|
-
if (posts.length < 2) return 1;
|
|
177
|
-
const latest = new Date(posts[0].date);
|
|
178
|
-
const earliest = new Date(posts[posts.length - 1].date);
|
|
179
|
-
return (
|
|
180
|
-
(latest.getFullYear() - earliest.getFullYear()) * 12 +
|
|
181
|
-
(latest.getMonth() - earliest.getMonth()) +
|
|
182
|
-
1
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ─── AI 生成 ───────────────────────────────────────────────────
|
|
187
|
-
|
|
188
|
-
async function generateReportWithAI(
|
|
189
|
-
context: ReturnType<typeof buildContext>
|
|
190
|
-
): Promise<unknown> {
|
|
191
|
-
if (!hasAPIKey()) {
|
|
192
|
-
throw new Error("未配置 AI API Key");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const config = getConfig();
|
|
196
|
-
|
|
197
|
-
const systemPrompt = `你是一位中文科技写作编辑。请基于给定上下文,以第三方视角生成作者画像 JSON。
|
|
198
|
-
要求:
|
|
199
|
-
1. 严格输出 JSON,不要输出 Markdown 或多余文本。
|
|
200
|
-
2. 语气客观、克制、具体,不要夸张和空泛。
|
|
201
|
-
3. 结论必须可由上下文支撑,避免编造。
|
|
202
|
-
4. 文案使用中文,第三人称,不使用"我"。
|
|
203
|
-
|
|
204
|
-
输出 schema:
|
|
205
|
-
{
|
|
206
|
-
"report": {
|
|
207
|
-
"hero": {"title":"AI 视角下的作者","summary":"...","intro":"..."},
|
|
208
|
-
"identities":[{"name":"...","description":"...","evidence":"..."}],
|
|
209
|
-
"strengths":[{"title":"...","points":["..."]}],
|
|
210
|
-
"styles":[{"trait":"...","description":"..."}],
|
|
211
|
-
"proofs":{
|
|
212
|
-
"posts":[{"title":"...","url":"...","reason":"...","date":"YYYY-MM-DD"}]
|
|
213
|
-
},
|
|
214
|
-
"disclaimer":"..."
|
|
215
|
-
}
|
|
216
|
-
}`;
|
|
217
|
-
|
|
218
|
-
const contextText = JSON.stringify(context, null, 2).slice(0, 25000);
|
|
219
|
-
const userPrompt = `上下文数据如下,请根据这些信息生成报告:\n${contextText}`;
|
|
220
|
-
|
|
221
|
-
const content = await chatCompletion(
|
|
222
|
-
[
|
|
223
|
-
{ role: "system", content: systemPrompt },
|
|
224
|
-
{ role: "user", content: userPrompt },
|
|
225
|
-
],
|
|
226
|
-
{ maxTokens: 3000, responseFormat: "json" }
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
const parsed = JSON.parse(content);
|
|
230
|
-
return {
|
|
231
|
-
meta: {
|
|
232
|
-
lastUpdated: new Date().toISOString(),
|
|
233
|
-
model: config.model,
|
|
234
|
-
generatedBy: "ai",
|
|
235
|
-
},
|
|
236
|
-
report: parsed.report || parsed,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// ─── 规则模板生成 ─────────────────────────────────────────────
|
|
241
|
-
|
|
242
|
-
function buildRuleBasedReport(
|
|
243
|
-
context: ReturnType<typeof buildContext>
|
|
244
|
-
): unknown {
|
|
245
|
-
const posts = context.posts.slice(0, 5).map((post) => ({
|
|
246
|
-
title: post.title,
|
|
247
|
-
url: post.url,
|
|
248
|
-
reason:
|
|
249
|
-
post.summary?.slice(0, 60) || "该文章体现了作者的写作风格。",
|
|
250
|
-
date: post.date,
|
|
251
|
-
}));
|
|
252
|
-
|
|
253
|
-
const topCategories = context.topCategories.slice(0, 3).join("、");
|
|
254
|
-
const authorName = context.profile.name;
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
meta: {
|
|
258
|
-
lastUpdated: new Date().toISOString(),
|
|
259
|
-
model: "rule-based-template",
|
|
260
|
-
generatedBy: "rule-based",
|
|
261
|
-
},
|
|
262
|
-
report: {
|
|
263
|
-
hero: {
|
|
264
|
-
title: `AI 视角下的 ${authorName}`,
|
|
265
|
-
summary: `一位专注于${topCategories}领域的博主,持续输出高质量内容。`,
|
|
266
|
-
intro: `博客已发布 ${context.sourceInfo.totalPosts} 篇文章,涵盖 ${context.topTags.length} 个主题标签。`,
|
|
267
|
-
},
|
|
268
|
-
identities: [
|
|
269
|
-
{
|
|
270
|
-
name: "技术博主",
|
|
271
|
-
description: "持续分享技术经验与实践心得。",
|
|
272
|
-
evidence: `已发布 ${context.sourceInfo.totalPosts} 篇文章。`,
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
name: "内容创作者",
|
|
276
|
-
description: "注重内容质量与读者体验。",
|
|
277
|
-
evidence: posts[0]
|
|
278
|
-
? `近期文章《${posts[0].title}》体现了专业的写作风格。`
|
|
279
|
-
: "",
|
|
280
|
-
},
|
|
281
|
-
],
|
|
282
|
-
strengths: [
|
|
283
|
-
{
|
|
284
|
-
title: "内容深度",
|
|
285
|
-
points: [
|
|
286
|
-
"文章结构清晰,逻辑性强",
|
|
287
|
-
"注重实践,配合代码示例",
|
|
288
|
-
"持续更新,覆盖多个技术领域",
|
|
289
|
-
],
|
|
290
|
-
},
|
|
291
|
-
{
|
|
292
|
-
title: "表达风格",
|
|
293
|
-
points: ["语言简洁明了", "注重可读性", "善于总结提炼"],
|
|
294
|
-
},
|
|
295
|
-
],
|
|
296
|
-
styles: [
|
|
297
|
-
{
|
|
298
|
-
trait: "技术导向",
|
|
299
|
-
description: "专注于技术内容的深度讲解与实践分享。",
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
trait: "结构化表达",
|
|
303
|
-
description: "文章结构清晰,便于读者理解和学习。",
|
|
304
|
-
},
|
|
305
|
-
],
|
|
306
|
-
proofs: { posts },
|
|
307
|
-
disclaimer:
|
|
308
|
-
"该页面由 AI 归纳与规则模板联合生成,旨在帮助访客快速建立认知,可能存在概括偏差,请以原始文章信息为准。",
|
|
309
|
-
},
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ─── 主流程 ───────────────────────────────────────────────────
|
|
314
|
-
|
|
315
|
-
async function main() {
|
|
316
|
-
const args = parseArgs();
|
|
317
|
-
await loadEnv();
|
|
318
|
-
|
|
319
|
-
console.log("👤 生成作者画像报告");
|
|
320
|
-
console.log("━".repeat(50));
|
|
321
|
-
|
|
322
|
-
const siteUrl = process.env.SITE_URL || DEFAULT_SITE_URL;
|
|
323
|
-
console.log(` 站点 URL: ${siteUrl}`);
|
|
324
|
-
console.log(` 输出目录: ${DATA_DIR}`);
|
|
325
|
-
console.log("");
|
|
326
|
-
|
|
327
|
-
// 收集数据
|
|
328
|
-
console.log("📂 收集文章数据...");
|
|
329
|
-
const posts = await collectPosts(siteUrl);
|
|
330
|
-
console.log(` 找到 ${posts.length} 篇文章`);
|
|
331
|
-
|
|
332
|
-
const context = buildContext(posts, siteUrl);
|
|
333
|
-
await writeJson(OUTPUT_CONTEXT, context);
|
|
334
|
-
console.log(` 上下文已保存: ${OUTPUT_CONTEXT}`);
|
|
335
|
-
|
|
336
|
-
// 生成报告
|
|
337
|
-
let report: unknown;
|
|
338
|
-
|
|
339
|
-
if (args.noAI) {
|
|
340
|
-
console.log("\n📝 使用规则模板生成...");
|
|
341
|
-
report = buildRuleBasedReport(context);
|
|
342
|
-
} else {
|
|
343
|
-
console.log("\n🤖 使用 AI 生成...");
|
|
344
|
-
try {
|
|
345
|
-
report = await generateReportWithAI(context);
|
|
346
|
-
console.log(" ✅ AI 生成成功");
|
|
347
|
-
} catch (error) {
|
|
348
|
-
const err = error as Error;
|
|
349
|
-
if (!args.force) {
|
|
350
|
-
console.warn(` ⚠️ AI 生成失败: ${err.message}`);
|
|
351
|
-
console.log(" 📝 回退使用规则模板...");
|
|
352
|
-
report = buildRuleBasedReport(context);
|
|
353
|
-
} else {
|
|
354
|
-
throw error;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
await writeJson(OUTPUT_REPORT, report);
|
|
360
|
-
|
|
361
|
-
console.log("\n✅ 画像报告生成完成");
|
|
362
|
-
console.log(`📄 报告文件: ${OUTPUT_REPORT}`);
|
|
363
|
-
console.log(`🧩 上下文文件: ${OUTPUT_CONTEXT}`);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
main().catch((error) => {
|
|
367
|
-
console.error("❌ 生成失败:", error.message);
|
|
368
|
-
process.exit(1);
|
|
369
|
-
});
|