growork 1.0.1 → 1.1.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/.claude/commands/review.md +62 -0
- package/README.md +92 -21
- package/claude.md +72 -1
- package/dist/index.js +185 -71
- package/docs/product/prd-1.0.0.md +86 -0
- package/docs/product/prd-1.1.0.md +215 -0
- package/docs/product/prd-template.md +65 -0
- package/growork.config.yaml +31 -33
- package/package.json +1 -1
- package/src/commands/init.ts +1 -5
- package/src/commands/list.ts +52 -19
- package/src/commands/sync.ts +40 -26
- package/src/index.ts +13 -4
- package/src/utils/config.ts +156 -40
- package/tests/config.test.ts +372 -3
- package/{docs/test → tests}/test-cases.md +76 -26
- package/docs/product/prd-v1.0.md +0 -418
- package/test/backend-ai.md +0 -539
- package/test/backend-api.md +0 -1236
- package/test/prd-5spread.md +0 -74
- package/test/push-prd-notion.md +0 -126
- package/test/push.md +0 -119
package/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ const program = new Command();
|
|
|
10
10
|
program
|
|
11
11
|
.name('growork')
|
|
12
12
|
.description('将飞书文档同步到本地,为 AI Agent 提供完整上下文')
|
|
13
|
-
.version('
|
|
13
|
+
.version('2.0.0');
|
|
14
14
|
|
|
15
15
|
program
|
|
16
16
|
.command('init')
|
|
@@ -18,9 +18,18 @@ program
|
|
|
18
18
|
.action(initCommand);
|
|
19
19
|
|
|
20
20
|
program
|
|
21
|
-
.command('sync
|
|
22
|
-
.description('
|
|
23
|
-
.
|
|
21
|
+
.command('sync')
|
|
22
|
+
.description('同步文档')
|
|
23
|
+
.option('--ver <version>', '只同步指定版本')
|
|
24
|
+
.option('-f, --feature <feature>', '只同步指定 feature')
|
|
25
|
+
.option('-c, --custom', '只同步全局文档')
|
|
26
|
+
.action((options) => {
|
|
27
|
+
// 将 ver 映射到 version
|
|
28
|
+
if (options.ver) {
|
|
29
|
+
options.version = options.ver;
|
|
30
|
+
}
|
|
31
|
+
syncCommand(options);
|
|
32
|
+
});
|
|
24
33
|
|
|
25
34
|
program
|
|
26
35
|
.command('list')
|
package/src/utils/config.ts
CHANGED
|
@@ -2,13 +2,6 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as yaml from 'yaml';
|
|
4
4
|
|
|
5
|
-
export interface DocConfig {
|
|
6
|
-
name: string;
|
|
7
|
-
type: 'feishu' | 'notion';
|
|
8
|
-
url: string;
|
|
9
|
-
output: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
5
|
export interface NotionConfig {
|
|
13
6
|
token: string;
|
|
14
7
|
}
|
|
@@ -16,13 +9,40 @@ export interface NotionConfig {
|
|
|
16
9
|
export interface FeishuConfig {
|
|
17
10
|
appId: string;
|
|
18
11
|
appSecret: string;
|
|
19
|
-
domain?: 'feishu' | 'lark';
|
|
12
|
+
domain?: 'feishu' | 'lark';
|
|
20
13
|
}
|
|
14
|
+
export type DocInput = string | { url: string; name?: string };
|
|
15
|
+
|
|
16
|
+
export type FeatureValue = DocInput[] | {
|
|
17
|
+
prd?: DocInput[];
|
|
18
|
+
design?: DocInput[];
|
|
19
|
+
api?: DocInput[];
|
|
20
|
+
test?: DocInput[];
|
|
21
|
+
};
|
|
21
22
|
|
|
22
|
-
export interface
|
|
23
|
+
export interface GroworkConfigV2 {
|
|
23
24
|
feishu?: FeishuConfig;
|
|
24
25
|
notion?: NotionConfig;
|
|
25
|
-
|
|
26
|
+
outputDir?: string; // 输出根目录,默认 "docs"
|
|
27
|
+
custom?: DocInput[];
|
|
28
|
+
versions?: {
|
|
29
|
+
[version: string]: {
|
|
30
|
+
[feature: string]: FeatureValue;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface NormalizedDoc {
|
|
36
|
+
url: string;
|
|
37
|
+
name?: string;
|
|
38
|
+
type: 'feishu' | 'notion';
|
|
39
|
+
outputPath: string; // 含 {title} 占位符
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SyncOptions {
|
|
43
|
+
version?: string;
|
|
44
|
+
feature?: string;
|
|
45
|
+
custom?: boolean;
|
|
26
46
|
}
|
|
27
47
|
|
|
28
48
|
const CONFIG_FILE_NAME = 'growork.config.yaml';
|
|
@@ -35,38 +55,122 @@ export function configExists(): boolean {
|
|
|
35
55
|
return fs.existsSync(getConfigPath());
|
|
36
56
|
}
|
|
37
57
|
|
|
38
|
-
export function
|
|
39
|
-
|
|
58
|
+
export function inferDocType(url: string): 'feishu' | 'notion' {
|
|
59
|
+
if (url.includes('feishu.cn') || url.includes('larksuite.com')) {
|
|
60
|
+
return 'feishu';
|
|
61
|
+
}
|
|
62
|
+
if (url.includes('notion.so') || url.includes('notion.site')) {
|
|
63
|
+
return 'notion';
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`无法从 URL 推断文档类型: ${url}`);
|
|
66
|
+
}
|
|
40
67
|
|
|
41
|
-
|
|
42
|
-
|
|
68
|
+
export function sanitizeFileName(title: string): string {
|
|
69
|
+
return title
|
|
70
|
+
.replace(/[\/\\:*?"<>|]/g, '') // 移除文件系统保留字符
|
|
71
|
+
.replace(/\s+/g, '-') // 空格替换为 -
|
|
72
|
+
.replace(/-+/g, '-') // 连续 - 合并
|
|
73
|
+
.replace(/^-|-$/g, ''); // 首尾 - 去除
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function parseDocInput(input: DocInput): { url: string; name?: string } {
|
|
77
|
+
if (typeof input === 'string') {
|
|
78
|
+
return { url: input };
|
|
43
79
|
}
|
|
80
|
+
return input;
|
|
81
|
+
}
|
|
44
82
|
|
|
45
|
-
|
|
46
|
-
|
|
83
|
+
function isTypedFeature(value: FeatureValue): value is { prd?: DocInput[]; design?: DocInput[]; api?: DocInput[]; test?: DocInput[] } {
|
|
84
|
+
return !Array.isArray(value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function normalizeConfig(config: GroworkConfigV2, options: SyncOptions = {}): NormalizedDoc[] {
|
|
88
|
+
const docs: NormalizedDoc[] = [];
|
|
89
|
+
const outputDir = config.outputDir || 'docs';
|
|
90
|
+
|
|
91
|
+
// 处理 custom 文档
|
|
92
|
+
if (config.custom && (options.custom || (!options.version && !options.feature))) {
|
|
93
|
+
for (const input of config.custom) {
|
|
94
|
+
const { url, name } = parseDocInput(input);
|
|
95
|
+
docs.push({
|
|
96
|
+
url,
|
|
97
|
+
name,
|
|
98
|
+
type: inferDocType(url),
|
|
99
|
+
outputPath: `${outputDir}/custom/{title}.md`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 如果只请求 custom,直接返回
|
|
105
|
+
if (options.custom) {
|
|
106
|
+
return docs;
|
|
107
|
+
}
|
|
47
108
|
|
|
48
|
-
//
|
|
49
|
-
if (
|
|
50
|
-
|
|
109
|
+
// 处理 versions
|
|
110
|
+
if (config.versions) {
|
|
111
|
+
for (const [version, features] of Object.entries(config.versions)) {
|
|
112
|
+
// 版本过滤
|
|
113
|
+
if (options.version && options.version !== version) continue;
|
|
114
|
+
|
|
115
|
+
for (const [feature, value] of Object.entries(features)) {
|
|
116
|
+
// feature 过滤
|
|
117
|
+
if (options.feature && options.feature !== feature) continue;
|
|
118
|
+
|
|
119
|
+
if (isTypedFeature(value)) {
|
|
120
|
+
// 分类型的 feature
|
|
121
|
+
for (const docType of ['prd', 'design', 'api', 'test'] as const) {
|
|
122
|
+
const docInputs = value[docType];
|
|
123
|
+
if (!docInputs) continue;
|
|
124
|
+
|
|
125
|
+
for (const input of docInputs) {
|
|
126
|
+
const { url, name } = parseDocInput(input);
|
|
127
|
+
docs.push({
|
|
128
|
+
url,
|
|
129
|
+
name,
|
|
130
|
+
type: inferDocType(url),
|
|
131
|
+
outputPath: `${outputDir}/${version}/${feature}/${docType}/{title}.md`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// 简单 feature(数组形式)
|
|
137
|
+
for (const input of value) {
|
|
138
|
+
const { url, name } = parseDocInput(input);
|
|
139
|
+
docs.push({
|
|
140
|
+
url,
|
|
141
|
+
name,
|
|
142
|
+
type: inferDocType(url),
|
|
143
|
+
outputPath: `${outputDir}/${version}/${feature}/{title}.md`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
51
149
|
}
|
|
52
150
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
151
|
+
return docs;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function loadConfigV2(): GroworkConfigV2 {
|
|
155
|
+
const configPath = getConfigPath();
|
|
56
156
|
|
|
57
|
-
if (
|
|
58
|
-
throw new Error(
|
|
157
|
+
if (!fs.existsSync(configPath)) {
|
|
158
|
+
throw new Error(`配置文件不存在: ${configPath}\n请先运行 growork init 初始化配置`);
|
|
59
159
|
}
|
|
60
160
|
|
|
61
|
-
|
|
62
|
-
|
|
161
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
162
|
+
const config = yaml.parse(content) as GroworkConfigV2;
|
|
163
|
+
|
|
164
|
+
// v2.0 至少需要 custom 或 versions
|
|
165
|
+
if (!config.custom && !config.versions) {
|
|
166
|
+
throw new Error('配置文件中没有配置任何文档(custom 或 versions)');
|
|
63
167
|
}
|
|
64
168
|
|
|
65
169
|
return config;
|
|
66
170
|
}
|
|
67
171
|
|
|
68
172
|
export function getDefaultConfig(): string {
|
|
69
|
-
return `# Growork 配置文件
|
|
173
|
+
return `# Growork v2.0 配置文件
|
|
70
174
|
|
|
71
175
|
# 飞书应用凭证 (使用飞书文档时需要)
|
|
72
176
|
feishu:
|
|
@@ -78,18 +182,30 @@ feishu:
|
|
|
78
182
|
notion:
|
|
79
183
|
token: "ntn_xxxx" # Notion Integration Token
|
|
80
184
|
|
|
81
|
-
#
|
|
82
|
-
docs
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
185
|
+
# 输出根目录(默认 "docs")
|
|
186
|
+
outputDir: "docs"
|
|
187
|
+
|
|
188
|
+
# 全局文档(不跟版本)
|
|
189
|
+
custom:
|
|
190
|
+
- "https://xxx.feishu.cn/docx/xxxxx" # 最简写法
|
|
191
|
+
# - url: "https://www.notion.so/xxxxx"
|
|
192
|
+
# name: "技术架构" # 可选:自定义名称
|
|
193
|
+
|
|
194
|
+
# 版本化文档
|
|
195
|
+
versions:
|
|
196
|
+
v1.0:
|
|
197
|
+
用户登录:
|
|
198
|
+
prd:
|
|
199
|
+
- "https://xxx.feishu.cn/docx/xxxxx"
|
|
200
|
+
# design:
|
|
201
|
+
# - "https://xxx.feishu.cn/docx/yyyyy"
|
|
202
|
+
# api:
|
|
203
|
+
# - "https://xxx.feishu.cn/docx/zzzzz"
|
|
204
|
+
# test:
|
|
205
|
+
# - "https://xxx.feishu.cn/docx/aaaaa"
|
|
206
|
+
|
|
207
|
+
# 简单 feature 可不分类
|
|
208
|
+
# 小优化:
|
|
209
|
+
# - "https://xxx.feishu.cn/docx/bbbbb"
|
|
94
210
|
`;
|
|
95
211
|
}
|
package/tests/config.test.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as path from 'path';
|
|
5
3
|
import * as yaml from 'yaml';
|
|
6
4
|
|
|
7
|
-
//
|
|
5
|
+
// ============ V1.0 配置类型(用于兼容性测试)============
|
|
8
6
|
interface DocConfig {
|
|
9
7
|
name: string;
|
|
10
8
|
type: 'feishu' | 'notion';
|
|
@@ -35,6 +33,130 @@ function validateConfig(config: GroworkConfig): void {
|
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
|
|
36
|
+
// ============ V2.0 配置类型和函数(从 config.ts 复制)============
|
|
37
|
+
type DocInput = string | { url: string; name?: string };
|
|
38
|
+
|
|
39
|
+
type FeatureValue = DocInput[] | {
|
|
40
|
+
prd?: DocInput[];
|
|
41
|
+
design?: DocInput[];
|
|
42
|
+
api?: DocInput[];
|
|
43
|
+
test?: DocInput[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
interface GroworkConfigV2 {
|
|
47
|
+
feishu?: { appId: string; appSecret: string; domain?: 'feishu' | 'lark' };
|
|
48
|
+
notion?: { token: string };
|
|
49
|
+
outputDir?: string;
|
|
50
|
+
custom?: DocInput[];
|
|
51
|
+
versions?: {
|
|
52
|
+
[version: string]: {
|
|
53
|
+
[feature: string]: FeatureValue;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface NormalizedDoc {
|
|
59
|
+
url: string;
|
|
60
|
+
name?: string;
|
|
61
|
+
type: 'feishu' | 'notion';
|
|
62
|
+
outputPath: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface SyncOptions {
|
|
66
|
+
version?: string;
|
|
67
|
+
feature?: string;
|
|
68
|
+
custom?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function inferDocType(url: string): 'feishu' | 'notion' {
|
|
72
|
+
if (url.includes('feishu.cn') || url.includes('larksuite.com')) {
|
|
73
|
+
return 'feishu';
|
|
74
|
+
}
|
|
75
|
+
if (url.includes('notion.so') || url.includes('notion.site')) {
|
|
76
|
+
return 'notion';
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`无法从 URL 推断文档类型: ${url}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sanitizeFileName(title: string): string {
|
|
82
|
+
return title
|
|
83
|
+
.replace(/[\/\\:*?"<>|]/g, '')
|
|
84
|
+
.replace(/\s+/g, '-')
|
|
85
|
+
.replace(/-+/g, '-')
|
|
86
|
+
.replace(/^-|-$/g, '');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function parseDocInput(input: DocInput): { url: string; name?: string } {
|
|
90
|
+
if (typeof input === 'string') {
|
|
91
|
+
return { url: input };
|
|
92
|
+
}
|
|
93
|
+
return input;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isTypedFeature(value: FeatureValue): value is { prd?: DocInput[]; design?: DocInput[]; api?: DocInput[]; test?: DocInput[] } {
|
|
97
|
+
return !Array.isArray(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeConfig(config: GroworkConfigV2, options: SyncOptions = {}): NormalizedDoc[] {
|
|
101
|
+
const docs: NormalizedDoc[] = [];
|
|
102
|
+
const outputDir = config.outputDir || 'docs';
|
|
103
|
+
|
|
104
|
+
if (config.custom && (options.custom || (!options.version && !options.feature))) {
|
|
105
|
+
for (const input of config.custom) {
|
|
106
|
+
const { url, name } = parseDocInput(input);
|
|
107
|
+
docs.push({
|
|
108
|
+
url,
|
|
109
|
+
name,
|
|
110
|
+
type: inferDocType(url),
|
|
111
|
+
outputPath: `${outputDir}/custom/{title}.md`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (options.custom) {
|
|
117
|
+
return docs;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (config.versions) {
|
|
121
|
+
for (const [version, features] of Object.entries(config.versions)) {
|
|
122
|
+
if (options.version && options.version !== version) continue;
|
|
123
|
+
|
|
124
|
+
for (const [feature, value] of Object.entries(features)) {
|
|
125
|
+
if (options.feature && options.feature !== feature) continue;
|
|
126
|
+
|
|
127
|
+
if (isTypedFeature(value)) {
|
|
128
|
+
for (const docType of ['prd', 'design', 'api', 'test'] as const) {
|
|
129
|
+
const docInputs = value[docType];
|
|
130
|
+
if (!docInputs) continue;
|
|
131
|
+
|
|
132
|
+
for (const input of docInputs) {
|
|
133
|
+
const { url, name } = parseDocInput(input);
|
|
134
|
+
docs.push({
|
|
135
|
+
url,
|
|
136
|
+
name,
|
|
137
|
+
type: inferDocType(url),
|
|
138
|
+
outputPath: `${outputDir}/${version}/${feature}/${docType}/{title}.md`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
for (const input of value) {
|
|
144
|
+
const { url, name } = parseDocInput(input);
|
|
145
|
+
docs.push({
|
|
146
|
+
url,
|
|
147
|
+
name,
|
|
148
|
+
type: inferDocType(url),
|
|
149
|
+
outputPath: `${outputDir}/${version}/${feature}/{title}.md`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return docs;
|
|
158
|
+
}
|
|
159
|
+
|
|
38
160
|
describe('配置文件解析', () => {
|
|
39
161
|
it('应正确解析有效的 YAML 配置', () => {
|
|
40
162
|
const yamlContent = `
|
|
@@ -184,3 +306,250 @@ describe('配置文件验证', () => {
|
|
|
184
306
|
);
|
|
185
307
|
});
|
|
186
308
|
});
|
|
309
|
+
|
|
310
|
+
// ============ V2.0 配置测试 ============
|
|
311
|
+
|
|
312
|
+
describe('文档类型推断', () => {
|
|
313
|
+
it('应识别飞书 feishu.cn URL', () => {
|
|
314
|
+
assert.strictEqual(inferDocType('https://xxx.feishu.cn/docx/abc123'), 'feishu');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('应识别飞书 larksuite.com URL', () => {
|
|
318
|
+
assert.strictEqual(inferDocType('https://xxx.larksuite.com/docx/abc123'), 'feishu');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('应识别 Notion notion.so URL', () => {
|
|
322
|
+
assert.strictEqual(inferDocType('https://www.notion.so/abc123'), 'notion');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('应识别 Notion notion.site URL', () => {
|
|
326
|
+
assert.strictEqual(inferDocType('https://myworkspace.notion.site/abc123'), 'notion');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('应拒绝未知域名', () => {
|
|
330
|
+
assert.throws(
|
|
331
|
+
() => inferDocType('https://example.com/doc/123'),
|
|
332
|
+
/无法从 URL 推断文档类型/
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
describe('文件名清理', () => {
|
|
338
|
+
it('应移除文件系统保留字符', () => {
|
|
339
|
+
assert.strictEqual(sanitizeFileName('test/file:name'), 'testfilename');
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('应将空格替换为连字符', () => {
|
|
343
|
+
assert.strictEqual(sanitizeFileName('hello world'), 'hello-world');
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('应合并连续空格为单个连字符', () => {
|
|
347
|
+
assert.strictEqual(sanitizeFileName('hello world'), 'hello-world');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('应移除首尾连字符', () => {
|
|
351
|
+
assert.strictEqual(sanitizeFileName(' hello world '), 'hello-world');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('应处理复杂标题', () => {
|
|
355
|
+
assert.strictEqual(sanitizeFileName('PRD: 用户登录 v1.0'), 'PRD-用户登录-v1.0');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('应处理只有特殊字符的情况', () => {
|
|
359
|
+
assert.strictEqual(sanitizeFileName('***'), '');
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('文档输入解析', () => {
|
|
364
|
+
it('应解析字符串输入', () => {
|
|
365
|
+
const result = parseDocInput('https://xxx.feishu.cn/docx/abc');
|
|
366
|
+
assert.deepStrictEqual(result, { url: 'https://xxx.feishu.cn/docx/abc' });
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('应解析对象输入(只有 url)', () => {
|
|
370
|
+
const result = parseDocInput({ url: 'https://xxx.feishu.cn/docx/abc' });
|
|
371
|
+
assert.deepStrictEqual(result, { url: 'https://xxx.feishu.cn/docx/abc' });
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('应解析对象输入(带 name)', () => {
|
|
375
|
+
const result = parseDocInput({ url: 'https://xxx.feishu.cn/docx/abc', name: '产品文档' });
|
|
376
|
+
assert.deepStrictEqual(result, { url: 'https://xxx.feishu.cn/docx/abc', name: '产品文档' });
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe('V2.0 配置规范化', () => {
|
|
381
|
+
it('应处理 custom 文档', () => {
|
|
382
|
+
const config: GroworkConfigV2 = {
|
|
383
|
+
custom: ['https://xxx.feishu.cn/docx/abc123']
|
|
384
|
+
};
|
|
385
|
+
const docs = normalizeConfig(config);
|
|
386
|
+
|
|
387
|
+
assert.strictEqual(docs.length, 1);
|
|
388
|
+
assert.strictEqual(docs[0].url, 'https://xxx.feishu.cn/docx/abc123');
|
|
389
|
+
assert.strictEqual(docs[0].type, 'feishu');
|
|
390
|
+
assert.strictEqual(docs[0].outputPath, 'docs/custom/{title}.md');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('应处理带 name 的 custom 文档', () => {
|
|
394
|
+
const config: GroworkConfigV2 = {
|
|
395
|
+
custom: [{ url: 'https://www.notion.so/abc123', name: '技术架构' }]
|
|
396
|
+
};
|
|
397
|
+
const docs = normalizeConfig(config);
|
|
398
|
+
|
|
399
|
+
assert.strictEqual(docs.length, 1);
|
|
400
|
+
assert.strictEqual(docs[0].name, '技术架构');
|
|
401
|
+
assert.strictEqual(docs[0].type, 'notion');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('应处理 versions 中的简单 feature', () => {
|
|
405
|
+
const config: GroworkConfigV2 = {
|
|
406
|
+
versions: {
|
|
407
|
+
'v1.0': {
|
|
408
|
+
'用户登录': ['https://xxx.feishu.cn/docx/abc']
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
const docs = normalizeConfig(config);
|
|
413
|
+
|
|
414
|
+
assert.strictEqual(docs.length, 1);
|
|
415
|
+
assert.strictEqual(docs[0].outputPath, 'docs/v1.0/用户登录/{title}.md');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('应处理 versions 中的分类型 feature', () => {
|
|
419
|
+
const config: GroworkConfigV2 = {
|
|
420
|
+
versions: {
|
|
421
|
+
'v1.0': {
|
|
422
|
+
'用户登录': {
|
|
423
|
+
prd: ['https://xxx.feishu.cn/docx/prd'],
|
|
424
|
+
design: ['https://xxx.feishu.cn/docx/design'],
|
|
425
|
+
api: ['https://xxx.feishu.cn/docx/api']
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const docs = normalizeConfig(config);
|
|
431
|
+
|
|
432
|
+
assert.strictEqual(docs.length, 3);
|
|
433
|
+
assert.strictEqual(docs[0].outputPath, 'docs/v1.0/用户登录/prd/{title}.md');
|
|
434
|
+
assert.strictEqual(docs[1].outputPath, 'docs/v1.0/用户登录/design/{title}.md');
|
|
435
|
+
assert.strictEqual(docs[2].outputPath, 'docs/v1.0/用户登录/api/{title}.md');
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('应支持自定义 outputDir', () => {
|
|
439
|
+
const config: GroworkConfigV2 = {
|
|
440
|
+
outputDir: 'output',
|
|
441
|
+
custom: ['https://xxx.feishu.cn/docx/abc']
|
|
442
|
+
};
|
|
443
|
+
const docs = normalizeConfig(config);
|
|
444
|
+
|
|
445
|
+
assert.strictEqual(docs[0].outputPath, 'output/custom/{title}.md');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('应支持 version 过滤', () => {
|
|
449
|
+
const config: GroworkConfigV2 = {
|
|
450
|
+
versions: {
|
|
451
|
+
'v1.0': { 'feat1': ['https://xxx.feishu.cn/docx/1'] },
|
|
452
|
+
'v2.0': { 'feat2': ['https://xxx.feishu.cn/docx/2'] }
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
const docs = normalizeConfig(config, { version: 'v1.0' });
|
|
456
|
+
|
|
457
|
+
assert.strictEqual(docs.length, 1);
|
|
458
|
+
assert.ok(docs[0].outputPath.includes('v1.0'));
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('应支持 feature 过滤', () => {
|
|
462
|
+
const config: GroworkConfigV2 = {
|
|
463
|
+
versions: {
|
|
464
|
+
'v1.0': {
|
|
465
|
+
'登录': ['https://xxx.feishu.cn/docx/1'],
|
|
466
|
+
'注册': ['https://xxx.feishu.cn/docx/2']
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const docs = normalizeConfig(config, { feature: '登录' });
|
|
471
|
+
|
|
472
|
+
assert.strictEqual(docs.length, 1);
|
|
473
|
+
assert.ok(docs[0].outputPath.includes('登录'));
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('应支持 custom 选项(只获取 custom 文档)', () => {
|
|
477
|
+
const config: GroworkConfigV2 = {
|
|
478
|
+
custom: ['https://xxx.feishu.cn/docx/custom'],
|
|
479
|
+
versions: {
|
|
480
|
+
'v1.0': { 'feat': ['https://xxx.feishu.cn/docx/versioned'] }
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
const docs = normalizeConfig(config, { custom: true });
|
|
484
|
+
|
|
485
|
+
assert.strictEqual(docs.length, 1);
|
|
486
|
+
assert.ok(docs[0].outputPath.includes('custom'));
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('默认情况下应返回所有文档', () => {
|
|
490
|
+
const config: GroworkConfigV2 = {
|
|
491
|
+
custom: ['https://xxx.feishu.cn/docx/custom'],
|
|
492
|
+
versions: {
|
|
493
|
+
'v1.0': { 'feat': ['https://xxx.feishu.cn/docx/versioned'] }
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const docs = normalizeConfig(config);
|
|
497
|
+
|
|
498
|
+
assert.strictEqual(docs.length, 2);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('应正确处理多版本多 feature', () => {
|
|
502
|
+
const config: GroworkConfigV2 = {
|
|
503
|
+
versions: {
|
|
504
|
+
'v1.0': {
|
|
505
|
+
'feat1': ['https://xxx.feishu.cn/docx/1'],
|
|
506
|
+
'feat2': ['https://xxx.feishu.cn/docx/2']
|
|
507
|
+
},
|
|
508
|
+
'v2.0': {
|
|
509
|
+
'feat3': ['https://xxx.feishu.cn/docx/3']
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
const docs = normalizeConfig(config);
|
|
514
|
+
|
|
515
|
+
assert.strictEqual(docs.length, 3);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
describe('V2.0 YAML 配置解析', () => {
|
|
520
|
+
it('应正确解析 V2.0 配置', () => {
|
|
521
|
+
const yamlContent = `
|
|
522
|
+
feishu:
|
|
523
|
+
appId: "cli_test"
|
|
524
|
+
appSecret: "secret_test"
|
|
525
|
+
domain: "feishu"
|
|
526
|
+
|
|
527
|
+
outputDir: "docs"
|
|
528
|
+
|
|
529
|
+
custom:
|
|
530
|
+
- "https://xxx.feishu.cn/docx/custom1"
|
|
531
|
+
- url: "https://xxx.feishu.cn/docx/custom2"
|
|
532
|
+
name: "技术架构"
|
|
533
|
+
|
|
534
|
+
versions:
|
|
535
|
+
v1.0:
|
|
536
|
+
用户登录:
|
|
537
|
+
prd:
|
|
538
|
+
- "https://xxx.feishu.cn/docx/prd"
|
|
539
|
+
api:
|
|
540
|
+
- "https://xxx.feishu.cn/docx/api"
|
|
541
|
+
小优化:
|
|
542
|
+
- "https://xxx.feishu.cn/docx/opt"
|
|
543
|
+
`;
|
|
544
|
+
const config = yaml.parse(yamlContent) as GroworkConfigV2;
|
|
545
|
+
|
|
546
|
+
assert.strictEqual(config.feishu?.appId, 'cli_test');
|
|
547
|
+
assert.strictEqual(config.feishu?.domain, 'feishu');
|
|
548
|
+
assert.strictEqual(config.outputDir, 'docs');
|
|
549
|
+
assert.strictEqual(config.custom?.length, 2);
|
|
550
|
+
assert.ok(config.versions?.['v1.0']);
|
|
551
|
+
|
|
552
|
+
const docs = normalizeConfig(config);
|
|
553
|
+
assert.strictEqual(docs.length, 5); // 2 custom + 2 分类 + 1 简单
|
|
554
|
+
});
|
|
555
|
+
});
|