@astro-minimax/cli 0.5.0 → 0.7.0

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.
Files changed (142) hide show
  1. package/README.md +69 -0
  2. package/dist/commands/ai.d.ts +2 -0
  3. package/dist/commands/ai.d.ts.map +1 -0
  4. package/dist/commands/ai.js +99 -0
  5. package/dist/commands/ai.js.map +1 -0
  6. package/dist/commands/data.d.ts +2 -0
  7. package/dist/commands/data.d.ts.map +1 -0
  8. package/dist/commands/data.js +111 -0
  9. package/dist/commands/data.js.map +1 -0
  10. package/dist/commands/hooks.d.ts +2 -0
  11. package/dist/commands/hooks.d.ts.map +1 -0
  12. package/dist/commands/hooks.js +378 -0
  13. package/dist/commands/hooks.js.map +1 -0
  14. package/dist/commands/init.d.ts +2 -0
  15. package/dist/commands/init.d.ts.map +1 -0
  16. package/dist/commands/init.js +50 -0
  17. package/dist/commands/init.js.map +1 -0
  18. package/dist/commands/post.d.ts +2 -0
  19. package/dist/commands/post.d.ts.map +1 -0
  20. package/dist/commands/post.js +190 -0
  21. package/dist/commands/post.js.map +1 -0
  22. package/dist/commands/profile.d.ts +2 -0
  23. package/dist/commands/profile.d.ts.map +1 -0
  24. package/dist/commands/profile.js +88 -0
  25. package/dist/commands/profile.js.map +1 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +81 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/tools/ai-process.d.ts +20 -0
  31. package/dist/tools/ai-process.d.ts.map +1 -0
  32. package/dist/tools/ai-process.js +607 -0
  33. package/dist/tools/ai-process.js.map +1 -0
  34. package/dist/tools/build-author-context.d.ts +13 -0
  35. package/dist/tools/build-author-context.d.ts.map +1 -0
  36. package/dist/tools/build-author-context.js +313 -0
  37. package/dist/tools/build-author-context.js.map +1 -0
  38. package/dist/tools/build-voice-profile.d.ts +12 -0
  39. package/dist/tools/build-voice-profile.d.ts.map +1 -0
  40. package/dist/tools/build-voice-profile.js +270 -0
  41. package/dist/tools/build-voice-profile.js.map +1 -0
  42. package/dist/tools/eval-ai-chat.d.ts +17 -0
  43. package/dist/tools/eval-ai-chat.d.ts.map +1 -0
  44. package/dist/tools/eval-ai-chat.js +332 -0
  45. package/dist/tools/eval-ai-chat.js.map +1 -0
  46. package/dist/tools/generate-author-profile.d.ts +14 -0
  47. package/dist/tools/generate-author-profile.d.ts.map +1 -0
  48. package/dist/tools/generate-author-profile.js +289 -0
  49. package/dist/tools/generate-author-profile.js.map +1 -0
  50. package/dist/tools/generate-cover.d.ts +14 -0
  51. package/dist/tools/generate-cover.d.ts.map +1 -0
  52. package/dist/tools/generate-cover.js +95 -0
  53. package/dist/tools/generate-cover.js.map +1 -0
  54. package/dist/tools/generate-og.d.ts +3 -0
  55. package/dist/tools/generate-og.d.ts.map +1 -0
  56. package/dist/tools/generate-og.js +254 -0
  57. package/dist/tools/generate-og.js.map +1 -0
  58. package/dist/tools/generate-related.d.ts +11 -0
  59. package/dist/tools/generate-related.d.ts.map +1 -0
  60. package/dist/tools/generate-related.js +124 -0
  61. package/dist/tools/generate-related.js.map +1 -0
  62. package/dist/tools/generate-tags.d.ts +14 -0
  63. package/dist/tools/generate-tags.d.ts.map +1 -0
  64. package/dist/tools/generate-tags.js +182 -0
  65. package/dist/tools/generate-tags.js.map +1 -0
  66. package/dist/tools/lib/ai-provider.d.ts +43 -0
  67. package/dist/tools/lib/ai-provider.d.ts.map +1 -0
  68. package/dist/tools/lib/ai-provider.js +146 -0
  69. package/dist/tools/lib/ai-provider.js.map +1 -0
  70. package/dist/tools/lib/frontmatter.d.ts +11 -0
  71. package/dist/tools/lib/frontmatter.d.ts.map +1 -0
  72. package/dist/tools/lib/frontmatter.js +80 -0
  73. package/dist/tools/lib/frontmatter.js.map +1 -0
  74. package/dist/tools/lib/index.d.ts +7 -0
  75. package/dist/tools/lib/index.d.ts.map +1 -0
  76. package/{template/tools/lib/index.ts → dist/tools/lib/index.js} +1 -0
  77. package/dist/tools/lib/index.js.map +1 -0
  78. package/dist/tools/lib/markdown.d.ts +6 -0
  79. package/dist/tools/lib/markdown.d.ts.map +1 -0
  80. package/dist/tools/lib/markdown.js +34 -0
  81. package/dist/tools/lib/markdown.js.map +1 -0
  82. package/dist/tools/lib/posts.d.ts +25 -0
  83. package/dist/tools/lib/posts.d.ts.map +1 -0
  84. package/dist/tools/lib/posts.js +63 -0
  85. package/dist/tools/lib/posts.js.map +1 -0
  86. package/dist/tools/lib/utils.d.ts +18 -0
  87. package/dist/tools/lib/utils.d.ts.map +1 -0
  88. package/dist/tools/lib/utils.js +121 -0
  89. package/dist/tools/lib/utils.js.map +1 -0
  90. package/dist/tools/lib/vectors.d.ts +27 -0
  91. package/dist/tools/lib/vectors.d.ts.map +1 -0
  92. package/dist/tools/lib/vectors.js +64 -0
  93. package/dist/tools/lib/vectors.js.map +1 -0
  94. package/dist/tools/summarize.d.ts +16 -0
  95. package/dist/tools/summarize.d.ts.map +1 -0
  96. package/dist/tools/summarize.js +108 -0
  97. package/dist/tools/summarize.js.map +1 -0
  98. package/dist/tools/translate.d.ts +13 -0
  99. package/dist/tools/translate.d.ts.map +1 -0
  100. package/dist/tools/translate.js +46 -0
  101. package/dist/tools/translate.js.map +1 -0
  102. package/dist/tools/vectorize.d.ts +13 -0
  103. package/dist/tools/vectorize.d.ts.map +1 -0
  104. package/dist/tools/vectorize.js +87 -0
  105. package/dist/tools/vectorize.js.map +1 -0
  106. package/package.json +14 -9
  107. package/template/astro.config.ts +8 -28
  108. package/template/datas/ai-seo.json +8 -0
  109. package/template/datas/ai-skip-list.json +1 -0
  110. package/template/datas/author-profile-context.json +21 -0
  111. package/template/datas/author-profile-report.json +21 -0
  112. package/template/datas/eval/gold-set.json +72 -0
  113. package/template/functions/README.md +82 -0
  114. package/template/functions/api/ai-info.ts +2 -2
  115. package/template/functions/api/chat.ts +4 -1
  116. package/template/functions/api/notify/comment.ts +140 -68
  117. package/template/functions/api/notify/debug.ts +41 -0
  118. package/template/functions/api/notify/status.ts +97 -0
  119. package/template/functions/api/notify/test-ai-chat.ts +67 -0
  120. package/template/package.json +22 -25
  121. package/template/src/config.ts +11 -0
  122. package/template/src/content.config.ts +29 -16
  123. package/template/src/env.d.ts +0 -5
  124. package/index.js +0 -36
  125. package/template/tools/README.md +0 -169
  126. package/template/tools/ai-process.ts +0 -816
  127. package/template/tools/build-author-context.ts +0 -405
  128. package/template/tools/build-voice-profile.ts +0 -322
  129. package/template/tools/generate-author-profile.ts +0 -369
  130. package/template/tools/generate-cover.ts +0 -123
  131. package/template/tools/generate-og.ts +0 -280
  132. package/template/tools/generate-related.ts +0 -146
  133. package/template/tools/generate-tags.ts +0 -251
  134. package/template/tools/lib/ai-provider.ts +0 -240
  135. package/template/tools/lib/frontmatter.ts +0 -94
  136. package/template/tools/lib/markdown.ts +0 -40
  137. package/template/tools/lib/posts.ts +0 -89
  138. package/template/tools/lib/utils.ts +0 -138
  139. package/template/tools/lib/vectors.ts +0 -96
  140. package/template/tools/summarize.ts +0 -142
  141. package/template/tools/translate.ts +0 -60
  142. package/template/tools/vectorize.ts +0 -105
