repowiki-core 0.1.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 (157) hide show
  1. package/dist/analyzer/api-analyzer.d.ts +22 -0
  2. package/dist/analyzer/api-analyzer.d.ts.map +1 -0
  3. package/dist/analyzer/api-analyzer.js +272 -0
  4. package/dist/analyzer/api-analyzer.js.map +1 -0
  5. package/dist/analyzer/config-analyzer.d.ts +18 -0
  6. package/dist/analyzer/config-analyzer.d.ts.map +1 -0
  7. package/dist/analyzer/config-analyzer.js +200 -0
  8. package/dist/analyzer/config-analyzer.js.map +1 -0
  9. package/dist/analyzer/database-analyzer.d.ts +24 -0
  10. package/dist/analyzer/database-analyzer.d.ts.map +1 -0
  11. package/dist/analyzer/database-analyzer.js +391 -0
  12. package/dist/analyzer/database-analyzer.js.map +1 -0
  13. package/dist/analyzer/index.d.ts +10 -0
  14. package/dist/analyzer/index.d.ts.map +1 -0
  15. package/dist/analyzer/index.js +10 -0
  16. package/dist/analyzer/index.js.map +1 -0
  17. package/dist/analyzer/module-analyzer.d.ts +20 -0
  18. package/dist/analyzer/module-analyzer.d.ts.map +1 -0
  19. package/dist/analyzer/module-analyzer.js +252 -0
  20. package/dist/analyzer/module-analyzer.js.map +1 -0
  21. package/dist/analyzer/workflow-analyzer.d.ts +19 -0
  22. package/dist/analyzer/workflow-analyzer.d.ts.map +1 -0
  23. package/dist/analyzer/workflow-analyzer.js +165 -0
  24. package/dist/analyzer/workflow-analyzer.js.map +1 -0
  25. package/dist/detector/dependency-detector.d.ts +50 -0
  26. package/dist/detector/dependency-detector.d.ts.map +1 -0
  27. package/dist/detector/dependency-detector.js +326 -0
  28. package/dist/detector/dependency-detector.js.map +1 -0
  29. package/dist/detector/entrypoint-detector.d.ts +30 -0
  30. package/dist/detector/entrypoint-detector.d.ts.map +1 -0
  31. package/dist/detector/entrypoint-detector.js +240 -0
  32. package/dist/detector/entrypoint-detector.js.map +1 -0
  33. package/dist/detector/index.d.ts +10 -0
  34. package/dist/detector/index.d.ts.map +1 -0
  35. package/dist/detector/index.js +10 -0
  36. package/dist/detector/index.js.map +1 -0
  37. package/dist/detector/tech-stack-detector.d.ts +41 -0
  38. package/dist/detector/tech-stack-detector.d.ts.map +1 -0
  39. package/dist/detector/tech-stack-detector.js +300 -0
  40. package/dist/detector/tech-stack-detector.js.map +1 -0
  41. package/dist/generator/index.d.ts +9 -0
  42. package/dist/generator/index.d.ts.map +1 -0
  43. package/dist/generator/index.js +9 -0
  44. package/dist/generator/index.js.map +1 -0
  45. package/dist/generator/markdown-generator.d.ts +71 -0
  46. package/dist/generator/markdown-generator.d.ts.map +1 -0
  47. package/dist/generator/markdown-generator.js +235 -0
  48. package/dist/generator/markdown-generator.js.map +1 -0
  49. package/dist/generator/mermaid-generator.d.ts +30 -0
  50. package/dist/generator/mermaid-generator.d.ts.map +1 -0
  51. package/dist/generator/mermaid-generator.js +297 -0
  52. package/dist/generator/mermaid-generator.js.map +1 -0
  53. package/dist/generator/sidebar-generator.d.ts +10 -0
  54. package/dist/generator/sidebar-generator.d.ts.map +1 -0
  55. package/dist/generator/sidebar-generator.js +120 -0
  56. package/dist/generator/sidebar-generator.js.map +1 -0
  57. package/dist/generator/wiki-generator.d.ts +45 -0
  58. package/dist/generator/wiki-generator.d.ts.map +1 -0
  59. package/dist/generator/wiki-generator.js +217 -0
  60. package/dist/generator/wiki-generator.js.map +1 -0
  61. package/dist/index.d.ts +12 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +12 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/llm/auth-manager.d.ts +50 -0
  66. package/dist/llm/auth-manager.d.ts.map +1 -0
  67. package/dist/llm/auth-manager.js +172 -0
  68. package/dist/llm/auth-manager.js.map +1 -0
  69. package/dist/llm/index.d.ts +10 -0
  70. package/dist/llm/index.d.ts.map +1 -0
  71. package/dist/llm/index.js +9 -0
  72. package/dist/llm/index.js.map +1 -0
  73. package/dist/llm/llm-client.d.ts +132 -0
  74. package/dist/llm/llm-client.d.ts.map +1 -0
  75. package/dist/llm/llm-client.js +308 -0
  76. package/dist/llm/llm-client.js.map +1 -0
  77. package/dist/llm/prompt-manager.d.ts +67 -0
  78. package/dist/llm/prompt-manager.d.ts.map +1 -0
  79. package/dist/llm/prompt-manager.js +283 -0
  80. package/dist/llm/prompt-manager.js.map +1 -0
  81. package/dist/models/analysis-result.d.ts +425 -0
  82. package/dist/models/analysis-result.d.ts.map +1 -0
  83. package/dist/models/analysis-result.js +34 -0
  84. package/dist/models/analysis-result.js.map +1 -0
  85. package/dist/models/analysis-types.d.ts +223 -0
  86. package/dist/models/analysis-types.d.ts.map +1 -0
  87. package/dist/models/analysis-types.js +95 -0
  88. package/dist/models/analysis-types.js.map +1 -0
  89. package/dist/models/file-reference.d.ts +62 -0
  90. package/dist/models/file-reference.d.ts.map +1 -0
  91. package/dist/models/file-reference.js +34 -0
  92. package/dist/models/file-reference.js.map +1 -0
  93. package/dist/models/index.d.ts +10 -0
  94. package/dist/models/index.d.ts.map +1 -0
  95. package/dist/models/index.js +10 -0
  96. package/dist/models/index.js.map +1 -0
  97. package/dist/models/project-profile.d.ts +48 -0
  98. package/dist/models/project-profile.d.ts.map +1 -0
  99. package/dist/models/project-profile.js +26 -0
  100. package/dist/models/project-profile.js.map +1 -0
  101. package/dist/models/wiki-page.d.ts +57 -0
  102. package/dist/models/wiki-page.d.ts.map +1 -0
  103. package/dist/models/wiki-page.js +19 -0
  104. package/dist/models/wiki-page.js.map +1 -0
  105. package/dist/pipeline.d.ts +30 -0
  106. package/dist/pipeline.d.ts.map +1 -0
  107. package/dist/pipeline.js +159 -0
  108. package/dist/pipeline.js.map +1 -0
  109. package/dist/scanner/file-scanner.d.ts +27 -0
  110. package/dist/scanner/file-scanner.d.ts.map +1 -0
  111. package/dist/scanner/file-scanner.js +149 -0
  112. package/dist/scanner/file-scanner.js.map +1 -0
  113. package/dist/scanner/ignore-rules.d.ts +31 -0
  114. package/dist/scanner/ignore-rules.d.ts.map +1 -0
  115. package/dist/scanner/ignore-rules.js +98 -0
  116. package/dist/scanner/ignore-rules.js.map +1 -0
  117. package/dist/scanner/index.d.ts +8 -0
  118. package/dist/scanner/index.d.ts.map +1 -0
  119. package/dist/scanner/index.js +8 -0
  120. package/dist/scanner/index.js.map +1 -0
  121. package/dist/scanner/tree-builder.d.ts +20 -0
  122. package/dist/scanner/tree-builder.d.ts.map +1 -0
  123. package/dist/scanner/tree-builder.js +118 -0
  124. package/dist/scanner/tree-builder.js.map +1 -0
  125. package/package.json +34 -0
  126. package/src/analyzer/api-analyzer.ts +324 -0
  127. package/src/analyzer/config-analyzer.ts +209 -0
  128. package/src/analyzer/database-analyzer.ts +468 -0
  129. package/src/analyzer/index.ts +26 -0
  130. package/src/analyzer/module-analyzer.ts +308 -0
  131. package/src/analyzer/workflow-analyzer.ts +190 -0
  132. package/src/detector/dependency-detector.ts +390 -0
  133. package/src/detector/entrypoint-detector.ts +270 -0
  134. package/src/detector/index.ts +21 -0
  135. package/src/detector/tech-stack-detector.ts +377 -0
  136. package/src/generator/index.ts +36 -0
  137. package/src/generator/markdown-generator.ts +277 -0
  138. package/src/generator/mermaid-generator.ts +340 -0
  139. package/src/generator/sidebar-generator.ts +134 -0
  140. package/src/generator/wiki-generator.ts +281 -0
  141. package/src/index.ts +12 -0
  142. package/src/llm/auth-manager.ts +207 -0
  143. package/src/llm/index.ts +21 -0
  144. package/src/llm/llm-client.ts +417 -0
  145. package/src/llm/prompt-manager.ts +325 -0
  146. package/src/models/analysis-result.ts +44 -0
  147. package/src/models/analysis-types.ts +121 -0
  148. package/src/models/file-reference.ts +41 -0
  149. package/src/models/index.ts +44 -0
  150. package/src/models/project-profile.ts +29 -0
  151. package/src/models/wiki-page.ts +23 -0
  152. package/src/pipeline.ts +225 -0
  153. package/src/scanner/file-scanner.ts +192 -0
  154. package/src/scanner/ignore-rules.ts +112 -0
  155. package/src/scanner/index.ts +19 -0
  156. package/src/scanner/tree-builder.ts +156 -0
  157. package/tsconfig.json +8 -0
