@aigne/doc-smith 0.9.8-alpha.15 → 0.9.8-alpha.16

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.
@@ -40,6 +40,7 @@ Content Checker 在文档生成流程的最后阶段执行:
40
40
  - 自动获取:
41
41
  - 从 `PATHS` 常量自动获取文档结构文件路径 (`planning/document-structure.yaml`)
42
42
  - 从 `PATHS` 常量自动获取文档目录路径 (`docs/`)
43
+ - 从 `config.yaml` 自动获取 `translateLanguages` 配置(目标翻译语言列表)
43
44
  - `autoFix` 默认为 `true`
44
45
  - `checkRemoteImages` 默认为 `true`
45
46
 
@@ -72,20 +73,39 @@ Content Checker 在文档生成流程的最后阶段执行:
72
73
  - 文档文件夹必须包含 `.meta.yaml` 文件
73
74
  - `.meta.yaml` 必须包含 `kind`、`source`、`default` 字段
74
75
  - `kind` 字段必须为 `"doc"`
76
+ - **`source` 字段必须与项目 `locale` 一致**(所有文档的源语言必须是项目主语言)
75
77
  - 至少存在一个语言版本文件(如 `zh.md`、`en.md`)
78
+ - `source` 和 `default` 语言文件必须存在
79
+ - 如果 `config.yaml` 中配置了 `translateLanguages`,则所有目标语言文件都必须存在(排除 source 语言本身)
76
80
 
77
81
  3. **内容规范**:
78
82
  - 文档内容不能为空(去除标题后至少 50 字符)
79
83
  - 标题层级不能跳级(H1 → H2 → H3,不能 H1 → H3)
80
84
  - 内部链接必须指向存在的文档
85
+ - 内部链接格式必须正确(详见下方链接格式规范)
81
86
  - 本地图片必须存在于指定路径
82
87
 
83
- 4. **检查规则**:
88
+ 4. **内部链接格式规范**:
89
+ - **正确格式**:
90
+ - 绝对路径:`/agent-types/ai-agent`
91
+ - 相对路径:`../ai-agent` 或 `./sub-doc`
92
+ - 带锚点:`/agent-types/ai-agent#section`
93
+ - **错误格式(Fatal 错误)**:
94
+ - 包含 `.md` 后缀的链接都视为格式错误
95
+ - 示例:`/agent-types/ai-agent/en.md`、`./ai-agent.md`、`../path.md`
96
+ - **智能建议**:
97
+ - 语言后缀模式(`/xx.md`):建议去掉整个 `/xx.md`,如 `/path/en.md` → `/path`
98
+ - 普通 `.md` 后缀:建议只去掉 `.md`,如 `./doc.md` → `./doc`
99
+ - **设计原因**:
100
+ - 内部链接使用文档路径,语言版本由文档系统在运行时根据用户偏好选择
101
+ - 硬编码 `.md` 后缀会导致发布流程异常
102
+
103
+ 5. **检查规则**:
84
104
  - 跳过代码块中的链接和图片
85
105
  - 忽略外部链接的有效性检查
86
106
  - 远程图片通过 HTTP HEAD 请求检查可访问性(3秒超时)
87
107
 
88
- 5. **Sources 绝对路径规则**:
108
+ 6. **Sources 绝对路径规则**:
89
109
  - 识别 `/sources/...` 格式的图片路径
90
110
  - 从 config.yaml 加载 sources 配置
91
111
  - 依次在每个配置的 source 中查找图片:
@@ -124,7 +144,8 @@ Content Checker 在文档生成流程的最后阶段执行:
124
144
  1. **检查通过**:
125
145
  - 所有文档文件和文件夹存在
126
146
  - 所有 `.meta.yaml` 格式正确且包含必需字段
127
- - 所有内部链接有效
147
+ - 所有必需的语言文件存在(source、default、translateLanguages 中的所有语言)
148
+ - 所有内部链接有效且格式正确
128
149
  - 所有本地图片存在
129
150
  - 文档内容充实且标题层级规范
130
151
 
@@ -140,11 +161,12 @@ Content Checker 在文档生成流程的最后阶段执行:
140
161
  1. **文件不存在**:文档结构文件或文档目录不存在