@@ -1,251 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- /**
3
- * AI 标签与分类自动生成工具
4
- *
5
- * 用法:
6
- * pnpm run tools:tags <文章路径> # 分析并推荐标签和分类
7
- * pnpm run tools:tags <文章路径> --write # 推荐并写入 frontmatter
8
- * pnpm run tools:tags --all # 分析所有文章(dry-run)
9
- *
10
- * 环境变量:
11
- * AI_API_KEY / OPENAI_API_KEY(可选,无 key 时使用关键词匹配)
12
- */
13
-
14
- import { readFile, writeFile } from "node:fs/promises";
15
- import { join } from "node:path";
16
- import { extractFrontmatter } from "./lib/frontmatter.js";
17
- import { stripMarkdown } from "./lib/markdown.js";
18
- import {
19
- getAllPosts,
20
- getExistingTaxonomy,
21
- type PostMeta,
22
- } from "./lib/posts.js";
23
- import { chatCompletion, hasAPIKey } from "./lib/ai-provider.js";
24
-
25
- interface TagSuggestion {
26
- tags: string[];
27
- category: string;
28
- reasoning: string;
29
- }
30
-
31
- const KEYWORD_TAG_MAP: Record<string, string[]> = {
32
- astro: ["astro"],
33
- react: ["reactjs"],
34
- vue: ["vue"],
35
- next: ["nextjs"],
36
- tailwind: ["tailwindcss"],
37
- typescript: ["typescript"],
38
- javascript: ["javascript"],
39
- css: ["css"],
40
- html: ["html"],
41
- markdown: ["markdown"],
42
- mdx: ["markdown"],
43
- api: ["api"],
44
- database: ["database"],
45
- docker: ["docker"],
46
- git: ["git"],
47
- python: ["python"],
48
- rust: ["rust"],
49
- node: ["nodejs"],
50
- deploy: ["deployment"],
51
- test: ["testing"],
52
- performance: ["performance"],
53
- seo: ["seo"],
54
- a11y: ["accessibility"],
55
- ai: ["ai"],
56
- 机器学习: ["ai", "machine-learning"],
57
- 深度学习: ["ai", "deep-learning"],
58
- 大模型: ["ai", "llm"],
59
- };
60
-
61
- function localTagSuggestion(
62
- post: PostMeta,
63
- existingTags: string[]
64
- ): TagSuggestion {
65
- const text = `${post.title} ${post.description} ${post.body}`.toLowerCase();
66
- const suggestedTags = new Set<string>();
67
-
68
- for (const [keyword, tags] of Object.entries(KEYWORD_TAG_MAP)) {
69
- if (text.includes(keyword.toLowerCase())) {
70
- tags.forEach(t => suggestedTags.add(t));
71
- }
72
- }
73
-
74
- for (const tag of existingTags) {
75
- if (text.includes(tag.toLowerCase()) && !suggestedTags.has(tag)) {
76
- suggestedTags.add(tag);
77
- }
78
- }
79
-
80
- const category = post.category || inferCategory(text);
81
-
82
- return {
83
- tags: Array.from(suggestedTags).slice(0, 6),
84
- category,
85
- reasoning: "基于关键词匹配(本地模式,无需 API Key)",
86
- };
87
- }
88
-
89
- function inferCategory(text: string): string {
90
- if (
91
- text.includes("教程") ||
92
- text.includes("tutorial") ||
93
- text.includes("how to")
94
- )
95
- return "教程";
96
- if (text.includes("配置") || text.includes("config")) return "教程/配置";
97
- if (text.includes("发布") || text.includes("release")) return "发布";
98
- if (
99
- text.includes("示例") ||
100
- text.includes("example") ||
101
- text.includes("showcase")
102
- )
103
- return "示例";
104
- return "技术";
105
- }
106
-
107
- async function aiTagSuggestion(
108
- post: PostMeta,
109
- existingTags: string[],
110
- existingCategories: string[]
111
- ): Promise<TagSuggestion> {
112
- const contentPreview = post.body.slice(0, 3000);
113
-
114
- const prompt = `分析以下博客文章,推荐合适的标签和分类。
115
-
116
- 文章标题: ${post.title}
117
- 文章描述: ${post.description}
118
- 正文摘要: ${contentPreview}
119
-
120
- 现有标签体系: ${existingTags.join(", ")}
121
- 现有分类体系: ${existingCategories.join(", ")}
122
-
123
- 请优先使用现有标签和分类以保持一致性。如果确实需要新标签也可以。
124
-
125
- 回复 JSON 格式:
126
- {
127
- "tags": ["tag1", "tag2", "tag3"],
128
- "category": "分类名",
129
- "reasoning": "推荐理由简述"
130
- }`;
131
-
132
- const result = await chatCompletion(
133
- [
134
- {
135
- role: "system",
136
- content:
137
- "你是一个技术博客编辑,擅长内容分类。只回复 JSON,不要其他文字。",
138
- },
139
- { role: "user", content: prompt },
140
- ],
141
- { maxTokens: 200, responseFormat: "json" }
142
- );
143
-
144
- return JSON.parse(result) as TagSuggestion;
145
- }
146
-
147
- async function main() {
148
- const args = process.argv.slice(2);
149
- const all = args.includes("--all");
150
- const write = args.includes("--write");
151
- const filePath = args.find(a => !a.startsWith("--"));
152
-
153
- const allPosts = await getAllPosts();
154
- const { tags: existingTags, categories: existingCategories } =
155
- getExistingTaxonomy(allPosts);
156
-
157
- console.log(
158
- `📊 现有标签 (${existingTags.length}): ${existingTags.slice(0, 15).join(", ")}...`
159
- );
160
- console.log(
161
- `📁 现有分类 (${existingCategories.length}): ${existingCategories.join(", ")}\n`
162
- );
163
-
164
- const postsToAnalyze: PostMeta[] = [];
165
-
166
- if (all) {
167
- postsToAnalyze.push(...allPosts);
168
- } else if (filePath) {
169
- const fullPath = join(process.cwd(), filePath);
170
- const content = await readFile(fullPath, "utf-8");
171
- const fm = extractFrontmatter(content);
172
-
173
- postsToAnalyze.push({
174
- id: filePath,
175
- filePath: fullPath,
176
- lang: "zh",
177
- title: (fm.data.title as string) || "",
178
- description: (fm.data.description as string) || "",
179
- tags: Array.isArray(fm.data.tags) ? (fm.data.tags as string[]) : [],
180
- category: (fm.data.category as string) || "",
181
- body: stripMarkdown(content),
182
- });
183
- } else {
184
- console.error(
185
- "用法: pnpm run tools:tags <文章路径> [--write] 或 pnpm run tools:tags --all"
186
- );
187
- process.exit(1);
188
- }
189
-
190
- const mode = hasAPIKey() ? "AI" : "关键词匹配";
191
- console.log(
192
- `🔍 使用 ${mode} 模式分析 ${postsToAnalyze.length} 篇文章...\n`
193
- );
194
-
195
- for (const post of postsToAnalyze) {
196
- let suggestion: TagSuggestion;
197
-
198
- try {
199
- if (hasAPIKey()) {
200
- suggestion = await aiTagSuggestion(
201
- post,
202
- existingTags,
203
- existingCategories
204
- );
205
- } else {
206
- suggestion = localTagSuggestion(post, existingTags);
207
- }
208
- } catch (err) {
209
- console.error(
210
- `❌ 分析失败 [${post.title}]:`,
211
- (err as Error).message
212
- );
213
- continue;
214
- }
215
-
216
- console.log(`📝 【${post.title}】`);
217
- console.log(` 当前标签: [${post.tags.join(", ")}]`);
218
- console.log(` 推荐标签: [${suggestion.tags.join(", ")}]`);
219
- console.log(` 当前分类: ${post.category || "(无)"}`);
220
- console.log(` 推荐分类: ${suggestion.category}`);
221
- console.log(` 理由: ${suggestion.reasoning}\n`);
222
-
223
- if (write && !all) {
224
- const content = await readFile(post.filePath, "utf-8");
225
- let newContent = content;
226
-
227
- const tagsYaml = suggestion.tags.map(t => ` - ${t}`).join("\n");
228
- newContent = newContent.replace(
229
- /^tags:\n((?:\s+-\s+.*\n)*)/m,
230
- `tags:\n${tagsYaml}\n`
231
- );
232
-
233
- if (post.category) {
234
- newContent = newContent.replace(
235
- /^category:.*$/m,
236
- `category: ${suggestion.category}`
237
- );
238
- } else {
239
- newContent = newContent.replace(
240
- /^(tags:\n(?:\s+-\s+.*\n)*)/,
241
- `$1category: ${suggestion.category}\n`
242
- );
243
- }
244
-
245
- await writeFile(post.filePath, newContent, "utf-8");
246
- console.log(` ✍️ 已更新 frontmatter\n`);
247
- }
248
- }
249
- }
250
-
251
- main().catch(console.error);
@@ -1,240 +0,0 @@
1
- import { fetch, ProxyAgent, type Dispatcher } from "undici";
2
-
3
- /**
4
- * 通用 AI Provider 抽象层
5
- *
6
- * 统一管理所有 AI API 调用,支持多种 provider 和自定义端点。
7
- * 工具脚本不应直接调用任何 AI API,而是通过此模块调用。
8
- *
9
- * 配置方式(环境变量):
10
- * AI_PROVIDER — 使用的 provider: "openai" | "anthropic" | "custom" (默认 "openai")
11
- * AI_API_KEY — API Key(也兼容 OPENAI_API_KEY / ANTHROPIC_API_KEY)
12
- * AI_BASE_URL — 自定义 API 基础 URL(用于兼容 OpenAI 接口的第三方服务)
13
- * AI_MODEL — 默认模型名称
14
- */
15
-
16
- export type AIProvider = "openai" | "anthropic" | "custom";
17
-
18
- export interface AIConfig {
19
- provider: AIProvider;
20
- apiKey: string;
21
- baseUrl: string;
22
- model: string;
23
- }
24
-
25
- export interface ChatMessage {
26
- role: "system" | "user" | "assistant";
27
- content: string;
28
- }
29
-
30
- export interface ChatOptions {
31
- model?: string;
32
- maxTokens?: number;
33
- responseFormat?: "text" | "json";
34
- }
35
-
36
- export interface EmbeddingOptions {
37
- model?: string;
38
- batchSize?: number;
39
- }
40
-
41
- export interface ImageOptions {
42
- model?: string;
43
- size?: string;
44
- quality?: string;
45
- }
46
-
47
- function resolveConfig(): AIConfig {
48
- const provider = (process.env.AI_PROVIDER || "openai") as AIProvider;
49
- const apiKey =
50
- process.env.AI_API_KEY ||
51
- process.env.OPENAI_API_KEY ||
52
- process.env.ANTHROPIC_API_KEY ||
53
- "";
54
- const baseUrl =
55
- process.env.AI_BASE_URL ||
56
- (provider === "anthropic"
57
- ? "https://api.anthropic.com"
58
- : "https://api.openai.com");
59
- const model = process.env.AI_MODEL || "gpt-4o-mini";
60
-
61
- return { provider, apiKey, baseUrl, model };
62
- }
63
-
64
- export function getConfig(): AIConfig {
65
- return resolveConfig();
66
- }
67
-
68
- export function hasAPIKey(): boolean {
69
- const cfg = resolveConfig();
70
- return cfg.apiKey.length > 0;
71
- }
72
-
73
- /**
74
- * 获取代理 dispatcher(如果配置了代理)
75
- */
76
- function getProxyDispatcher(): Dispatcher | undefined {
77
- const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy ||
78
- process.env.HTTP_PROXY || process.env.http_proxy;
79
- if (proxyUrl) {
80
- return new ProxyAgent(proxyUrl);
81
- }
82
- return undefined;
83
- }
84
-
85
- export async function chatCompletion(
86
- messages: ChatMessage[],
87
- options?: ChatOptions
88
- ): Promise<string> {
89
- const cfg = resolveConfig();
90
- if (!cfg.apiKey) {
91
- throw new Error(
92
- "AI API Key 未设置。请设置 AI_API_KEY 或 OPENAI_API_KEY 环境变量。"
93
- );
94
- }
95
-
96
- const model = options?.model || cfg.model;
97
- const maxTokens = options?.maxTokens || 1024;
98
-
99
- // 处理 baseUrl 可能已经包含 /v1 的情况
100
- const baseUrl = cfg.baseUrl.replace(/\/v1\/?$/, '');
101
- const url = `${baseUrl}/v1/chat/completions`;
102
- const body: Record<string, unknown> = {
103
- model,
104
- messages,
105
- max_tokens: maxTokens,
106
- };
107
-
108
- if (options?.responseFormat === "json") {
109
- body.response_format = { type: "json_object" };
110
- }
111
-
112
- let response: Awaited<ReturnType<typeof fetch>>;
113
- try {
114
- const dispatcher = getProxyDispatcher();
115
- response = await fetch(url, {
116
- method: "POST",
117
- headers: {
118
- "Content-Type": "application/json",
119
- Authorization: `Bearer ${cfg.apiKey}`,
120
- },
121
- body: JSON.stringify(body),
122
- ...(dispatcher && { dispatcher }),
123
- });
124
- } catch (fetchErr) {
125
- const err = fetchErr as Error;
126
- throw new Error(
127
- `网络请求失败: ${err.message}\n` +
128
- ` 请检查: API URL 是否正确 (${cfg.baseUrl})\n` +
129
- ` 网络是否可访问该端点`
130
- );
131
- }
132
-
133
- if (!response.ok) {
134
- const errText = await response.text();
135
- throw new Error(`API 错误 (${response.status}): ${errText.slice(0, 200)}`);
136
- }
137
-
138
- const data = (await response.json()) as {
139
- choices?: { message?: { content?: string } }[];
140
- };
141
-
142
- return data.choices?.[0]?.message?.content?.trim() || "";
143
- }
144
-
145
- export async function generateEmbeddings(
146
- texts: string[],
147
- options?: EmbeddingOptions
148
- ): Promise<number[][]> {
149
- const cfg = resolveConfig();
150
- if (!cfg.apiKey) {
151
- throw new Error("AI API Key 未设置。");
152
- }
153
-
154
- const model = options?.model || "text-embedding-3-small";
155
- const batchSize = options?.batchSize || 20;
156
- const allEmbeddings: number[][] = [];
157
-
158
- for (let i = 0; i < texts.length; i += batchSize) {
159
- const batch = texts.slice(i, i + batchSize).map(t => t.slice(0, 8000));
160
-
161
- const dispatcher = getProxyDispatcher();
162
- const baseUrl = cfg.baseUrl.replace(/\/v1\/?$/, '');
163
- const response = await fetch(`${baseUrl}/v1/embeddings`, {
164
- method: "POST",
165
- headers: {
166
- "Content-Type": "application/json",
167
- Authorization: `Bearer ${cfg.apiKey}`,
168
- },
169
- body: JSON.stringify({ model, input: batch }),
170
- ...(dispatcher && { dispatcher }),
171
- });
172
-
173
- if (!response.ok) {
174
- const errText = await response.text();
175
- throw new Error(`Embeddings API 错误 (${response.status}): ${errText}`);
176
- }
177
-
178
- const data = (await response.json()) as {
179
- data: { embedding: number[] }[];
180
- };
181
-
182
- allEmbeddings.push(...data.data.map(d => d.embedding));
183
-
184
- if (i + batchSize < texts.length) {
185
- await new Promise(r => setTimeout(r, 200));
186
- }
187
- }
188
-
189
- return allEmbeddings;
190
- }
191
-
192
- export async function generateImage(
193
- prompt: string,
194
- options?: ImageOptions
195
- ): Promise<Uint8Array> {
196
- const cfg = resolveConfig();
197
- if (!cfg.apiKey) {
198
- throw new Error("AI API Key 未设置。");
199
- }
200
-
201
- const model = options?.model || "dall-e-3";
202
- const size = options?.size || "1792x1024";
203
- const quality = options?.quality || "standard";
204
-
205
- const dispatcher = getProxyDispatcher();
206
- const baseUrl = cfg.baseUrl.replace(/\/v1\/?$/, '');
207
- const response = await fetch(`${baseUrl}/v1/images/generations`, {
208
- method: "POST",
209
- headers: {
210
- "Content-Type": "application/json",
211
- Authorization: `Bearer ${cfg.apiKey}`,
212
- },
213
- body: JSON.stringify({
214
- model,
215
- prompt,
216
- n: 1,
217
- size,
218
- quality,
219
- response_format: "b64_json",
220
- }),
221
- ...(dispatcher && { dispatcher }),
222
- });
223
-
224
- if (!response.ok) {
225
- const errText = await response.text();
226
- throw new Error(`Image API 错误 (${response.status}): ${errText}`);
227
- }
228
-
229
- const result = (await response.json()) as {
230
- data: { b64_json: string; revised_prompt?: string }[];
231
- };
232
-
233
- if (result.data[0].revised_prompt) {
234
- console.log(
235
- ` 修正后的提示词: ${result.data[0].revised_prompt.slice(0, 100)}...`
236
- );
237
- }
238
-
239
- return new Uint8Array(Buffer.from(result.data[0].b64_json, "base64"));
240
- }
@@ -1,94 +0,0 @@
1
- /**
2
- * Frontmatter 解析与更新工具
3
- */
4
-
5
- export interface FrontmatterResult {
6
- raw: string;
7
- body: string;
8
- data: Record<string, unknown>;
9
- }
10
-
11
- export function extractFrontmatter(content: string): FrontmatterResult {
12
- const match = content.match(/^(---\n[\s\S]*?\n---)\n?([\s\S]*)/);
13
- if (!match) return { raw: "", body: content, data: {} };
14
-
15
- const raw = match[1];
16
- const body = match[2];
17
- const data: Record<string, unknown> = {};
18
-
19
- const lines = raw.replace(/^---\n/, "").replace(/\n---$/, "").split("\n");
20
- let currentKey = "";
21
- let arrayValues: string[] = [];
22
- let inArray = false;
23
-
24
- for (const line of lines) {
25
- if (inArray) {
26
- if (line.match(/^\s+-\s+/)) {
27
- arrayValues.push(line.replace(/^\s+-\s+/, "").trim());
28
- continue;
29
- } else {
30
- data[currentKey] = arrayValues;
31
- inArray = false;
32
- arrayValues = [];
33
- }
34
- }
35
-
36
- const colonIndex = line.indexOf(":");
37
- if (colonIndex === -1) continue;
38
-
39
- const key = line.slice(0, colonIndex).trim();
40
- const value = line.slice(colonIndex + 1).trim();
41
-
42
- if (value === "") {
43
- currentKey = key;
44
- inArray = true;
45
- arrayValues = [];
46
- continue;
47
- }
48
-
49
- if (value === "true") data[key] = true;
50
- else if (value === "false") data[key] = false;
51
- else data[key] = value;
52
- }
53
-
54
- if (inArray) data[currentKey] = arrayValues;
55
-
56
- return { raw, body, data };
57
- }
58
-
59
- export function updateFrontmatterField(
60
- content: string,
61
- field: string,
62
- value: string | string[]
63
- ): string {
64
- const lines = content.split("\n");
65
- const closingIdx = lines.indexOf("---", 1);
66
- if (closingIdx < 0) return content;
67
-
68
- const fieldRegex = new RegExp(`^${field}:`);
69
- const existingIdx = lines.findIndex(
70
- (l, i) => i > 0 && i < closingIdx && fieldRegex.test(l)
71
- );
72
-
73
- if (Array.isArray(value)) {
74
- const yamlArray = value.map(v => ` - ${v}`).join("\n");
75
- const newBlock = `${field}:\n${yamlArray}`;
76
-
77
- if (existingIdx >= 0) {
78
- let endIdx = existingIdx + 1;
79
- while (endIdx < closingIdx && lines[endIdx].match(/^\s+-\s+/)) endIdx++;
80
- lines.splice(existingIdx, endIdx - existingIdx, newBlock);
81
- } else {
82
- lines.splice(closingIdx, 0, newBlock);
83
- }
84
- } else {
85
- const newLine = `${field}: ${value}`;
86
- if (existingIdx >= 0) {
87
- lines[existingIdx] = newLine;
88
- } else {
89
- lines.splice(closingIdx, 0, newLine);
90
- }
91
- }
92
-
93
- return lines.join("\n");
94
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * Markdown 文本处理工具
3
- */
4
-
5
- export function stripMarkdown(content: string): string {
6
- return content
7
- .replace(/^---[\s\S]*?---\n?/, "")
8
- .replace(/^import\s+.*$/gm, "")
9
- .replace(/<[^>]+\/>/g, "")
10
- .replace(/<[^>]+>[\s\S]*?<\/[^>]+>/g, "")
11
- .replace(/```[\s\S]*?```/g, "")
12
- .replace(/`[^`]*`/g, "")
13
- .replace(/!\[.*?\]\(.*?\)/g, "")
14
- .replace(/\[([^\]]*)\]\(.*?\)/g, "$1")
15
- .replace(/#{1,6}\s+/g, "")
16
- .replace(/[*_~]+/g, "")
17
- .replace(/^\s*[-*+]\s+/gm, "")
18
- .replace(/^\s*\d+\.\s+/gm, "")
19
- .replace(/\n{3,}/g, "\n\n")
20
- .trim();
21
- }
22
-
23
- export function chunkText(
24
- text: string,
25
- size: number = 500,
26
- overlap: number = 50
27
- ): string[] {
28
- const words = text.split(/\s+/);
29
- const chunks: string[] = [];
30
-
31
- for (let i = 0; i < words.length; i += size - overlap) {
32
- const chunk = words.slice(i, i + size).join(" ");
33
- if (chunk.trim().length > 20) {
34
- chunks.push(chunk.trim());
35
- }
36
- if (i + size >= words.length) break;
37
- }
38
-
39
- return chunks.length > 0 ? chunks : [text.slice(0, 2000)];
40
- }
@@ -1,89 +0,0 @@
1
- /**
2
- * 博客文章读取与遍历工具
3
- */
4
-
5
- import { readdir, readFile } from "node:fs/promises";
6
- import { join } from "node:path";
7
- import { extractFrontmatter } from "./frontmatter.js";
8
- import { stripMarkdown } from "./markdown.js";
9
-
10
- export const BLOG_PATH = join(process.cwd(), "src/data/blog");
11
-
12
- export interface PostMeta {
13
- id: string;
14
- filePath: string;
15
- lang: string;
16
- title: string;
17
- description: string;
18
- tags: string[];
19
- category: string;
20
- body: string;
21
- draft?: boolean;
22
- }
23
-
24
- export function getPostURL(id: string): string {
25
- const parts = id.split("/");
26
- const lang = parts[0];
27
- const slug = parts.slice(1).join("/");
28
- return `/${lang}/posts/${slug}/`;
29
- }
30
-
31
- export async function getAllPosts(opts?: {
32
- includeDrafts?: boolean;
33
- stripBody?: boolean;
34
- }): Promise<PostMeta[]> {
35
- const posts: PostMeta[] = [];
36
-
37
- async function walk(dir: string) {
38
- const entries = await readdir(dir, { withFileTypes: true });
39
- for (const entry of entries) {
40
- const fullPath = join(dir, entry.name);
41
- if (entry.isDirectory() && !entry.name.startsWith("_")) {
42
- await walk(fullPath);
43
- } else if (entry.name.endsWith(".md") || entry.name.endsWith(".mdx")) {
44
- const content = await readFile(fullPath, "utf-8");
45
- const fm = extractFrontmatter(content);
46
- const isDraft = fm.data.draft === true;
47
-
48
- if (isDraft && !opts?.includeDrafts) continue;
49
-
50
- const relativePath = fullPath.replace(BLOG_PATH + "/", "");
51
- const lang = relativePath.startsWith("en/") ? "en" : "zh";
52
- const id = relativePath.replace(/\.(md|mdx)$/, "");
53
-
54
- posts.push({
55
- id,
56
- filePath: fullPath,
57
- lang,
58
- title: (fm.data.title as string) || "",
59
- description: (fm.data.description as string) || "",
60
- tags: Array.isArray(fm.data.tags) ? (fm.data.tags as string[]) : [],
61
- category: (fm.data.category as string) || "",
62
- body: opts?.stripBody === false ? fm.body : stripMarkdown(content),
63
- draft: isDraft,
64
- });
65
- }
66
- }
67
- }
68
-
69
- await walk(BLOG_PATH);
70
- return posts;
71
- }
72
-
73
- export function getExistingTaxonomy(posts: PostMeta[]): {
74
- tags: string[];
75
- categories: string[];
76
- } {
77
- const tagSet = new Set<string>();
78
- const catSet = new Set<string>();
79
-
80
- for (const post of posts) {
81
- post.tags.forEach(t => tagSet.add(t));
82
- if (post.category) catSet.add(post.category);
83
- }
84
-
85
- return {
86
- tags: Array.from(tagSet).sort(),
87
- categories: Array.from(catSet).sort(),
88
- };
89
- }