czon 0.8.10 → 0.9.1
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/dist/ai/extractMetadataFromMarkdown.js +13 -6
- package/dist/cli.js +1 -0
- package/dist/commands/check.js +33 -0
- package/dist/commands/index.js +3 -1
- package/dist/commands/ls-files.js +7 -2
- package/dist/findEntries.js +9 -7
- package/dist/process/checkLinks.js +260 -0
- package/dist/process/scanSourceFiles.js +1 -1
- package/dist/process/summary.js +65 -8
- package/package.json +1 -1
- package/prompts/summary-base.md +5 -5
|
@@ -7,15 +7,22 @@ const openai_1 = require("../services/openai");
|
|
|
7
7
|
* AI Metadata 提取模块
|
|
8
8
|
*
|
|
9
9
|
* 优化策略说明:
|
|
10
|
-
* 1. 从 MetaData 全局状态读取已有 slug,不作为参数传递
|
|
11
|
-
* 2. 如果已有 slug,条件化 prompt -
|
|
10
|
+
* 1. 从 MetaData 全局状态读取已有 title/slug,不作为参数传递
|
|
11
|
+
* 2. 如果已有 title/slug,条件化 prompt - 完全不提及对应字段的指令
|
|
12
12
|
* 3. trade-off: 优先提升 AI 质量(减少无关任务干扰),可能降低 context 缓存命中率
|
|
13
13
|
*/
|
|
14
14
|
async function extractMetadataFromMarkdown(filePath, content) {
|
|
15
|
-
const
|
|
15
|
+
const existingMeta = metadata_1.MetaData.files.find(f => f.path === filePath)?.metadata;
|
|
16
|
+
const existingSlug = existingMeta?.slug;
|
|
16
17
|
const hasExistingSlug = !!existingSlug;
|
|
18
|
+
const existingTitle = existingMeta?.title;
|
|
19
|
+
const hasExistingTitle = !!existingTitle;
|
|
17
20
|
const fields = [
|
|
18
|
-
|
|
21
|
+
...(hasExistingTitle
|
|
22
|
+
? []
|
|
23
|
+
: [
|
|
24
|
+
'title: 文档的标题(简洁明了,不超过 30 个字。优先采用文档的一级标题或 frontmatter 中的 title 字段,如果没有则自行提炼)',
|
|
25
|
+
]),
|
|
19
26
|
'tags: 关键词列表(3-8 个关键词,使用中文或英文)',
|
|
20
27
|
'description: 文档的简短描述,微摘要(用一句话概括本文核心价值,不超过 100 字符),用于 SEO meta description,社交卡片短描述',
|
|
21
28
|
'summary: 文档中型摘要(用一段话总结文章,包含关键论点和结论,控制在 300 字以内),用于 邮件推送内容,newsletter 介绍',
|
|
@@ -30,7 +37,7 @@ async function extractMetadataFromMarkdown(filePath, content) {
|
|
|
30
37
|
];
|
|
31
38
|
const jsonFields = [
|
|
32
39
|
'{',
|
|
33
|
-
' "title": "文档标题",',
|
|
40
|
+
...(hasExistingTitle ? [] : [' "title": "文档标题",']),
|
|
34
41
|
' "description": "用一句话概括本文核心价值,不超过 100 字符",',
|
|
35
42
|
' "summary": "中型摘要,用一段话总结文章,包含关键论点和结论",',
|
|
36
43
|
' "short_summary": "超短摘要,用 2-3 句话概括文章主要内容,突出核心观点",',
|
|
@@ -70,7 +77,7 @@ ${jsonFields.join('\n')}`;
|
|
|
70
77
|
});
|
|
71
78
|
const metadata = JSON.parse(response.choices[0].message.content);
|
|
72
79
|
const result = {
|
|
73
|
-
title: metadata.title?.trim() || '',
|
|
80
|
+
title: metadata.title?.trim() || existingTitle || '',
|
|
74
81
|
description: metadata.description?.trim() || '',
|
|
75
82
|
short_summary: metadata.short_summary?.trim() || '',
|
|
76
83
|
audience: metadata.audience?.trim() || '',
|
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ const cli = new clipanion_1.Cli({
|
|
|
15
15
|
});
|
|
16
16
|
// 注册命令
|
|
17
17
|
cli.register(commands_1.BuildCommand);
|
|
18
|
+
cli.register(commands_1.CheckCommand);
|
|
18
19
|
cli.register(commands_1.LsFilesCommand);
|
|
19
20
|
cli.register(commands_1.SummaryCommand);
|
|
20
21
|
cli.register(commands_1.ConfigGithubCommand);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CheckCommand = void 0;
|
|
4
|
+
const clipanion_1 = require("clipanion");
|
|
5
|
+
const checkLinks_1 = require("../process/checkLinks");
|
|
6
|
+
class CheckCommand extends clipanion_1.Command {
|
|
7
|
+
async execute() {
|
|
8
|
+
try {
|
|
9
|
+
const issues = await (0, checkLinks_1.checkLinks)();
|
|
10
|
+
const output = (0, checkLinks_1.formatCheckResults)(issues);
|
|
11
|
+
this.context.stdout.write(output + '\n');
|
|
12
|
+
return issues.length > 0 ? 1 : 0;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
this.context.stderr.write(`Check failed: ${error}\n`);
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.CheckCommand = CheckCommand;
|
|
21
|
+
CheckCommand.paths = [['check']];
|
|
22
|
+
CheckCommand.usage = clipanion_1.Command.Usage({
|
|
23
|
+
description: 'Check Markdown files for broken links and path format issues',
|
|
24
|
+
details: `
|
|
25
|
+
This command scans all Markdown files in the current directory and checks for:
|
|
26
|
+
- Dead links (link targets that do not exist)
|
|
27
|
+
- Path format issues (absolute paths starting with /)
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
$ czon check
|
|
31
|
+
`,
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=check.js.map
|
package/dist/commands/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SummaryCommand = exports.LsFilesCommand = exports.ConfigGithubCommand = exports.BuildCommand = void 0;
|
|
3
|
+
exports.SummaryCommand = exports.LsFilesCommand = exports.ConfigGithubCommand = exports.CheckCommand = exports.BuildCommand = void 0;
|
|
4
4
|
var build_1 = require("./build");
|
|
5
5
|
Object.defineProperty(exports, "BuildCommand", { enumerable: true, get: function () { return build_1.BuildCommand; } });
|
|
6
|
+
var check_1 = require("./check");
|
|
7
|
+
Object.defineProperty(exports, "CheckCommand", { enumerable: true, get: function () { return check_1.CheckCommand; } });
|
|
6
8
|
var config_github_1 = require("./config-github");
|
|
7
9
|
Object.defineProperty(exports, "ConfigGithubCommand", { enumerable: true, get: function () { return config_github_1.ConfigGithubCommand; } });
|
|
8
10
|
var ls_files_1 = require("./ls-files");
|
|
@@ -9,10 +9,13 @@ class LsFilesCommand extends clipanion_1.Command {
|
|
|
9
9
|
this.aigc = clipanion_1.Option.Boolean('--aigc', false, {
|
|
10
10
|
description: 'Include files under .czon/AIGC directory',
|
|
11
11
|
});
|
|
12
|
+
this.allTypes = clipanion_1.Option.Boolean('--all-types', false, {
|
|
13
|
+
description: 'List all file types, not just Markdown files',
|
|
14
|
+
});
|
|
12
15
|
}
|
|
13
16
|
async execute() {
|
|
14
17
|
try {
|
|
15
|
-
const files = await (0, findEntries_1.
|
|
18
|
+
const files = await (0, findEntries_1.findEntries)(process.cwd(), { aigc: this.aigc, allTypes: this.allTypes });
|
|
16
19
|
if (files.length === 0) {
|
|
17
20
|
this.context.stdout.write('No markdown files found.\n');
|
|
18
21
|
}
|
|
@@ -35,10 +38,12 @@ LsFilesCommand.usage = clipanion_1.Command.Usage({
|
|
|
35
38
|
description: 'List all markdown files in the current directory',
|
|
36
39
|
details: `
|
|
37
40
|
This command lists all markdown files in the current directory using git.
|
|
38
|
-
It uses the same logic as the internal
|
|
41
|
+
It uses the same logic as the internal findEntries function.
|
|
42
|
+
Use --all-types to list all file types, not just Markdown.
|
|
39
43
|
|
|
40
44
|
Examples:
|
|
41
45
|
$ czon ls-files
|
|
46
|
+
$ czon ls-files --all-types
|
|
42
47
|
`,
|
|
43
48
|
});
|
|
44
49
|
//# sourceMappingURL=ls-files.js.map
|
package/dist/findEntries.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.findEntries = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
const util_1 = require("util");
|
|
7
7
|
const isExists_1 = require("./utils/isExists");
|
|
8
8
|
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
9
9
|
/**
|
|
10
|
-
* 使用git
|
|
10
|
+
* 使用git命令查找项目中的文件
|
|
11
11
|
* 使用git ls-files --others --cached --exclude-standard获取所有文件
|
|
12
|
-
* 然后过滤掉.czon
|
|
12
|
+
* 然后过滤掉.czon目录,默认只保留.md文件
|
|
13
13
|
*
|
|
14
14
|
* @param dirPath 要扫描的目录路径
|
|
15
15
|
* @param options 可选参数
|
|
16
16
|
* @param options.aigc 是否包含 .czon/AIGC 目录下的文件
|
|
17
|
-
* @
|
|
17
|
+
* @param options.allTypes 是否返回所有类型的文件(默认 false,仅返回 .md 文件)
|
|
18
|
+
* @returns Promise<string[]> 返回文件的相对路径数组
|
|
18
19
|
*/
|
|
19
|
-
const
|
|
20
|
+
const findEntries = async (dirPath, options) => {
|
|
20
21
|
const aigc = options?.aigc ?? false;
|
|
22
|
+
const allTypes = options?.allTypes ?? false;
|
|
21
23
|
// 获取git仓库的根目录
|
|
22
24
|
const gitRoot = (await execAsync('git rev-parse --show-toplevel', { cwd: dirPath })).stdout.trim();
|
|
23
25
|
// 使用git命令获取所有文件(包括已跟踪和未跟踪的文件)
|
|
@@ -31,7 +33,7 @@ const findMarkdownEntries = async (dirPath, options) => {
|
|
|
31
33
|
.split('\0') // 按空字符分割文件名
|
|
32
34
|
.filter(line => line.trim() !== '') // 移除空行
|
|
33
35
|
.filter(file => !file.startsWith('.') || (aigc && file.startsWith('.czon/AIGC/'))) // 过滤掉隐藏目录下的文件(aigc 模式下保留 .czon/AIGC/)
|
|
34
|
-
.filter(file => file.endsWith('.md')); //
|
|
36
|
+
.filter(file => allTypes || file.endsWith('.md')); // allTypes 时返回所有文件,否则只保留 .md 文件
|
|
35
37
|
// 排除文件系统中不存在的文件
|
|
36
38
|
const existingFiles = [];
|
|
37
39
|
for (const file of files) {
|
|
@@ -41,5 +43,5 @@ const findMarkdownEntries = async (dirPath, options) => {
|
|
|
41
43
|
}
|
|
42
44
|
return existingFiles;
|
|
43
45
|
};
|
|
44
|
-
exports.
|
|
46
|
+
exports.findEntries = findEntries;
|
|
45
47
|
//# sourceMappingURL=findEntries.js.map
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.checkLinks = checkLinks;
|
|
7
|
+
exports.formatCheckResults = formatCheckResults;
|
|
8
|
+
const promises_1 = require("fs/promises");
|
|
9
|
+
const marked_1 = require("marked");
|
|
10
|
+
/**
|
|
11
|
+
* 独立的 marked 实例,仅用于链接检查的词法分析。
|
|
12
|
+
* 不加载 marked-footnote / marked-katex 等全局扩展,避免扩展 tokenizer 的兼容性问题。
|
|
13
|
+
*/
|
|
14
|
+
const lexer = new marked_1.Marked({ gfm: true });
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const findEntries_1 = require("../findEntries");
|
|
17
|
+
const paths_1 = require("../paths");
|
|
18
|
+
const isExists_1 = require("../utils/isExists");
|
|
19
|
+
/**
|
|
20
|
+
* 递归遍历 marked token 树,收集所有真实的 link 和 image token。
|
|
21
|
+
* 自动跳过 code(代码块)和 codespan(行内代码)中的内容。
|
|
22
|
+
*/
|
|
23
|
+
function collectLinksFromTokens(tokens, results) {
|
|
24
|
+
for (const token of tokens) {
|
|
25
|
+
if (token.type === 'link') {
|
|
26
|
+
const link = token;
|
|
27
|
+
// 跳过脚注引用 — 无 marked-footnote 时 [^label] 会被误解析为链接
|
|
28
|
+
if (/^\[\^[^\]]+\]/.test(link.raw))
|
|
29
|
+
continue;
|
|
30
|
+
results.push({ raw: link.raw, href: link.href, isImage: false });
|
|
31
|
+
}
|
|
32
|
+
else if (token.type === 'image') {
|
|
33
|
+
const image = token;
|
|
34
|
+
results.push({ raw: image.raw, href: image.href, isImage: true });
|
|
35
|
+
}
|
|
36
|
+
// 跳过 code / codespan — 其中的链接语法不是真正的链接
|
|
37
|
+
if (token.type === 'code' || token.type === 'codespan')
|
|
38
|
+
continue;
|
|
39
|
+
// 递归子 token
|
|
40
|
+
if ('tokens' in token && Array.isArray(token.tokens)) {
|
|
41
|
+
collectLinksFromTokens(token.tokens, results);
|
|
42
|
+
}
|
|
43
|
+
// list -> items
|
|
44
|
+
if (token.type === 'list') {
|
|
45
|
+
const list = token;
|
|
46
|
+
for (const item of list.items) {
|
|
47
|
+
if (item.tokens) {
|
|
48
|
+
collectLinksFromTokens(item.tokens, results);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// table -> header cells + row cells
|
|
53
|
+
if (token.type === 'table') {
|
|
54
|
+
const table = token;
|
|
55
|
+
for (const cell of table.header) {
|
|
56
|
+
if (cell.tokens) {
|
|
57
|
+
collectLinksFromTokens(cell.tokens, results);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const row of table.rows) {
|
|
61
|
+
for (const cell of row) {
|
|
62
|
+
if (cell.tokens) {
|
|
63
|
+
collectLinksFromTokens(cell.tokens, results);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* 根据 token 的 raw 字段在原始内容中定位行号(1-based)。
|
|
72
|
+
* 使用递增的 searchIndex 来正确处理重复出现的相同 raw 文本。
|
|
73
|
+
*/
|
|
74
|
+
function findLineNumber(content, raw, startIndex) {
|
|
75
|
+
const idx = content.indexOf(raw, startIndex);
|
|
76
|
+
if (idx === -1) {
|
|
77
|
+
return { line: -1, nextIndex: startIndex };
|
|
78
|
+
}
|
|
79
|
+
const line = content.substring(0, idx).split('\n').length;
|
|
80
|
+
return { line, nextIndex: idx + raw.length };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 使用 marked 词法分析器从 Markdown 内容中提取所有真实链接及其行号。
|
|
84
|
+
* 代码块和行内代码中的链接语法会被正确忽略。
|
|
85
|
+
*/
|
|
86
|
+
function extractLinksWithLineNumbers(content) {
|
|
87
|
+
const tokens = lexer.lexer(content);
|
|
88
|
+
const extracted = [];
|
|
89
|
+
collectLinksFromTokens(tokens, extracted);
|
|
90
|
+
// 通过 raw 字段在原始内容中搜索来确定行号
|
|
91
|
+
const results = [];
|
|
92
|
+
let searchIndex = 0;
|
|
93
|
+
for (const link of extracted) {
|
|
94
|
+
const { line, nextIndex } = findLineNumber(content, link.raw, searchIndex);
|
|
95
|
+
searchIndex = nextIndex;
|
|
96
|
+
results.push({ line, raw: link.raw, href: link.href, isImage: link.isImage });
|
|
97
|
+
}
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* 检查所有 Markdown 文件中的超链接
|
|
102
|
+
* @returns 发现的问题列表
|
|
103
|
+
*/
|
|
104
|
+
async function checkLinks() {
|
|
105
|
+
console.log('🔍 正在扫描 Markdown 文件...');
|
|
106
|
+
const markdownFiles = await (0, findEntries_1.findEntries)(paths_1.INPUT_DIR, { aigc: true });
|
|
107
|
+
const allFiles = await (0, findEntries_1.findEntries)(paths_1.INPUT_DIR, { allTypes: true });
|
|
108
|
+
const issues = [];
|
|
109
|
+
// 构建已知文件集合,用于死链接检测
|
|
110
|
+
const knownFiles = new Set(markdownFiles);
|
|
111
|
+
// 构建全部文件集合,用于建议生成
|
|
112
|
+
const allFilesSet = new Set(allFiles);
|
|
113
|
+
// 构建 basename -> 相对路径列表 的索引,用于模糊匹配
|
|
114
|
+
const basenameIndex = new Map();
|
|
115
|
+
for (const file of allFiles) {
|
|
116
|
+
const base = path_1.default.basename(file);
|
|
117
|
+
const list = basenameIndex.get(base) ?? [];
|
|
118
|
+
list.push(file);
|
|
119
|
+
basenameIndex.set(base, list);
|
|
120
|
+
}
|
|
121
|
+
console.log(`📄 发现 ${markdownFiles.length} 个 Markdown 文件,${allFiles.length} 个总文件\n`);
|
|
122
|
+
for (const filePath of markdownFiles) {
|
|
123
|
+
const fullPath = path_1.default.join(paths_1.INPUT_DIR, filePath);
|
|
124
|
+
const content = await (0, promises_1.readFile)(fullPath, 'utf-8');
|
|
125
|
+
const links = extractLinksWithLineNumbers(content);
|
|
126
|
+
for (const link of links) {
|
|
127
|
+
// 跳过外部 URL
|
|
128
|
+
if (URL.canParse(link.href))
|
|
129
|
+
continue;
|
|
130
|
+
// 跳过锚点链接
|
|
131
|
+
if (link.href.startsWith('#'))
|
|
132
|
+
continue;
|
|
133
|
+
// 去掉锚点部分
|
|
134
|
+
const hrefWithoutHash = link.href.split('#')[0];
|
|
135
|
+
if (!hrefWithoutHash)
|
|
136
|
+
continue;
|
|
137
|
+
// 检查 1: 路径格式规范 — 不应使用 / 开头的绝对路径
|
|
138
|
+
if (hrefWithoutHash.startsWith('/')) {
|
|
139
|
+
// 尝试生成建议:去掉开头的 /,看是否能在已知文件中找到
|
|
140
|
+
const suggestions = [];
|
|
141
|
+
const targetRelative = hrefWithoutHash.slice(1); // 去掉开头的 /
|
|
142
|
+
if (allFilesSet.has(targetRelative)) {
|
|
143
|
+
const suggested = path_1.default.relative(path_1.default.dirname(filePath), targetRelative);
|
|
144
|
+
suggestions.push(suggested);
|
|
145
|
+
}
|
|
146
|
+
issues.push({
|
|
147
|
+
file: filePath,
|
|
148
|
+
line: link.line,
|
|
149
|
+
raw: link.raw,
|
|
150
|
+
href: link.href,
|
|
151
|
+
type: 'absolute-path',
|
|
152
|
+
message: '不应使用 / 开头的绝对路径,请使用相对路径',
|
|
153
|
+
suggestions,
|
|
154
|
+
});
|
|
155
|
+
// 绝对路径无法正确解析,跳过死链接检测
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
// 解析相对路径
|
|
159
|
+
const resolvedRelative = path_1.default.normalize(path_1.default.join(path_1.default.dirname(filePath), hrefWithoutHash));
|
|
160
|
+
// 检查是否跳出项目目录
|
|
161
|
+
if (resolvedRelative.startsWith('..')) {
|
|
162
|
+
issues.push({
|
|
163
|
+
file: filePath,
|
|
164
|
+
line: link.line,
|
|
165
|
+
raw: link.raw,
|
|
166
|
+
href: link.href,
|
|
167
|
+
type: 'dead-link',
|
|
168
|
+
message: '链接指向项目目录之外',
|
|
169
|
+
suggestions: [],
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// 检查 2: .czon/src 边界隔离 — 外部文件不能引用 .czon/src 内部的文件
|
|
174
|
+
const sourceIsInternal = filePath.startsWith('.czon/src/');
|
|
175
|
+
const targetIsInternal = resolvedRelative.startsWith('.czon/src/');
|
|
176
|
+
if (!sourceIsInternal && targetIsInternal) {
|
|
177
|
+
issues.push({
|
|
178
|
+
file: filePath,
|
|
179
|
+
line: link.line,
|
|
180
|
+
raw: link.raw,
|
|
181
|
+
href: link.href,
|
|
182
|
+
type: 'cross-boundary',
|
|
183
|
+
message: '不能引用 .czon/src 内部的文件',
|
|
184
|
+
suggestions: [],
|
|
185
|
+
});
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// 检查 2: 死链接检测 — 目标文件是否存在
|
|
189
|
+
// 先检查已知的 Markdown 文件集合
|
|
190
|
+
if (hrefWithoutHash.endsWith('.md') && knownFiles.has(resolvedRelative)) {
|
|
191
|
+
continue; // 文件存在于已知集合中
|
|
192
|
+
}
|
|
193
|
+
// 再检查文件系统
|
|
194
|
+
const resolvedFullPath = path_1.default.join(paths_1.INPUT_DIR, resolvedRelative);
|
|
195
|
+
if (!(await (0, isExists_1.isExists)(resolvedFullPath))) {
|
|
196
|
+
// 通过 basename 模糊匹配,生成候选建议(最多 3 个)
|
|
197
|
+
const suggestions = [];
|
|
198
|
+
const targetBasename = path_1.default.basename(hrefWithoutHash);
|
|
199
|
+
const candidates = basenameIndex.get(targetBasename) ?? [];
|
|
200
|
+
for (const candidate of candidates) {
|
|
201
|
+
if (suggestions.length >= 3)
|
|
202
|
+
break;
|
|
203
|
+
const suggested = path_1.default.relative(path_1.default.dirname(filePath), candidate);
|
|
204
|
+
suggestions.push(suggested);
|
|
205
|
+
}
|
|
206
|
+
issues.push({
|
|
207
|
+
file: filePath,
|
|
208
|
+
line: link.line,
|
|
209
|
+
raw: link.raw,
|
|
210
|
+
href: link.href,
|
|
211
|
+
type: 'dead-link',
|
|
212
|
+
message: '目标文件不存在',
|
|
213
|
+
suggestions,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return issues;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 格式化并输出检查结果到终端
|
|
222
|
+
*/
|
|
223
|
+
function formatCheckResults(issues) {
|
|
224
|
+
if (issues.length === 0) {
|
|
225
|
+
return '✅ 未发现链接问题。';
|
|
226
|
+
}
|
|
227
|
+
const lines = [];
|
|
228
|
+
// 按文件分组
|
|
229
|
+
const grouped = new Map();
|
|
230
|
+
for (const issue of issues) {
|
|
231
|
+
const list = grouped.get(issue.file) ?? [];
|
|
232
|
+
list.push(issue);
|
|
233
|
+
grouped.set(issue.file, list);
|
|
234
|
+
}
|
|
235
|
+
for (const [file, fileIssues] of grouped) {
|
|
236
|
+
lines.push(file);
|
|
237
|
+
for (const issue of fileIssues) {
|
|
238
|
+
const icon = issue.type === 'dead-link' ? '✖' : issue.type === 'cross-boundary' ? '⊘' : '⚠';
|
|
239
|
+
lines.push(` line ${issue.line}: ${icon} ${issue.raw}`);
|
|
240
|
+
lines.push(` ${issue.message}`);
|
|
241
|
+
for (const suggestion of issue.suggestions) {
|
|
242
|
+
lines.push(` 建议: ${suggestion}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
lines.push('');
|
|
246
|
+
}
|
|
247
|
+
const deadCount = issues.filter(i => i.type === 'dead-link').length;
|
|
248
|
+
const formatCount = issues.filter(i => i.type === 'absolute-path').length;
|
|
249
|
+
const boundaryCount = issues.filter(i => i.type === 'cross-boundary').length;
|
|
250
|
+
const parts = [];
|
|
251
|
+
if (deadCount > 0)
|
|
252
|
+
parts.push(`${deadCount} 个死链接`);
|
|
253
|
+
if (formatCount > 0)
|
|
254
|
+
parts.push(`${formatCount} 个路径格式问题`);
|
|
255
|
+
if (boundaryCount > 0)
|
|
256
|
+
parts.push(`${boundaryCount} 个跨边界引用`);
|
|
257
|
+
lines.push(`发现 ${issues.length} 个问题(${parts.join(',')}),涉及 ${grouped.size} 个文件。`);
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=checkLinks.js.map
|
|
@@ -25,7 +25,7 @@ async function scanSourceFiles() {
|
|
|
25
25
|
console.log(`🔍 Scanning source directory...`);
|
|
26
26
|
const queue = [];
|
|
27
27
|
const isVisited = new Set();
|
|
28
|
-
const markdownFiles = await (0, findEntries_1.
|
|
28
|
+
const markdownFiles = await (0, findEntries_1.findEntries)(paths_1.INPUT_DIR, { aigc: true });
|
|
29
29
|
for (const filePath of markdownFiles) {
|
|
30
30
|
queue.push(filePath);
|
|
31
31
|
}
|
package/dist/process/summary.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.processSummary = void 0;
|
|
4
4
|
const promises_1 = require("fs/promises");
|
|
5
5
|
const path_1 = require("path");
|
|
6
|
+
const metadata_1 = require("../metadata");
|
|
6
7
|
const paths_1 = require("../paths");
|
|
7
8
|
const opencode_1 = require("../services/opencode");
|
|
8
9
|
// Prompt 模板目录路径(在项目根目录的 prompts/ 文件夹中)
|
|
@@ -13,14 +14,54 @@ const SUMMARY_DIR = (0, path_1.join)(paths_1.CZON_DIR, 'AIGC', 'SUMMARY');
|
|
|
13
14
|
const MAX_RETRIES = 3;
|
|
14
15
|
// 风格配置
|
|
15
16
|
const SUMMARY_STYLES = [
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{
|
|
23
|
-
|
|
17
|
+
{
|
|
18
|
+
skill: 'summary-objective',
|
|
19
|
+
name: '客观中立',
|
|
20
|
+
title: 'AI 总结: 客观中立风格',
|
|
21
|
+
slug: 'aigc-summary-objective',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
skill: 'summary-critical',
|
|
25
|
+
name: '客观批判',
|
|
26
|
+
title: 'AI 总结: 客观批判风格',
|
|
27
|
+
slug: 'aigc-summary-critical',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
skill: 'summary-positive',
|
|
31
|
+
name: '赞扬鼓励',
|
|
32
|
+
title: 'AI 总结: 赞扬鼓励风格',
|
|
33
|
+
slug: 'aigc-summary-positive',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
skill: 'summary-popular',
|
|
37
|
+
name: '科普介绍',
|
|
38
|
+
title: 'AI 总结: 科普介绍风格',
|
|
39
|
+
slug: 'aigc-summary-popular',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
skill: 'summary-artistic',
|
|
43
|
+
name: '文艺感性',
|
|
44
|
+
title: 'AI 总结: 文艺感性风格',
|
|
45
|
+
slug: 'aigc-summary-artistic',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
skill: 'summary-philosophical',
|
|
49
|
+
name: '哲学思辨',
|
|
50
|
+
title: 'AI 总结: 哲学思辨风格',
|
|
51
|
+
slug: 'aigc-summary-philosophical',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
skill: 'summary-psychological',
|
|
55
|
+
name: '心理分析',
|
|
56
|
+
title: 'AI 总结: 心理分析风格',
|
|
57
|
+
slug: 'aigc-summary-psychological',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
skill: 'summary-historical',
|
|
61
|
+
name: '历史时间跨度',
|
|
62
|
+
title: 'AI 总结: 历史时间跨度风格',
|
|
63
|
+
slug: 'aigc-summary-historical',
|
|
64
|
+
},
|
|
24
65
|
];
|
|
25
66
|
/**
|
|
26
67
|
* 读取 Prompt 模板文件内容
|
|
@@ -114,6 +155,8 @@ ${styleContent}
|
|
|
114
155
|
*/
|
|
115
156
|
const processSummary = async (model) => {
|
|
116
157
|
const cwd = process.cwd();
|
|
158
|
+
// 加载 MetaData 以便写入预设的 title/slug
|
|
159
|
+
await (0, metadata_1.loadMetaData)();
|
|
117
160
|
// 加载基础规则
|
|
118
161
|
console.info('📖 加载基础规则...');
|
|
119
162
|
const baseContent = await loadPromptTemplate('summary-base');
|
|
@@ -129,6 +172,18 @@ const processSummary = async (model) => {
|
|
|
129
172
|
...result,
|
|
130
173
|
});
|
|
131
174
|
if (result.success) {
|
|
175
|
+
// 预设 title 和 slug 到 MetaData(不设 hash,确保后续 AI 提取不跳过)
|
|
176
|
+
const relativePath = `.czon/AIGC/SUMMARY/${style.skill}.md`;
|
|
177
|
+
let fileMeta = metadata_1.MetaData.files.find(f => f.path === relativePath);
|
|
178
|
+
if (!fileMeta) {
|
|
179
|
+
fileMeta = { path: relativePath, links: [] };
|
|
180
|
+
metadata_1.MetaData.files.push(fileMeta);
|
|
181
|
+
}
|
|
182
|
+
fileMeta.metadata = {
|
|
183
|
+
...fileMeta.metadata,
|
|
184
|
+
title: style.title,
|
|
185
|
+
slug: style.slug,
|
|
186
|
+
};
|
|
132
187
|
console.info(`✅ 「${style.name}」风格报告生成成功\n`);
|
|
133
188
|
}
|
|
134
189
|
else {
|
|
@@ -167,6 +222,8 @@ const processSummary = async (model) => {
|
|
|
167
222
|
const missingCount = SUMMARY_STYLES.length - successCount;
|
|
168
223
|
throw new Error(`生成不完整: ${missingCount} 个报告未能成功生成。请检查上述错误信息并重试。`);
|
|
169
224
|
}
|
|
225
|
+
// 保存 MetaData(包含预设的 title/slug)
|
|
226
|
+
await (0, metadata_1.saveMetaData)();
|
|
170
227
|
};
|
|
171
228
|
exports.processSummary = processSummary;
|
|
172
229
|
//# sourceMappingURL=summary.js.map
|
package/package.json
CHANGED
package/prompts/summary-base.md
CHANGED
|
@@ -115,20 +115,20 @@
|
|
|
115
115
|
- 引用原文链接时,保证链接有效
|
|
116
116
|
- **永远链接到具体的 Markdown 文件**,不要链接目录
|
|
117
117
|
- **链接文本应当是对应的标题**,而不是文件名
|
|
118
|
-
- 由于生成到 SUMMARY 目录,引用时使用
|
|
118
|
+
- 由于生成到 `.czon/AIGC/SUMMARY/` 目录,引用时使用 `../../../` 开头的相对路径
|
|
119
119
|
|
|
120
120
|
**正确示例**:
|
|
121
121
|
|
|
122
122
|
```markdown
|
|
123
|
-
[资本持久战:个人投资者跨越阶级的战略](
|
|
124
|
-
[从创作到分发——构建AI-Native内容引擎](
|
|
123
|
+
[资本持久战:个人投资者跨越阶级的战略](../../../INSIGHTS/6.md)
|
|
124
|
+
[从创作到分发——构建AI-Native内容引擎](../../../INSIGHTS/4.md)
|
|
125
125
|
```
|
|
126
126
|
|
|
127
127
|
**错误示例**:
|
|
128
128
|
|
|
129
129
|
```markdown
|
|
130
|
-
[INSIGHTS/6.md](
|
|
131
|
-
[资本持久战](
|
|
130
|
+
[INSIGHTS/6.md](../../../INSIGHTS/6.md) ← 使用了文件名而非标题
|
|
131
|
+
[资本持久战](../../../INSIGHTS/) ← 链接到了目录
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
### 3. 头部格式
|