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/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('1.0.0');
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 [name]')
22
- .description('同步文档,可选指定文档名称')
23
- .action(syncCommand);
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')
@@ -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'; // 默认 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 GroworkConfig {
23
+ export interface GroworkConfigV2 {
23
24
  feishu?: FeishuConfig;
24
25
  notion?: NotionConfig;
25
- docs: DocConfig[];
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 loadConfig(): GroworkConfig {
39
- const configPath = getConfigPath();
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
- if (!fs.existsSync(configPath)) {
42
- throw new Error(`配置文件不存在: ${configPath}\n请先运行 growork init 初始化配置`);
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
- const content = fs.readFileSync(configPath, 'utf-8');
46
- const config = yaml.parse(content) as GroworkConfig;
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 (!config.docs || config.docs.length === 0) {
50
- throw new Error('配置文件中没有配置任何文档');
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
- const hasFeishuDocs = config.docs.some(d => d.type === 'feishu');
55
- const hasNotionDocs = config.docs.some(d => d.type === 'notion');
151
+ return docs;
152
+ }
153
+
154
+ export function loadConfigV2(): GroworkConfigV2 {
155
+ const configPath = getConfigPath();
56
156
 
57
- if (hasFeishuDocs && (!config.feishu?.appId || !config.feishu?.appSecret)) {
58
- throw new Error('配置文件缺少飞书凭证 (feishu.appId, feishu.appSecret)');
157
+ if (!fs.existsSync(configPath)) {
158
+ throw new Error(`配置文件不存在: ${configPath}\n请先运行 growork init 初始化配置`);
59
159
  }
60
160
 
61
- if (hasNotionDocs && !config.notion?.token) {
62
- throw new Error('配置文件缺少 Notion 凭证 (notion.token)');
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
- - name: prd
85
- type: feishu
86
- url: "https://xxx.feishu.cn/docx/xxxxx"
87
- output: "docs/product/prd.md"
88
-
89
- # Notion 文档示例
90
- - name: notion-prd
91
- type: notion
92
- url: "https://www.notion.so/xxxxx"
93
- output: "docs/product/notion-prd.md"
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
  }
@@ -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
- // 直接测试配置解析逻辑,不依赖 process.cwd()
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
+ });