141
162
  2. **文档文件夹缺失**:结构中定义的文档文件夹未生成
142
163
  3. **元数据错误**:`.meta.yaml` 缺失、格式错误或缺少必需字段
143
- 4. **语言文件缺失**:没有任何语言版本文件或缺少 default/source 语言
144
- 5. **内容问题**:空文档、标题跳级
145
- 6. **链接问题**:内部链接死链、路径超出根目录
146
- 7. **图片问题**:本地图片不存在、远程图片无法访问
147
- 8. **Sources 路径问题**:无法解析的 source name、物理路径上图片不存在
164
+ 4. **Source 与 Locale 不一致**:文档的 `source` 字段与项目 `locale` 不一致(应该修改文档的 source 或重新生成文档)
165
+ 5. **语言文件缺失**:没有任何语言版本文件、缺少 default/source 语言、或缺少 translateLanguages 中配置的目标语言
166
+ 6. **内容问题**:空文档、标题跳级
167
+ 7. **链接问题**:内部链接死链、路径超出根目录、链接格式不正确(包含 `.md` 后缀或语言版本后缀如 `/en.md`)
168
+ 8. **图片问题**:本地图片不存在、远程图片无法访问
169
+ 9. **Sources 路径问题**:无法解析的 source name、物理路径上图片不存在
148
170
 
149
171
  ### 处理策略
150
172
 
@@ -3,7 +3,7 @@ import { constants } from "node:fs";
3
3
  import { parse as yamlParse } from "yaml";
4
4
  import path from "node:path";
5
5
  import { collectDocumentPaths } from "../../utils/document-paths.mjs";
6
- import { PATHS } from "../../utils/agent-constants.mjs";
6
+ import { PATHS, ERROR_CODES } from "../../utils/agent-constants.mjs";
7
7
  import { isSourcesAbsolutePath, resolveSourcesPath } from "../../utils/sources-path-resolver.mjs";
8
8
  import { loadConfigFromFile } from "../../utils/config.mjs";
9
9
 
@@ -34,18 +34,33 @@ class DocumentContentValidator {
34
34
  this.documents = [];
35
35
  this.documentPaths = new Set();
36
36
  this.remoteImageCache = new Map();
37
- this.sourcesConfig = null; // 缓存 sources 配置
37
+ this.workspaceConfig = null; // 缓存 workspace 配置
38
+ }
39
+
40
+ /**
41
+ * 加载 workspace 配置(懒加载)
42
+ */
43
+ async loadWorkspaceConfig() {
44
+ if (this.workspaceConfig === null) {
45
+ this.workspaceConfig = (await loadConfigFromFile()) || {};
46
+ }
47
+ return this.workspaceConfig;
38
48
  }
39
49
 
40
50
  /**
41
51
  * 加载 sources 配置(懒加载)
42
52
  */
43
53
  async loadSourcesConfig() {
44
- if (this.sourcesConfig === null) {
45
- const config = await loadConfigFromFile();
46
- this.sourcesConfig = config?.sources || [];
47
- }
48
- return this.sourcesConfig;
54
+ const config = await this.loadWorkspaceConfig();
55
+ return config.sources || [];
56
+ }
57
+
58
+ /**
59
+ * 加载 translateLanguages 配置(懒加载)
60
+ */
61
+ async loadTranslateLanguages() {
62
+ const config = await this.loadWorkspaceConfig();
63
+ return config.translateLanguages || [];
49
64
  }
50
65
 