@@ -0,0 +1,281 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import type { AnalysisResult, WikiPage, ProjectProfile, ModuleInfo } from '../models/index.js';
4
+ import type { LLMClient } from '../llm/index.js';
5
+ import { buildWikiPlanPrompt, buildWikiPagePrompt, buildSourceSummaryPrompt } from '../llm/index.js';
6
+ import { assembleWikiPage, formatTroubleshootingTable } from './markdown-generator.js';
7
+ import {
8
+ generateArchitectureDiagram,
9
+ generateERDiagram,
10
+ generateDependencyDiagram,
11
+ generateApiDiagram,
12
+ } from './mermaid-generator.js';
13
+
14
+ export interface WikiGeneratorConfig {
15
+ outputDir: string;
16
+ concurrency?: number;
17
+ }
18
+
19
+ export interface PlannedPage {
20
+ title: string;
21
+ filename: string;
22
+ summary: string;
23
+ requiredModules: string[];
24
+ }
25
+
26
+ /**
27
+ * 核心 Wiki 生成器
28
+ */
29
+ export class WikiGenerator {
30
+ private config: Required<WikiGeneratorConfig>;
31
+ private llmClient: LLMClient | null;
32
+
33
+ constructor(llmClient: LLMClient | null, config: WikiGeneratorConfig) {
34
+ this.llmClient = llmClient;
35
+ this.config = {
36
+ outputDir: config.outputDir,
37
+ concurrency: config.concurrency ?? 3,
38
+ };
39
+ }
40
+
41
+ /**
42
+ * 规划 Wiki 页面列表
43
+ */
44
+ public async planPages(analysisResult: AnalysisResult): Promise<PlannedPage[]> {
45
+ if (!this.llmClient) {
46
+ return this.getDefaultPagePlan(analysisResult);
47
+ }
48
+
49
+ try {
50
+ const modulesStr = JSON.stringify(
51
+ analysisResult.modules.map((m) => ({
52
+ moduleName: m.moduleName,
53
+ directory: m.directory,
54
+ summary: m.summary,
55
+ filesCount: m.files.length,
56
+ })),
57
+ null,
58
+ 2
59
+ );
60
+
61
+ const techStackStr = JSON.stringify(analysisResult.project, null, 2);
62
+ const prompt = buildWikiPlanPrompt(analysisResult.tree, techStackStr, modulesStr);
63
+
64
+ const planned = await this.llmClient.chatJSON<PlannedPage[]>(prompt);
65
+ if (Array.isArray(planned) && planned.length > 0) {
66
+ return planned;
67
+ }
68
+ } catch {
69
+ // 回退到默认规划
70
+ }
71
+
72
+ return this.getDefaultPagePlan(analysisResult);
73
+ }
74
+
75
+ /**
76
+ * 默认的 Wiki 页面规划(无 LLM 或 LLM 规划失败时使用)
77
+ */
78
+ private getDefaultPagePlan(analysisResult: AnalysisResult): PlannedPage[] {
79
+ const plan: PlannedPage[] = [
80
+ {
81
+ title: '项目概述',
82
+ filename: '项目概述/项目概述.md',
83
+ summary: '项目概要介绍与基本结构',
84
+ requiredModules: ['.'],
85
+ },
86
+ ];
87
+
88
+ // 为每个主要逻辑模块分别规划一个文档页面
89
+ for (const mod of analysisResult.modules) {
90
+ if (mod.directory === '.') continue;
91
+ plan.push({
92
+ title: `${mod.moduleName} 模块分析`,
93
+ filename: `核心功能模块/${mod.moduleName}.md`,
94
+ summary: `${mod.moduleName} 模块的设计与实现细节`,
95
+ requiredModules: [mod.directory],
96
+ });
97
+ }
98
+
99
+ if (analysisResult.databaseModels.length > 0) {
100
+ plan.push({
101
+ title: '数据库设计',
102
+ filename: '数据库设计/数据库设计.md',
103
+ summary: '项目数据库结构及实体模型定义',
104
+ requiredModules: [],
105
+ });
106
+ }
107
+
108
+ if (analysisResult.apiRoutes.length > 0) {
109
+ plan.push({
110
+ title: 'API 参考文档',
111
+ filename: 'API参考文档/API参考文档.md',
112
+ summary: '项目公开的 HTTP 接口路由规范',
113
+ requiredModules: [],
114
+ });
115
+ }
116
+
117
+ return plan;
118
+ }
119
+
120
+ /**
121
+ * 对文件列表进行摘要压缩(如文件太大,先使用 LLM 摘要以节省 Context 空间)
122
+ */
123
+ private async prepareSourceSummaries(
124
+ rootPath: string,
125
+ plannedPage: PlannedPage,
126
+ analysisResult: AnalysisResult
127
+ ): Promise<string> {
128
+ const summaries: string[] = [];
129
+
130
+ // 收集所有关联模块内的文件
131
+ const relatedFiles: string[] = [];
132
+ for (const modDir of plannedPage.requiredModules) {
133
+ const mod = analysisResult.modules.find((m) => m.directory === modDir);
134
+ if (mod) {
135
+ relatedFiles.push(...mod.files);
136
+ }
137
+ }
138
+
139
+ // 如果没有关联模块,则提取根目录附近的重要源文件
140
+ if (relatedFiles.length === 0) {
141
+ relatedFiles.push(
142
+ ...analysisResult.modules
143
+ .filter((m) => m.directory !== '.')
144
+ .slice(0, 3)
145
+ .flatMap((m) => m.files.slice(0, 2))
146
+ );
147
+ }
148
+
149
+ // 限制最多分析前 8 个文件,防止 Context 溢出
150
+ const targetFiles = relatedFiles.slice(0, 8);
151
+
152
+ for (const relPath of targetFiles) {
153
+ try {
154
+ const absPath = path.resolve(rootPath, relPath);
155
+ const content = await fs.readFile(absPath, 'utf-8');
156
+
157
+ // 超过 4KB 的文件进行 LLM 摘要,小文件直接使用首部几行
158
+ if (content.length > 4000 && this.llmClient) {
159
+ const prompt = buildSourceSummaryPrompt(relPath, content.slice(0, 20000));
160
+ const summaryResult = await this.llmClient.chat(prompt);
161
+ summaries.push(`### 文件: ${relPath} (代码摘要)\n${summaryResult.content}`);
162
+ } else {
163
+ const lines = content.split('\n');
164
+ const truncated = lines.slice(0, 100).join('\n');
165
+ summaries.push(`### 文件: ${relPath}\n\`\`\`\n${truncated}\n\`\`\``);
166
+ }
167
+ } catch {
168
+ // 忽略单个文件读取错误
169
+ }
170
+ }
171
+
172
+ return summaries.join('\n\n');
173
+ }
174
+
175
+ /**
176
+ * 生成单个页面内容
177
+ */
178
+ public async generatePage(
179
+ rootPath: string,
180
+ page: PlannedPage,
181
+ analysisResult: AnalysisResult,
182
+ existingPagesSummary?: string
183
+ ): Promise<WikiPage> {
184
+ if (this.llmClient) {
185
+ try {
186
+ const sourceSummaries = await this.prepareSourceSummaries(rootPath, page, analysisResult);
187
+ const prompt = buildWikiPagePrompt(
188
+ page.title,
189
+ analysisResult.tree,
190
+ sourceSummaries,
191
+ existingPagesSummary
192
+ );
193
+
194
+ const response = await this.llmClient.chat(prompt);
195
+ const content = response.content;
196
+
197
+ return {
198
+ title: page.title,
199
+ filename: page.filename,
200
+ summary: page.summary,
201
+ content,
202
+ sourceRefs: [], // 稍后在后置处理中补充或由 LLM 直接写在 Markdown 中
203
+ };
204
+ } catch {
205
+ // LLM 生成失败时回退到模板组装
206
+ }
207
+ }
208
+
209
+ // 本地模板组装(免 LLM 回退逻辑)
210
+ return this.assembleFallbackPage(page, analysisResult);
211
+ }
212
+
213
+ /**
214
+ * 无大模型回退页面生成逻辑
215
+ */
216
+ private assembleFallbackPage(page: PlannedPage, analysisResult: AnalysisResult): WikiPage {
217
+ // 构建默认的图表
218
+ let archDiagram: string | undefined;
219
+ let erDiagram: string | undefined;
220
+ let depDiagram: string | undefined;
221
+ let apiDiagram: string | undefined;
222
+
223
+ if (page.title.includes('架构') || page.title.includes('概述')) {
224
+ archDiagram = generateArchitectureDiagram(analysisResult.modules, analysisResult.dependencies.edges || []);
225
+ }
226
+ if (page.title.includes('数据') || page.title.includes('DB') || page.filename.includes('database')) {
227
+ erDiagram = generateERDiagram(analysisResult.databaseModels);
228
+ }
229
+ if (page.title.includes('依赖') || page.filename.includes('dependency')) {
230
+ depDiagram = generateDependencyDiagram(analysisResult.modules, analysisResult.dependencies.edges || []);
231
+ }
232
+ if (page.title.includes('API') || page.filename.includes('api')) {
233
+ apiDiagram = generateApiDiagram(analysisResult.apiRoutes);
234
+ }
235
+
236
+ const projectRoot = analysisResult.project.rootPath;
237
+
238
+ const content = assembleWikiPage({
239
+ title: page.title,
240
+ citeRefs: [],
241
+ projectRoot,
242
+ introduction: `该文档是针对 "${page.title}" 模块的自动生成概要文档。\n${page.summary}`,
243
+ projectStructure: '项目整体目录结构树状视图如下。',
244
+ projectStructureDiagram: 'flowchart TD\n Root["项目根目录"]', // 简易树
245
+ coreComponents: '以下是该页面对应模块的核心类与组件列表。',
246
+ architectureOverview: archDiagram ? '项目整体模块架构设计及数据交互图如下。' : undefined,
247
+ architectureDiagram: archDiagram,
248
+ detailedAnalysis: erDiagram ? '数据表关系 (ER) 设计图如下。' : undefined,
249
+ troubleshooting: formatTroubleshootingTable([
250
+ {
251
+ problem: '模块无法加载/包缺失',
252
+ cause: '依赖未正确安装',
253
+ resolution: '在根目录下运行 `pnpm install` 或对应的依赖安装指令。',
254
+ },
255
+ ]),
256
+ conclusion: '文档自动构建完毕,详细系统设计请参考内部核心组件代码。',
257
+ appendix: `技术栈详情: ${analysisResult.project.languages.join(', ')}`,
258
+ });
259
+
260
+ return {
261
+ title: page.title,
262
+ filename: page.filename,
263
+ summary: page.summary,
264
+ content,
265
+ sourceRefs: [],
266
+ };
267
+ }
268
+
269
+ /**
270
+ * 保存 Wiki 页面文件到 docs/wiki 目录
271
+ */
272
+ public async savePage(page: WikiPage): Promise<string> {
273
+ const destPath = path.join(this.config.outputDir, page.filename);
274
+ const destDir = path.dirname(destPath);
275
+
276
+ await fs.mkdir(destDir, { recursive: true });
277
+ await fs.writeFile(destPath, page.content, 'utf-8');
278
+
279
+ return destPath;
280
+ }
281
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * RepoWiki Core API
3
+ * 包含代码库扫描、技术栈检测、静态代码分析、LLM 集成及 Markdown Wiki 生成的完整流程。
4
+ */
5
+
6
+ export * from './models/index.js';
7
+ export * from './scanner/index.js';
8
+ export * from './detector/index.js';
9
+ export * from './analyzer/index.js';
10
+ export * from './llm/index.js';
11
+ export * from './generator/index.js';
12
+ export * from './pipeline.js';
@@ -0,0 +1,207 @@
1
+ /**
2
+ * @module auth-manager
3
+ * @description 管理 LLM API 凭证,支持多来源优先级链解析配置。
4
+ *
5
+ * 优先级链(按优先级从高到低):
6
+ * 1. 环境变量: REPOWIKI_API_KEY, REPOWIKI_BASE_URL, REPOWIKI_MODEL
7
+ * 2. 环境变量 (OpenAI 兼容): OPENAI_API_KEY, OPENAI_BASE_URL
8
+ * 3. 全局配置文件: ~/.repowiki/config.json
9
+ * 4. 工作区 .env 文件(使用 dotenv 解析)
10
+ */
11
+
12
+ import { promises as fs } from 'node:fs';
13
+ import path from 'node:path';
14
+ import os from 'node:os';
15
+ import dotenv from 'dotenv';
16
+
17
+ // ============================================================================
18
+ // 类型定义
19
+ // ============================================================================
20
+
21
+ /** LLM 服务配置 */
22
+ export interface LLMConfig {
23
+ /** API 端点地址 */
24
+ apiEndpoint: string;
25
+ /** 模型名称 */
26
+ modelName: string;
27
+ /** API 密钥 */
28
+ apiKey: string;
29
+ }
30
+
31
+ // ============================================================================
32
+ // 常量
33
+ // ============================================================================
34
+
35
+ /** 默认 API 端点 */
36
+ const DEFAULT_API_ENDPOINT = 'https://api.openai.com/v1';
37
+
38
+ /** 默认模型名称 */
39
+ const DEFAULT_MODEL_NAME = 'gpt-4o';
40
+
41
+ /** 全局配置目录名 */
42
+ const GLOBAL_CONFIG_DIR = '.repowiki';
43
+
44
+ /** 全局配置文件名 */
45
+ const GLOBAL_CONFIG_FILE = 'config.json';
46
+
47
+ // ============================================================================
48
+ // 内部工具函数
49
+ // ============================================================================
50
+
51
+ /**
52
+ * 获取全局配置文件的绝对路径
53
+ */
54
+ function getGlobalConfigPath(): string {
55
+ return path.join(os.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_FILE);
56
+ }
57
+
58
+ /**
59
+ * 尝试读取并解析 JSON 配置文件,若文件不存在或解析失败则返回空对象
60
+ */
61
+ async function readJsonConfig(filePath: string): Promise<Record<string, unknown>> {
62
+ try {
63
+ const content = await fs.readFile(filePath, 'utf-8');
64
+ const parsed: unknown = JSON.parse(content);
65
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
66
+ return parsed as Record<string, unknown>;
67
+ }
68
+ return {};
69
+ } catch {
70
+ // 文件不存在或内容无效,均视为空配置
71
+ return {};
72
+ }
73
+ }
74
+
75
+ /**
76
+ * 尝试解析工作区 .env 文件,返回解析后的键值对
77
+ */
78
+ async function readWorkspaceEnv(workspacePath: string): Promise<Record<string, string>> {
79
+ const envFilePath = path.join(workspacePath, '.env');
80
+ try {
81
+ const content = await fs.readFile(envFilePath, 'utf-8');
82
+ const parsed = dotenv.parse(content);
83
+ return parsed;
84
+ } catch {
85
+ return {};
86
+ }
87
+ }
88
+
89
+ /**
90
+ * 返回第一个非空字符串值,若全部为空则返回 undefined
91
+ */
92
+ function firstNonEmpty(...values: Array<string | undefined | null>): string | undefined {
93
+ for (const v of values) {
94
+ if (v && v.trim().length > 0) {
95
+ return v.trim();
96
+ }
97
+ }
98
+ return undefined;
99
+ }
100
+
101
+ // ============================================================================
102
+ // 公开 API
103
+ // ============================================================================
104
+
105
+ /**
106
+ * 按优先级链加载 LLM 配置。
107
+ *
108
+ * 解析顺序:
109
+ * 1. 进程环境变量(REPOWIKI_* 优先,其次 OPENAI_*)
110
+ * 2. 全局配置文件 ~/.repowiki/config.json
111
+ * 3. 工作区 .env 文件
112
+ *
113
+ * 对于每个字段,取优先级最高的非空值;若全部为空则使用默认值或空字符串。
114
+ *
115
+ * @param workspacePath - 可选的工作区路径,用于读取 .env 文件
116
+ * @returns 解析后的 LLM 配置
117
+ */
118
+ export async function loadLLMConfig(workspacePath?: string): Promise<LLMConfig> {
119
+ // --- 来源 1 & 2: 环境变量(已在 process.env 中) ---
120
+ const envApiKey = firstNonEmpty(process.env['REPOWIKI_API_KEY'], process.env['OPENAI_API_KEY']);
121
+ const envEndpoint = firstNonEmpty(process.env['REPOWIKI_BASE_URL'], process.env['OPENAI_BASE_URL']);
122
+ const envModel = firstNonEmpty(process.env['REPOWIKI_MODEL']);
123
+
124
+ // --- 来源 3: 全局配置文件 ---
125
+ const globalConfig = await readJsonConfig(getGlobalConfigPath());
126
+ const globalApiKey = typeof globalConfig['apiKey'] === 'string' ? globalConfig['apiKey'] : undefined;
127
+ const globalEndpoint = typeof globalConfig['apiEndpoint'] === 'string' ? globalConfig['apiEndpoint'] : undefined;
128
+ const globalModel = typeof globalConfig['modelName'] === 'string' ? globalConfig['modelName'] : undefined;
129
+
130
+ // --- 来源 4: 工作区 .env ---
131
+ let wsApiKey: string | undefined;
132
+ let wsEndpoint: string | undefined;
133
+ let wsModel: string | undefined;
134
+ if (workspacePath) {
135
+ const wsEnv = await readWorkspaceEnv(workspacePath);
136
+ wsApiKey = firstNonEmpty(wsEnv['REPOWIKI_API_KEY'], wsEnv['OPENAI_API_KEY']);
137
+ wsEndpoint = firstNonEmpty(wsEnv['REPOWIKI_BASE_URL'], wsEnv['OPENAI_BASE_URL']);
138
+ wsModel = firstNonEmpty(wsEnv['REPOWIKI_MODEL']);
139
+ }
140
+
141
+ // --- 合并:对每个字段,按优先级取第一个非空值 ---
142
+ const apiKey = firstNonEmpty(envApiKey, globalApiKey, wsApiKey) ?? '';
143
+ const apiEndpoint = firstNonEmpty(envEndpoint, globalEndpoint, wsEndpoint) ?? DEFAULT_API_ENDPOINT;
144
+ const modelName = firstNonEmpty(envModel, globalModel, wsModel) ?? DEFAULT_MODEL_NAME;
145
+
146
+ return { apiEndpoint, modelName, apiKey };
147
+ }
148
+
149
+ /**
150
+ * 将配置保存到全局配置文件 ~/.repowiki/config.json。
151
+ *
152
+ * 如果文件已存在,则与现有配置合并(新值覆盖旧值)。
153
+ * 如果目录不存在,则自动创建。
154
+ *
155
+ * @param config - 要保存的部分配置
156
+ */
157
+ export async function saveGlobalConfig(config: Partial<LLMConfig>): Promise<void> {
158
+ const configPath = getGlobalConfigPath();
159
+ const configDir = path.dirname(configPath);
160
+
161
+ // 确保配置目录存在
162
+ await fs.mkdir(configDir, { recursive: true });
163
+
164
+ // 读取现有配置并合并
165
+ const existing = await readJsonConfig(configPath);
166
+
167
+ // 仅合并非 undefined 的字段
168
+ const merged: Record<string, unknown> = { ...existing };
169
+ if (config.apiEndpoint !== undefined) merged['apiEndpoint'] = config.apiEndpoint;
170
+ if (config.modelName !== undefined) merged['modelName'] = config.modelName;
171
+ if (config.apiKey !== undefined) merged['apiKey'] = config.apiKey;
172
+
173
+ await fs.writeFile(configPath, JSON.stringify(merged, null, 2), 'utf-8');
174
+ }
175
+
176
+ /**
177
+ * 验证 LLM 配置的完整性。
178
+ *
179
+ * @param config - 待验证的配置
180
+ * @returns 错误消息数组。若为空数组,则配置有效。
181
+ */
182
+ export function validateConfig(config: LLMConfig): string[] {
183
+ const errors: string[] = [];
184
+
185
+ if (!config.apiKey || config.apiKey.trim().length === 0) {
186
+ errors.push('缺少 API 密钥 (apiKey)。请通过环境变量 REPOWIKI_API_KEY 或全局配置文件设置。');
187
+ }
188
+
189
+ if (!config.apiEndpoint || config.apiEndpoint.trim().length === 0) {
190
+ errors.push('缺少 API 端点 (apiEndpoint)。');
191
+ }
192
+
193
+ if (!config.modelName || config.modelName.trim().length === 0) {
194
+ errors.push('缺少模型名称 (modelName)。');
195
+ }
196
+
197
+ // 验证 apiEndpoint 是合法 URL
198
+ if (config.apiEndpoint && config.apiEndpoint.trim().length > 0) {
199
+ try {
200
+ new URL(config.apiEndpoint);
201
+ } catch {
202
+ errors.push(`API 端点格式无效: "${config.apiEndpoint}"。请提供合法的 URL。`);
203
+ }
204
+ }
205
+
206
+ return errors;
207
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @module llm
3
+ * @description LLM 层入口 - 统一导出认证管理、客户端和提示词管理功能。
4
+ */
5
+
6
+ // 认证管理
7
+ export type { LLMConfig } from './auth-manager.js';
8
+ export { loadLLMConfig, saveGlobalConfig, validateConfig } from './auth-manager.js';
9
+
10
+ // LLM 客户端
11
+ export type { ChatMessage, LLMClientOptions, LLMResponse, ChatOptions } from './llm-client.js';
12
+ export { LLMClient, LLMError, LLMAuthError, LLMRateLimitError, LLMJsonParseError } from './llm-client.js';
13
+
14
+ // 提示词管理
15
+ export {
16
+ buildModuleAnalysisPrompt,
17
+ buildMermaidPrompt,
18
+ buildWikiPagePrompt,
19
+ buildWikiPlanPrompt,
20
+ buildSourceSummaryPrompt,
21
+ } from './prompt-manager.js';