@aigne/doc-smith 0.9.8-alpha.14 → 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/CLAUDE.md +42 -1
- package/agents/content-checker/ai/intent.md +30 -8
- package/agents/content-checker/validate-content.mjs +90 -14
- package/agents/localize/index.yaml +1 -1
- package/agents/publish/ai/intent.md +4 -1
- package/agents/publish/publish-docs.mjs +11 -2
- package/agents/publish/translate-meta.mjs +54 -47
- 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
- package/utils/workspace.mjs +75 -19
package/CLAUDE.md
CHANGED
|
@@ -231,6 +231,11 @@ Intent 文档是设计阶段的产物,实施阶段会基于它创建具体的
|
|
|
231
231
|
- **文件删除**:必须提供 `dryRun` 选项,允许预览将要删除的内容
|
|
232
232
|
- **批量操作**:在执行前应验证数据源(如 YAML 文件)的完整性
|
|
233
233
|
|
|
234
|
+
### 代码检查
|
|
235
|
+
|
|
236
|
+
- **每次修改后检查 lint**:执行 `pnpm lint:fix` 检查并自动修复问题
|
|
237
|
+
- **提交前确保通过**:不要提交带有 lint 错误的代码
|
|
238
|
+
|
|
234
239
|
## AIGNE 框架开发指南
|
|
235
240
|
|
|
236
241
|
### Function Agent 基本结构
|
|
@@ -438,4 +443,40 @@ agentName.task_render_mode = "collapse";
|
|
|
438
443
|
|
|
439
444
|
- **入口 Agent**:`skills-entry/doc-smith/index.mjs`
|
|
440
445
|
- **Function Agent**:`agents/content-checker/index.mjs`
|
|
441
|
-
- **共享模块**:`utils/workspace.mjs`、`utils/afs-factory.mjs`
|
|
446
|
+
- **共享模块**:`utils/workspace.mjs`、`utils/afs-factory.mjs`
|
|
447
|
+
|
|
448
|
+
## 设计决策原则
|
|
449
|
+
|
|
450
|
+
### KISS 原则应用
|
|
451
|
+
|
|
452
|
+
在设计功能时,优先考虑最简方案。复杂度不是功能强大的标志,往往是设计不成熟的表现。
|
|
453
|
+
|
|
454
|
+
**案例:Workspace 与主项目的关系**
|
|
455
|
+
|
|
456
|
+
| 方案 | 复杂度 | 问题 |
|
|
457
|
+
|------|--------|------|
|
|
458
|
+
| ❌ 自动添加 submodule | 高 | 本地路径无法共享、需要手动同步远程 |
|
|
459
|
+
| ✅ 添加到 .gitignore | 低 | 简单、无副作用、用户自主选择 |
|
|
460
|
+
|
|
461
|
+
**决策过程**:
|
|
462
|
+
1. 先问"为什么需要这个功能?"而不是"如何实现这个功能?"
|
|
463
|
+
2. 识别出 submodule 解决的是"追踪子项目版本",但 doc-smith 不需要这种强耦合
|
|
464
|
+
3. 最简方案:独立 git 仓库 + gitignore,用户需要时自行决定如何关联
|
|
465
|
+
|
|
466
|
+
### 问对的问题
|
|
467
|
+
|
|
468
|
+
遇到技术问题时,先审视问题本身是否正确:
|
|
469
|
+
|
|
470
|
+
| 错误的问题 | 正确的问题 |
|
|
471
|
+
|-----------|-----------|
|
|
472
|
+
| 如何让 submodule URL 自动同步? | 为什么需要 submodule? |
|
|
473
|
+
| 如何处理复杂的边界情况? | 这个功能是否应该存在? |
|
|
474
|
+
| 如何让用户更容易使用? | 用户真正需要什么? |
|
|
475
|
+
|
|
476
|
+
**原则**:如果一个功能需要大量边界处理,可能说明功能本身的抽象层次有问题。
|
|
477
|
+
|
|
478
|
+
### 默认行为设计
|
|
479
|
+
|
|
480
|
+
- **默认做最少的事**:不自作主张添加可能有问题的配置
|
|
481
|
+
- **复杂操作由用户显式触发**:提供可选命令而非自动执行
|
|
482
|
+
- **松耦合优于强耦合**:让组件独立存在,用户自主决定关联方式
|
|
@@ -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++;
|
|
@@ -21,7 +21,8 @@ DocSmith 生成的文档存储在本地 workspace 目录中,使用特定的目
|
|
|
21
21
|
└─ 检查文档内容(checkContent)
|
|
22
22
|
↓
|
|
23
23
|
2. translate-meta.mjs - 翻译元数据(可选)
|
|
24
|
-
|
|
24
|
+
├─ 如果只有一种语言,跳过翻译,直接保存到缓存
|
|
25
|
+
└─ 如果有多种语言,将项目名称和描述翻译为目标语言
|
|
25
26
|
↓
|
|
26
27
|
3. publish-docs.mjs - 执行发布
|
|
27
28
|
├─ 加载文档结构
|
|
@@ -48,6 +49,8 @@ DocSmith 生成的文档存储在本地 workspace 目录中,使用特定的目
|
|
|
48
49
|
- 调整相对路径以适应发布后的目录结构
|
|
49
50
|
4. **多平台支持**:DocSmith Cloud、自有网站、新建网站
|
|
50
51
|
5. **元数据翻译**:支持将项目信息翻译为多语言
|
|
52
|
+
- 单语言时跳过翻译,直接缓存原始内容
|
|
53
|
+
- 多语言时才调用 AI 进行翻译
|
|
51
54
|
|
|
52
55
|
## 输入输出
|
|
53
56
|
|
|
@@ -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] : []),
|
|
@@ -59,36 +59,47 @@ export default async function translateMeta({ config }, options) {
|
|
|
59
59
|
const titleTranslation = parsedTranslationCache[projectName] || {};
|
|
60
60
|
const descTranslation = parsedTranslationCache[projectDesc] || {};
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
62
|
+
// If only one language, skip translation and cache original content directly
|
|
63
|
+
if (languages.length <= 1) {
|
|
64
|
+
const singleLang = languages[0] || locale || "en";
|
|
65
|
+
if (projectName && !titleTranslation[singleLang]) {
|
|
66
|
+
titleTranslation[singleLang] = projectName;
|
|
67
|
+
}
|
|
68
|
+
if (finalProjectDesc && !descTranslation[singleLang]) {
|
|
69
|
+
descTranslation[singleLang] = finalProjectDesc;
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
// Multiple languages: need translation
|
|
73
|
+
const titleLanguages = languages.filter((lang) => !titleTranslation[lang]);
|
|
74
|
+
const descLanguages = languages.filter((lang) => !descTranslation[lang]);
|
|
75
|
+
const titleTranslationSchema = z.object(
|
|
76
|
+
titleLanguages.reduce((shape, lang) => {
|
|
77
|
+
shape[lang] = z.string();
|
|
78
|
+
return shape;
|
|
79
|
+
}, {}),
|
|
80
|
+
);
|
|
81
|
+
const descTranslationSchema = z.object(
|
|
82
|
+
descLanguages.reduce((shape, lang) => {
|
|
83
|
+
shape[lang] = z.string();
|
|
84
|
+
return shape;
|
|
85
|
+
}, {}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const agent = AIAgent.from({
|
|
89
|
+
name: "translateMeta",
|
|
90
|
+
instructions:
|
|
91
|
+
"You are an **Elite Polyglot Localization and Translation Specialist** with extensive professional experience across multiple domains. Your core mission is to produce translations that are not only **100% accurate** to the source meaning but are also **natively fluent, highly readable, and culturally appropriate** in the target language.",
|
|
92
|
+
inputKey: "message",
|
|
93
|
+
outputSchema: z.object({
|
|
94
|
+
title: titleTranslationSchema.describe("Translated titles with language codes as keys"),
|
|
95
|
+
desc: descTranslationSchema.describe(
|
|
96
|
+
"Translated descriptions with language codes as keys. Each description MUST be within 100 characters.",
|
|
97
|
+
),
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
if (titleLanguages.length > 0 || descLanguages.length > 0) {
|
|
101
|
+
const translatedMetadata = await options.context.invoke(agent, {
|
|
102
|
+
message: `Translate the following title and description into all target languages except the source language. Provide the translations in a JSON object with the language codes as keys. If the project title or description is empty, return an empty string for that field.
|
|
92
103
|
|
|
93
104
|
**IMPORTANT**: The description translations MUST be concise and within 100 characters. If the source description is long, extract and translate only the key points or create a brief summary that captures the essence.
|
|
94
105
|
|
|
@@ -117,23 +128,19 @@ Respond with a JSON object in the following format:
|
|
|
117
128
|
- Be concise and capture the core essence of the project
|
|
118
129
|
- Use natural, fluent language appropriate for the target culture
|
|
119
130
|
- If the source is very long, create a brief summary instead of a full translation
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (translatedMetadata.desc[lang]) {
|
|
134
|
-
descTranslation[lang] = translatedMetadata.desc[lang];
|
|
135
|
-
}
|
|
136
|
-
});
|
|
131
|
+
`,
|
|
132
|
+
});
|
|
133
|
+
Object.keys(translatedMetadata.title || {}).forEach((lang) => {
|
|
134
|
+
if (translatedMetadata.title[lang]) {
|
|
135
|
+
titleTranslation[lang] = translatedMetadata.title[lang];
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
Object.keys(translatedMetadata.desc || {}).forEach((lang) => {
|
|
139
|
+
if (translatedMetadata.desc[lang]) {
|
|
140
|
+
descTranslation[lang] = translatedMetadata.desc[lang];
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
if (!projectDesc && finalProjectDesc) {
|
|
@@ -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
|
package/utils/workspace.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { access, readFile, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { access, readFile, mkdir, writeFile, appendFile } from "node:fs/promises";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { exec } from "node:child_process";
|
|
5
5
|
import { promisify } from "node:util";
|
|
@@ -24,6 +24,17 @@ export const DOC_SMITH_DIR = ".aigne/doc-smith";
|
|
|
24
24
|
export const SOURCES_DIR = "sources";
|
|
25
25
|
export const WORKSPACE_SUBDIRS = ["intent", "planning", "docs"];
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* doc-smith workspace 的 .gitignore 内容
|
|
29
|
+
*/
|
|
30
|
+
export const GITIGNORE_CONTENT = `\
|
|
31
|
+
# Ignore sources directory
|
|
32
|
+
sources/
|
|
33
|
+
|
|
34
|
+
# Ignore temporary files
|
|
35
|
+
tmp/
|
|
36
|
+
`;
|
|
37
|
+
|
|
27
38
|
/**
|
|
28
39
|
* 检查路径是否存在
|
|
29
40
|
* @param {string} path - 路径
|
|
@@ -48,12 +59,13 @@ export function pathExistsSync(path) {
|
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
/**
|
|
51
|
-
*
|
|
52
|
-
* @param {string}
|
|
62
|
+
* 检查是否在 git 仓库内(支持子目录)
|
|
63
|
+
* @param {string} cwd - 工作目录
|
|
53
64
|
* @returns {Promise<boolean>}
|
|
54
65
|
*/
|
|
55
|
-
export async function isGitRepo(
|
|
56
|
-
|
|
66
|
+
export async function isGitRepo(cwd = ".") {
|
|
67
|
+
const result = await gitExec("rev-parse --is-inside-work-tree", cwd);
|
|
68
|
+
return result.success && result.output === "true";
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
/**
|
|
@@ -71,6 +83,49 @@ export async function gitExec(command, cwd = ".") {
|
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
|
|
86
|
+
/**
|
|
87
|
+
* 获取 git 仓库根目录
|
|
88
|
+
* @param {string} cwd - 起始目录
|
|
89
|
+
* @returns {Promise<string | null>}
|
|
90
|
+
*/
|
|
91
|
+
export async function getGitRoot(cwd = ".") {
|
|
92
|
+
const result = await gitExec("rev-parse --show-toplevel", cwd);
|
|
93
|
+
if (result.success) {
|
|
94
|
+
return result.output;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 向 .gitignore 添加忽略规则(如果不存在)
|
|
101
|
+
* @param {string} gitRoot - git 仓库根目录
|
|
102
|
+
* @param {string} pattern - 要忽略的模式
|
|
103
|
+
* @returns {Promise<boolean>} 是否添加成功
|
|
104
|
+
*/
|
|
105
|
+
export async function addToGitignore(gitRoot, pattern) {
|
|
106
|
+
const gitignorePath = join(gitRoot, ".gitignore");
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// 检查 .gitignore 是否存在
|
|
110
|
+
if (await pathExists(gitignorePath)) {
|
|
111
|
+
// 读取现有内容,检查是否已包含该模式
|
|
112
|
+
const content = await readFile(gitignorePath, "utf8");
|
|
113
|
+
if (content.includes(pattern)) {
|
|
114
|
+
return true; // 已存在,无需添加
|
|
115
|
+
}
|
|
116
|
+
// 追加到文件末尾(确保换行)
|
|
117
|
+
const prefix = content.endsWith("\n") ? "" : "\n";
|
|
118
|
+
await appendFile(gitignorePath, `${prefix}${pattern}\n`, "utf8");
|
|
119
|
+
} else {
|
|
120
|
+
// 创建新的 .gitignore
|
|
121
|
+
await writeFile(gitignorePath, `${pattern}\n`, "utf8");
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
74
129
|
/**
|
|
75
130
|
* 检测 workspace 模式(同步版本)
|
|
76
131
|
* 用于需要在模块加载时同步判断的场景
|
|
@@ -179,8 +234,7 @@ export async function initProjectMode() {
|
|
|
179
234
|
await createDirectoryStructure(DOC_SMITH_DIR);
|
|
180
235
|
|
|
181
236
|
// 创建 .gitignore
|
|
182
|
-
|
|
183
|
-
await writeFile(join(DOC_SMITH_DIR, ".gitignore"), gitignoreContent, "utf8");
|
|
237
|
+
await writeFile(join(DOC_SMITH_DIR, ".gitignore"), GITIGNORE_CONTENT, "utf8");
|
|
184
238
|
|
|
185
239
|
// 生成 config.yaml
|
|
186
240
|
const configContent = generateConfig({
|
|
@@ -194,7 +248,7 @@ export async function initProjectMode() {
|
|
|
194
248
|
});
|
|
195
249
|
await writeFile(join(DOC_SMITH_DIR, "config.yaml"), configContent, "utf8");
|
|
196
250
|
|
|
197
|
-
// 在 doc-smith repo
|
|
251
|
+
// 在 doc-smith repo 中创建初始提交
|
|
198
252
|
await gitExec("add .", DOC_SMITH_DIR);
|
|
199
253
|
const commitResult = await gitExec(
|
|
200
254
|
'commit -m "Initial commit: doc-smith workspace"',
|
|
@@ -204,17 +258,20 @@ export async function initProjectMode() {
|
|
|
204
258
|
console.log(`✅ Created initial commit in ${DOC_SMITH_DIR}`);
|
|
205
259
|
}
|
|
206
260
|
|
|
207
|
-
// 如果外层是 git
|
|
208
|
-
const
|
|
261
|
+
// 如果外层是 git 仓库,在其 .gitignore 中添加忽略规则
|
|
262
|
+
const gitRoot = await getGitRoot(".");
|
|
209
263
|
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
const
|
|
264
|
+
if (gitRoot) {
|
|
265
|
+
// 计算相对于 git 根目录的忽略路径
|
|
266
|
+
const cwd = process.cwd();
|
|
267
|
+
const relativePath = cwd.startsWith(gitRoot)
|
|
268
|
+
? cwd.slice(gitRoot.length + 1) // 去掉 gitRoot 和 /
|
|
269
|
+
: "";
|
|
270
|
+
const ignorePattern = relativePath ? `${relativePath}/${DOC_SMITH_DIR}/` : `${DOC_SMITH_DIR}/`;
|
|
213
271
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
console.log(`⚠️ Failed to add submodule: ${result.error}`);
|
|
272
|
+
const added = await addToGitignore(gitRoot, ignorePattern);
|
|
273
|
+
if (added) {
|
|
274
|
+
console.log(`✅ Added ${ignorePattern} to .gitignore`);
|
|
218
275
|
}
|
|
219
276
|
}
|
|
220
277
|
|
|
@@ -239,8 +296,7 @@ export async function initStandaloneMode() {
|
|
|
239
296
|
await gitExec("init");
|
|
240
297
|
|
|
241
298
|
// 创建 .gitignore
|
|
242
|
-
|
|
243
|
-
await writeFile(".gitignore", gitignoreContent, "utf8");
|
|
299
|
+
await writeFile(".gitignore", GITIGNORE_CONTENT, "utf8");
|
|
244
300
|
|
|
245
301
|
// 创建目录结构(包括 sources/)
|
|
246
302
|
await createDirectoryStructure(".", true);
|