51
66
  /**
@@ -204,6 +219,22 @@ class DocumentContentValidator {
204
219
  suggestion: "修改为 kind: doc",
205
220
  });
206
221
  }
222
+
223
+ // source 与项目 locale 一致性校验
224
+ if (meta.source) {
225
+ const config = await this.loadWorkspaceConfig();
226
+ const projectLocale = config?.locale;
227
+ if (projectLocale && meta.source !== projectLocale) {
228
+ this.errors.fatal.push({
229
+ type: ERROR_CODES.SOURCE_LOCALE_MISMATCH,
230
+ path: doc.path,
231
+ source: meta.source,
232
+ locale: projectLocale,
233
+ message: `文档 source (${meta.source}) 与项目 locale (${projectLocale}) 不一致: ${doc.path}`,
234
+ suggestion: `修改文档的 source 为 "${projectLocale}",或重新生成该文档的主语言版本`,
235
+ });
236
+ }
237
+ }
207
238
  } catch (error) {
208
239
  this.errors.fatal.push({
209
240
  type: "INVALID_META",
@@ -265,6 +296,26 @@ class DocumentContentValidator {
265
296
  });
266
297
  }
267
298
  }
299
+
300
+ // 检查 translateLanguages 中配置的目标语言文件是否存在
301
+ const translateLanguages = await this.loadTranslateLanguages();
302
+ if (translateLanguages.length > 0) {
303
+ for (const lang of translateLanguages) {
304
+ // 跳过源语言(源语言不需要作为翻译目标)
305
+ if (lang === meta.source) continue;
306
+
307
+ const langFile = `${lang}.md`;
308
+ if (!langFiles.includes(langFile)) {
309
+ this.errors.fatal.push({
310
+ type: ERROR_CODES.MISSING_TRANSLATE_LANGUAGE,
311
+ path: doc.path,
312
+ lang,
313
+ message: `翻译语言文件缺失: ${langFile}`,
314
+ suggestion: `请翻译文档到 ${lang} 语言,或从 config.yaml 的 translateLanguages 中移除 ${lang}`,
315
+ });
316
+ }
317
+ }
318
+ }
268
319
  } catch (_error) {
269
320
  // .meta.yaml 错误已在 validateMetaFile 中报告
270
321
  }
@@ -530,11 +581,36 @@ class DocumentContentValidator {
530
581
  async validateInternalLink(linkUrl, doc, linkText, langFile) {
531
582
  let targetPath;
532
583
 
533
- // 链接预处理:移除锚点、语言文件名和 .md 后缀
534
- const cleanLinkUrl = linkUrl
535
- .split("#")[0] // 移除锚点
536
- .replace(/\/[a-z]{2}(-[A-Z]{2})?\.md$/, "") // 移除 /zh.md
537
- .replace(/\.md$/, ""); // 移除 .md
584
+ // 移除锚点部分用于格式检查
585
+ const urlWithoutAnchor = linkUrl.split("#")[0];
586
+
587
+ // 检查链接格式是否正确:内部链接不应包含 .md 后缀
588
+ const langSuffixPattern = /\/[a-z]{2}(-[A-Z]{2})?\.md$/; // 匹配 /en.md, /zh.md, /en-US.md
589
+ const mdSuffixPattern = /\.md$/;
590
+
591
+ if (mdSuffixPattern.test(urlWithoutAnchor)) {
592
+ // 链接包含 .md 后缀,这是格式错误
593
+ // 如果是语言后缀模式,去掉整个 /xx.md 部分;否则只去掉 .md
594
+ const isLangSuffix = langSuffixPattern.test(urlWithoutAnchor);
595
+ const suggestedLink = isLangSuffix
596
+ ? urlWithoutAnchor.replace(langSuffixPattern, "")
597
+ : urlWithoutAnchor.replace(mdSuffixPattern, "");
598
+
599
+ this.stats.brokenLinks++;
600
+ this.errors.fatal.push({
601
+ type: ERROR_CODES.INVALID_LINK_FORMAT,
602
+ path: doc.path,
603
+ langFile,
604
+ link: linkUrl,
605
+ linkText,
606
+ message: `内部链接格式错误: [${linkText}](${linkUrl})`,
607
+ suggestion: `链接不应包含 .md 后缀,建议改为: ${suggestedLink}`,
608
+ });
609
+ return;
610
+ }
611
+
612
+ // 链接格式正确,继续验证目标是否存在
613
+ const cleanLinkUrl = urlWithoutAnchor;
538
614
 
539
615
  // 如果链接只是锚点(如 #section),cleanLinkUrl 会是空字符串,跳过检查
540
616
  if (!cleanLinkUrl) {
@@ -632,8 +708,8 @@ class DocumentContentValidator {
632
708
 
633
709
  // 检查是否为 /sources/... 绝对路径
634
710
  if (isSourcesAbsolutePath(imageUrl)) {
635
- await this.loadSourcesConfig();
636
- const resolved = await resolveSourcesPath(imageUrl, this.sourcesConfig, PATHS.WORKSPACE_BASE);
711
+ const sourcesConfig = await this.loadSourcesConfig();
712
+ const resolved = await resolveSourcesPath(imageUrl, sourcesConfig, PATHS.WORKSPACE_BASE);
637
713
 
638
714
  if (!resolved) {
639
715
  this.stats.missingImages++;
@@ -15,7 +15,7 @@ import { PATHS } from "../../utils/agent-constants.mjs";
15
15
  import { deploy } from "../../utils/deploy.mjs";
16
16
  import { loadConfigFromFile, saveValueToConfig } from "../../utils/config.mjs";
17
17
  import { ensureTmpDir } from "../../utils/files.mjs";
18
- import { getGithubRepoUrl } from "../../utils/git.mjs";
18
+ import { getGithubRepoUrl, isValidGithubUrl } from "../../utils/git.mjs";
19
19
  import updateBranding from "../../utils/branding.mjs";
20
20
  import { generateSidebar, loadDocumentStructure } from "../../utils/docs.mjs";
21
21
  import { copyDocumentsToTemp } from "../../utils/docs-converter.mjs";
@@ -245,9 +245,18 @@ export default async function publishDocs(
245
245
  }
246
246
 
247
247
  // Construct boardMeta object
248
+ // In standalone mode, get GitHub URL from git-clone type source in sources array
249
+ // In project mode, use current git repo URL (even if git-clone sources exist as supplements)
250
+ let githubRepoUrl = getGithubRepoUrl();
251
+ if (config?.mode === "standalone") {
252
+ const gitCloneSource = config?.sources?.find((s) => s.type === "git-clone");
253
+ const configUrl = gitCloneSource?.url;
254
+ // Only use config URL if it's a valid GitHub URL
255
+ githubRepoUrl = isValidGithubUrl(configUrl) ? configUrl : "";
256
+ }
248
257
  const boardMeta = {
249
258
  category: config?.documentPurpose || [],
250
- githubRepoUrl: getGithubRepoUrl(),
259
+ githubRepoUrl,
251
260
  commitSha: config?.lastGitHead || "",
252
261
  languages: [
253
262
  ...(config?.locale ? [config.locale] : []),
@@ -1,4 +1,5 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
2
3
  import { stringify as yamlStringify } from "yaml";
3
4
  import path from "node:path";
4
5
  import {
@@ -7,6 +8,7 @@ import {
7
8
  isValidDocumentPath,
8
9
  } from "../../utils/document-paths.mjs";
9
10
  import { PATHS, ERROR_CODES, FILE_TYPES, DOC_META_DEFAULTS } from "../../utils/agent-constants.mjs";
11
+ import { loadLocale } from "../../utils/config.mjs";
10
12
 
11
13
  /**
12
14
  * 创建文档文件夹和文件
@@ -124,10 +126,38 @@ export default async function saveDocument({ path: rawPath, content, options = {
124
126
  };
125
127
  }
126
128
 
127
- // 7. 创建文件夹和文件
129
+ // 7. 检查新建文档时 language 必须等于项目 locale
130
+ const docFolder = path.join(PATHS.DOCS_DIR, filePath);
131
+ const metaPath = path.join(docFolder, FILE_TYPES.META);
132
+ const isNewDocument = !existsSync(metaPath);
133
+
134
+ if (isNewDocument) {
135
+ let projectLocale;
136
+ try {
137
+ projectLocale = await loadLocale();
138
+ } catch (_error) {
139
+ return {
140
+ success: false,
141
+ error: ERROR_CODES.MISSING_CONFIG_FILE,
142
+ message: "无法读取项目配置文件",
143
+ suggestion: "请确保 config.yaml 存在且包含 locale 字段",
144
+ };
145
+ }
146
+
147
+ if (language !== projectLocale) {
148
+ return {
149
+ success: false,
150
+ error: ERROR_CODES.INVALID_LANGUAGE,
151
+ message: `新建文档时必须使用项目主语言: ${projectLocale},当前传入: ${language}`,
152
+ suggestion: `请将 language 参数改为 "${projectLocale}"(项目 locale),首先生成主语言版本`,
153
+ };
154
+ }
155
+ }
156
+
157
+ // 8. 创建文件夹和文件
128
158
  const files = await createDocumentFiles(filePath, language, content);
129
159
 
130
- // 8. 返回成功响应
160
+ // 9. 返回成功响应
131
161
  return {
132
162
  success: true,
133
163
  path: displayPath,
@@ -154,7 +184,7 @@ saveDocument.description =
154
184
  `保存文档到 ${PATHS.DOCS_DIR} 目录,自动创建文件夹结构、元信息文件和语言版本文件。` +
155
185
  "【重要限制】此工具仅用于新增文档时调用。编辑已有文档时,请直接使用 Edit 工具修改对应的语言文件。" +
156
186
  `使用前必须确保 ${PATHS.DOCUMENT_STRUCTURE} 已存在且包含目标文档路径。` +
157
- `language 参数必须从 ${PATHS.CONFIG} locale 字段读取并传入。`;
187
+ `【强制要求】新建文档时 language 必须等于项目 locale(config.yaml 中的 locale 字段),系统会自动验证。`;
158
188
 
159
189
  // 定义输入 schema
160
190
  saveDocument.input_schema = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/doc-smith",
3
- "version": "0.9.8-alpha.15",
3
+ "version": "0.9.8-alpha.16",
4
4
  "description": "AI-driven documentation generation tool built on the AIGNE Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -37,6 +37,7 @@
37
37
 
38
38
  在每个文档中添加导航链接,引导用户在文档之间流畅跳转。
39
39
  只能链接生成的其他文档,不能链接到工作目录中的 markdown 文件,文档发布后会导致无法访问。
40
+ 导航链接应该使用文档结构中文档的 `path`
40
41
 
41
42
  ### 文档开头导航
42
43
 
@@ -104,7 +104,7 @@ find . -type f \( -name "*.png" -o -name "*.jpg" -o -name "*.jpeg" -o -name "*.g
104
104
  - 文档开头:前置条件(prerequisites)、父主题(parent topic)
105
105
  - 文档结尾:相关主题(related topics)、下一步(next steps)、子文档(child documents)
106
106
  - 只能链接生成的其他文档,不能链接到工作目录中的 markdown 文件,文档发布后会导致无法访问。
107
- - 导航链接可直接使用文件结构中文档的 `path`
107
+ - 导航链接应该使用文档结构中文档的 `path`
108
108
 
109
109
  #### 主体内容
110
110
  - **结构化章节**:逻辑清晰的信息层次
@@ -70,6 +70,11 @@ export const ERROR_CODES = {
70
70
  MISSING_LANGS: "MISSING_LANGS",
71
71
  MISSING_SOURCE_FILE: "MISSING_SOURCE_FILE",
72
72
 
73
+ // 内容校验相关
74
+ SOURCE_LOCALE_MISMATCH: "SOURCE_LOCALE_MISMATCH",
75
+ MISSING_TRANSLATE_LANGUAGE: "MISSING_TRANSLATE_LANGUAGE",
76
+ INVALID_LINK_FORMAT: "INVALID_LINK_FORMAT",
77
+
73
78
  // 其他
74
79
  SAVE_ERROR: "SAVE_ERROR",
75
80
  UNEXPECTED_ERROR: "UNEXPECTED_ERROR",
package/utils/git.mjs CHANGED
@@ -1,5 +1,16 @@
1
1
  import { execSync } from "node:child_process";
2
2
 
3
+ /**
4
+ * Validate if a URL is a valid GitHub repository URL
5
+ * @param {string} url - The URL to validate
6
+ * @returns {boolean} - True if valid GitHub URL
7
+ */
8
+ export function isValidGithubUrl(url) {
9
+ if (!url || typeof url !== "string") return false;
10
+ // Match both HTTPS and SSH GitHub URLs
11
+ return /^(https:\/\/github\.com\/|git@github\.com:)[^/]+\/[^/]+/.test(url);
12
+ }
13
+
3
14
  /**
4
15
  * Get GitHub repository URL
5
16
  * @returns {string} - GitHub repository URL or empty string