@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.
- package/agents/content-checker/ai/intent.md +30 -8
- package/agents/content-checker/validate-content.mjs +90 -14
- package/agents/publish/publish-docs.mjs +11 -2
- package/agents/save-document/index.mjs +33 -3
- package/package.json +1 -1
- package/skills/doc-smith/references/document-content-guide.md +1 -0
- package/skills/doc-smith-docs-detail/SKILL.md +1 -1
- package/utils/agent-constants.mjs +5 -0
- package/utils/git.mjs +11 -0
|
@@ -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
|
-
|
|
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.
|
|
144
|
-
5.
|
|
145
|
-
6.
|
|
146
|
-
7.
|
|
147
|
-
8.
|
|
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.
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
//
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
187
|
+
`【强制要求】新建文档时 language 必须等于项目 locale(config.yaml 中的 locale 字段),系统会自动验证。`;
|
|
158
188
|
|
|
159
189
|
// 定义输入 schema
|
|
160
190
|
saveDocument.input_schema = {
|
package/package.json
CHANGED
|
@@ -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
|
-
-
|
|
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
|