growork 1.0.1 → 1.1.2

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/index.js CHANGED
@@ -19,7 +19,83 @@ function getConfigPath() {
19
19
  function configExists() {
20
20
  return fs.existsSync(getConfigPath());
21
21
  }
22
- function loadConfig() {
22
+ function inferDocType(url) {
23
+ if (url.includes("feishu.cn") || url.includes("larksuite.com")) {
24
+ return "feishu";
25
+ }
26
+ if (url.includes("notion.so") || url.includes("notion.site")) {
27
+ return "notion";
28
+ }
29
+ if (url.includes("figma.com")) {
30
+ return "figma";
31
+ }
32
+ throw new Error(`\u65E0\u6CD5\u4ECE URL \u63A8\u65AD\u6587\u6863\u7C7B\u578B: ${url}`);
33
+ }
34
+ function sanitizeFileName(title) {
35
+ return title.replace(/[\/\\:*?"<>|]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
36
+ }
37
+ function parseDocInput(input) {
38
+ if (typeof input === "string") {
39
+ return { url: input };
40
+ }
41
+ return input;
42
+ }
43
+ function isTypedFeature(value) {
44
+ return !Array.isArray(value);
45
+ }
46
+ function normalizeConfig(config, options = {}) {
47
+ const docs = [];
48
+ const outputDir = config.outputDir || "docs";
49
+ if (config.custom && (options.custom || !options.version && !options.feature)) {
50
+ for (const input of config.custom) {
51
+ const { url, name } = parseDocInput(input);
52
+ docs.push({
53
+ url,
54
+ name,
55
+ type: inferDocType(url),
56
+ outputPath: `${outputDir}/custom/{title}.md`
57
+ });
58
+ }
59
+ }
60
+ if (options.custom) {
61
+ return docs;
62
+ }
63
+ if (config.versions) {
64
+ for (const [version, features] of Object.entries(config.versions)) {
65
+ if (options.version && options.version !== version) continue;
66
+ for (const [feature, value] of Object.entries(features)) {
67
+ if (options.feature && options.feature !== feature) continue;
68
+ if (isTypedFeature(value)) {
69
+ for (const docType of ["prd", "design", "api", "test"]) {
70
+ const docInputs = value[docType];
71
+ if (!docInputs) continue;
72
+ for (const input of docInputs) {
73
+ const { url, name } = parseDocInput(input);
74
+ docs.push({
75
+ url,
76
+ name,
77
+ type: inferDocType(url),
78
+ outputPath: `${outputDir}/${version}/${feature}/${docType}/{title}.md`
79
+ });
80
+ }
81
+ }
82
+ } else {
83
+ for (const input of value) {
84
+ const { url, name } = parseDocInput(input);
85
+ docs.push({
86
+ url,
87
+ name,
88
+ type: inferDocType(url),
89
+ outputPath: `${outputDir}/${version}/${feature}/{title}.md`
90
+ });
91
+ }
92
+ }
93
+ }
94
+ }
95
+ }
96
+ return docs;
97
+ }
98
+ function loadConfigV2() {
23
99
  const configPath = getConfigPath();
24
100
  if (!fs.existsSync(configPath)) {
25
101
  throw new Error(`\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728: ${configPath}
@@ -27,21 +103,13 @@ function loadConfig() {
27
103
  }
28
104
  const content = fs.readFileSync(configPath, "utf-8");
29
105
  const config = yaml.parse(content);
30
- if (!config.docs || config.docs.length === 0) {
31
- throw new Error("\u914D\u7F6E\u6587\u4EF6\u4E2D\u6CA1\u6709\u914D\u7F6E\u4EFB\u4F55\u6587\u6863");
32
- }
33
- const hasFeishuDocs = config.docs.some((d) => d.type === "feishu");
34
- const hasNotionDocs = config.docs.some((d) => d.type === "notion");
35
- if (hasFeishuDocs && (!config.feishu?.appId || !config.feishu?.appSecret)) {
36
- throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11\u98DE\u4E66\u51ED\u8BC1 (feishu.appId, feishu.appSecret)");
37
- }
38
- if (hasNotionDocs && !config.notion?.token) {
39
- throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11 Notion \u51ED\u8BC1 (notion.token)");
106
+ if (!config.custom && !config.versions) {
107
+ throw new Error("\u914D\u7F6E\u6587\u4EF6\u4E2D\u6CA1\u6709\u914D\u7F6E\u4EFB\u4F55\u6587\u6863\uFF08custom \u6216 versions\uFF09");
40
108
  }
41
109
  return config;
42
110
  }
43
111
  function getDefaultConfig() {
44
- return `# Growork \u914D\u7F6E\u6587\u4EF6
112
+ return `# Growork v2.0 \u914D\u7F6E\u6587\u4EF6
45
113
 
46
114
  # \u98DE\u4E66\u5E94\u7528\u51ED\u8BC1 (\u4F7F\u7528\u98DE\u4E66\u6587\u6863\u65F6\u9700\u8981)
47
115
  feishu:
@@ -53,29 +121,37 @@ feishu:
53
121
  notion:
54
122
  token: "ntn_xxxx" # Notion Integration Token
55
123
 
56
- # \u6587\u6863\u540C\u6B65\u914D\u7F6E
57
- docs:
58
- # \u98DE\u4E66\u6587\u6863\u793A\u4F8B
59
- - name: prd
60
- type: feishu
61
- url: "https://xxx.feishu.cn/docx/xxxxx"
62
- output: "docs/product/prd.md"
124
+ # \u8F93\u51FA\u6839\u76EE\u5F55\uFF08\u9ED8\u8BA4 "docs"\uFF09
125
+ outputDir: "docs"
126
+
127
+ # \u5168\u5C40\u6587\u6863\uFF08\u4E0D\u8DDF\u7248\u672C\uFF09
128
+ custom:
129
+ - "https://xxx.feishu.cn/docx/xxxxx" # \u6700\u7B80\u5199\u6CD5
130
+ # - url: "https://www.notion.so/xxxxx"
131
+ # name: "\u6280\u672F\u67B6\u6784" # \u53EF\u9009\uFF1A\u81EA\u5B9A\u4E49\u540D\u79F0
132
+
133
+ # \u7248\u672C\u5316\u6587\u6863
134
+ versions:
135
+ v1.0:
136
+ \u7528\u6237\u767B\u5F55:
137
+ prd:
138
+ - "https://xxx.feishu.cn/docx/xxxxx"
139
+ # design:
140
+ # - "https://xxx.feishu.cn/docx/yyyyy"
141
+ # api:
142
+ # - "https://xxx.feishu.cn/docx/zzzzz"
143
+ # test:
144
+ # - "https://xxx.feishu.cn/docx/aaaaa"
63
145
 
64
- # Notion \u6587\u6863\u793A\u4F8B
65
- - name: notion-prd
66
- type: notion
67
- url: "https://www.notion.so/xxxxx"
68
- output: "docs/product/notion-prd.md"
146
+ # \u7B80\u5355 feature \u53EF\u4E0D\u5206\u7C7B
147
+ # \u5C0F\u4F18\u5316:
148
+ # - "https://xxx.feishu.cn/docx/bbbbb"
69
149
  `;
70
150
  }
71
151
 
72
152
  // src/commands/init.ts
73
153
  var DIRS_TO_CREATE = [
74
- "docs/product",
75
- "docs/design",
76
- "docs/api",
77
- "docs/tech",
78
- "docs/test"
154
+ "docs/custom"
79
155
  ];
80
156
  async function initCommand() {
81
157
  console.log(chalk.blue("\u{1F4C1} \u521D\u59CB\u5316 Growork \u9879\u76EE...\n"));
@@ -593,89 +669,153 @@ function clearLine() {
593
669
  console.log("");
594
670
  }
595
671
  }
596
- async function syncCommand(docName) {
597
- const config = loadConfig();
672
+ function extractTitleFromMarkdown(content) {
673
+ const match = content.match(/^#\s+(.+)$/m);
674
+ return match ? match[1].trim() : "\u672A\u547D\u540D\u6587\u6863";
675
+ }
676
+ async function syncCommand(options = {}) {
677
+ const config = loadConfigV2();
678
+ const docs = normalizeConfig(config, options);
679
+ if (docs.length === 0) {
680
+ console.log(chalk2.yellow("\u26A0\uFE0F \u6CA1\u6709\u627E\u5230\u5339\u914D\u7684\u6587\u6863"));
681
+ return;
682
+ }
683
+ const normalDocs = docs.filter((d) => d.type !== "figma");
684
+ const figmaDocs = docs.filter((d) => d.type === "figma");
598
685
  const feishuService = config.feishu ? new FeishuService(config.feishu) : null;
599
686
  const notionService = config.notion ? new NotionService(config.notion) : null;
600
- let docsToSync = config.docs;
601
- if (docName) {
602
- const doc = config.docs.find((d) => d.name === docName);
603
- if (!doc) {
604
- console.log(chalk2.red(`\u274C \u672A\u627E\u5230\u540D\u4E3A "${docName}" \u7684\u6587\u6863\u914D\u7F6E`));
605
- console.log(chalk2.gray(`\u53EF\u7528\u7684\u6587\u6863: ${config.docs.map((d) => d.name).join(", ")}`));
606
- process.exit(1);
607
- }
608
- docsToSync = [doc];
687
+ const hasFeishuDocs = normalDocs.some((d) => d.type === "feishu");
688
+ const hasNotionDocs = normalDocs.some((d) => d.type === "notion");
689
+ if (hasFeishuDocs && !feishuService) {
690
+ throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11\u98DE\u4E66\u51ED\u8BC1 (feishu.appId, feishu.appSecret)");
691
+ }
692
+ if (hasNotionDocs && !notionService) {
693
+ throw new Error("\u914D\u7F6E\u6587\u4EF6\u7F3A\u5C11 Notion \u51ED\u8BC1 (notion.token)");
609
694
  }
610
695
  console.log(chalk2.blue("\u{1F4C4} \u5F00\u59CB\u540C\u6B65\u6587\u6863...\n"));
611
696
  let successCount = 0;
612
- for (const doc of docsToSync) {
613
- process.stdout.write(chalk2.gray(` \u23F3 ${doc.name.padEnd(15)} \u2192 ${doc.output}`));
697
+ const figmaGroups = /* @__PURE__ */ new Map();
698
+ for (const doc of figmaDocs) {
699
+ const dir = path3.dirname(doc.outputPath);
700
+ const feature = path3.basename(path3.dirname(dir));
701
+ if (!figmaGroups.has(dir)) {
702
+ figmaGroups.set(dir, { feature, urls: [] });
703
+ }
704
+ figmaGroups.get(dir).urls.push(doc.url);
705
+ }
706
+ for (const doc of normalDocs) {
707
+ const displayName = doc.name || doc.url.slice(-20);
708
+ process.stdout.write(chalk2.gray(` \u23F3 ${displayName}`));
614
709
  try {
615
710
  let content;
616
711
  if (doc.type === "feishu") {
617
- if (!feishuService) throw new Error("\u98DE\u4E66\u670D\u52A1\u672A\u914D\u7F6E");
618
712
  content = await feishuService.getDocumentAsMarkdown(doc.url);
619
- } else if (doc.type === "notion") {
620
- if (!notionService) throw new Error("Notion \u670D\u52A1\u672A\u914D\u7F6E");
621
- content = await notionService.getPageAsMarkdown(doc.url);
622
713
  } else {
623
- throw new Error(`\u4E0D\u652F\u6301\u7684\u6587\u6863\u7C7B\u578B: ${doc.type}`);
714
+ content = await notionService.getPageAsMarkdown(doc.url);
624
715
  }
625
- const outputPath = path3.join(process.cwd(), doc.output);
716
+ const title = doc.name || extractTitleFromMarkdown(content);
717
+ const safeTitle = sanitizeFileName(title);
718
+ const outputPath = path3.join(process.cwd(), doc.outputPath.replace("{title}", safeTitle));
626
719
  const outputDir = path3.dirname(outputPath);
627
720
  if (!fs3.existsSync(outputDir)) {
628
721
  fs3.mkdirSync(outputDir, { recursive: true });
629
722
  }
630
723
  fs3.writeFileSync(outputPath, content, "utf-8");
631
724
  clearLine();
632
- console.log(chalk2.green(` \u2713 ${doc.name.padEnd(15)} \u2192 ${doc.output}`));
725
+ console.log(chalk2.green(` \u2713 ${safeTitle.padEnd(25)} \u2192 ${path3.relative(process.cwd(), outputPath)}`));
633
726
  successCount++;
634
727
  } catch (error) {
635
728
  clearLine();
636
729
  const errorMessage = error instanceof Error ? error.message : String(error);
637
- console.log(chalk2.red(` \u2717 ${doc.name.padEnd(15)} \u2192 ${errorMessage}`));
730
+ console.log(chalk2.red(` \u2717 ${displayName.padEnd(25)} \u2192 ${errorMessage}`));
638
731
  }
639
732
  }
733
+ for (const [dir, { feature, urls }] of figmaGroups) {
734
+ const content = `# ${feature} \u8BBE\u8BA1\u7A3F
735
+
736
+ ${urls.map((u) => `- ${u}`).join("\n")}
737
+ `;
738
+ const outputPath = path3.join(process.cwd(), dir, "design.md");
739
+ fs3.mkdirSync(path3.dirname(outputPath), { recursive: true });
740
+ fs3.writeFileSync(outputPath, content, "utf-8");
741
+ console.log(chalk2.green(` \u2713 design.md \u2192 ${path3.relative(process.cwd(), outputPath)}`));
742
+ successCount++;
743
+ }
640
744
  console.log("");
641
- if (successCount === docsToSync.length) {
642
- console.log(chalk2.green(`\u2705 \u540C\u6B65\u5B8C\u6210\uFF0C\u5171 ${docsToSync.length} \u4E2A\u6587\u6863`));
745
+ const totalCount = normalDocs.length + figmaGroups.size;
746
+ if (successCount === totalCount) {
747
+ console.log(chalk2.green(`\u2705 \u540C\u6B65\u5B8C\u6210\uFF0C\u5171 ${totalCount} \u4E2A\u6587\u6863`));
643
748
  } else {
644
- console.log(chalk2.yellow(`\u26A0\uFE0F \u540C\u6B65\u5B8C\u6210: ${successCount}/${docsToSync.length} \u4E2A\u6587\u6863\u6210\u529F`));
749
+ console.log(chalk2.yellow(`\u26A0\uFE0F \u540C\u6B65\u5B8C\u6210: ${successCount}/${totalCount} \u4E2A\u6587\u6863\u6210\u529F`));
645
750
  }
646
751
  }
647
752
 
648
753
  // src/commands/list.ts
649
- import * as fs4 from "fs";
650
- import * as path4 from "path";
651
754
  import chalk3 from "chalk";
755
+ function shortenUrl(url, maxLen = 40) {
756
+ if (url.length <= maxLen) return url;
757
+ return url.slice(0, maxLen - 3) + "...";
758
+ }
652
759
  async function listCommand() {
653
760
  if (!configExists()) {
654
761
  console.log(chalk3.red("\u274C \u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5148\u8FD0\u884C growork init"));
655
762
  process.exit(1);
656
763
  }
657
- const config = loadConfig();
658
- const cwd = process.cwd();
764
+ const config = loadConfigV2();
659
765
  console.log(chalk3.blue("\u{1F4CB} \u6587\u6863\u5217\u8868\n"));
660
- console.log(chalk3.gray(" \u540D\u79F0".padEnd(18) + "\u7C7B\u578B".padEnd(10) + "\u8F93\u51FA\u8DEF\u5F84".padEnd(30) + "\u72B6\u6001"));
661
- console.log(chalk3.gray(" " + "-".repeat(70)));
662
- for (const doc of config.docs) {
663
- const outputPath = path4.join(cwd, doc.output);
664
- const exists = fs4.existsSync(outputPath);
665
- const status = exists ? chalk3.green("\u5DF2\u540C\u6B65") : chalk3.yellow("\u672A\u540C\u6B65");
666
- const name = ` ${doc.name}`.padEnd(18);
667
- const type = doc.type.padEnd(10);
668
- const output = doc.output.padEnd(30);
669
- console.log(`${name}${chalk3.gray(type)}${output}${status}`);
766
+ let totalCount = 0;
767
+ if (config.custom && config.custom.length > 0) {
768
+ console.log(chalk3.cyan("\u{1F4C1} custom (\u5168\u5C40\u6587\u6863)"));
769
+ for (const input of config.custom) {
770
+ const { url, name } = parseDocInput(input);
771
+ const displayName = name || shortenUrl(url);
772
+ console.log(chalk3.gray(` - ${displayName}`));
773
+ totalCount++;
774
+ }
775
+ console.log("");
670
776
  }
671
- console.log("");
672
- console.log(chalk3.gray(`\u5171 ${config.docs.length} \u4E2A\u6587\u6863\u914D\u7F6E`));
777
+ if (config.versions) {
778
+ for (const [version, features] of Object.entries(config.versions)) {
779
+ console.log(chalk3.cyan(`\u{1F4C1} ${version}`));
780
+ for (const [feature, value] of Object.entries(features)) {
781
+ if (Array.isArray(value)) {
782
+ console.log(chalk3.white(` \u2514\u2500 ${feature}`));
783
+ for (const input of value) {
784
+ const { url, name } = parseDocInput(input);
785
+ const displayName = name || shortenUrl(url, 30);
786
+ console.log(chalk3.gray(` - ${displayName}`));
787
+ totalCount++;
788
+ }
789
+ } else {
790
+ console.log(chalk3.white(` \u2514\u2500 ${feature}`));
791
+ for (const docType of ["prd", "design", "api", "test"]) {
792
+ const docInputs = value[docType];
793
+ if (!docInputs || docInputs.length === 0) continue;
794
+ console.log(chalk3.yellow(` ${docType}:`));
795
+ for (const input of docInputs) {
796
+ const { url, name } = parseDocInput(input);
797
+ const displayName = name || shortenUrl(url, 25);
798
+ console.log(chalk3.gray(` - ${displayName}`));
799
+ totalCount++;
800
+ }
801
+ }
802
+ }
803
+ }
804
+ console.log("");
805
+ }
806
+ }
807
+ console.log(chalk3.gray(`\u5171 ${totalCount} \u4E2A\u6587\u6863\u914D\u7F6E`));
673
808
  }
674
809
 
675
810
  // src/index.ts
676
811
  var program = new Command();
677
- program.name("growork").description("\u5C06\u98DE\u4E66\u6587\u6863\u540C\u6B65\u5230\u672C\u5730\uFF0C\u4E3A AI Agent \u63D0\u4F9B\u5B8C\u6574\u4E0A\u4E0B\u6587").version("1.0.0");
812
+ program.name("growork").description("\u5C06\u98DE\u4E66\u6587\u6863\u540C\u6B65\u5230\u672C\u5730\uFF0C\u4E3A AI Agent \u63D0\u4F9B\u5B8C\u6574\u4E0A\u4E0B\u6587").version("2.0.0");
678
813
  program.command("init").description("\u521D\u59CB\u5316 Growork \u914D\u7F6E\u548C\u76EE\u5F55\u7ED3\u6784").action(initCommand);
679
- program.command("sync [name]").description("\u540C\u6B65\u6587\u6863\uFF0C\u53EF\u9009\u6307\u5B9A\u6587\u6863\u540D\u79F0").action(syncCommand);
814
+ program.command("sync").description("\u540C\u6B65\u6587\u6863").option("--ver <version>", "\u53EA\u540C\u6B65\u6307\u5B9A\u7248\u672C").option("-f, --feature <feature>", "\u53EA\u540C\u6B65\u6307\u5B9A feature").option("-c, --custom", "\u53EA\u540C\u6B65\u5168\u5C40\u6587\u6863").action((options) => {
815
+ if (options.ver) {
816
+ options.version = options.ver;
817
+ }
818
+ syncCommand(options);
819
+ });
680
820
  program.command("list").description("\u5217\u51FA\u6240\u6709\u914D\u7F6E\u7684\u6587\u6863").action(listCommand);
681
821
  program.parse();
@@ -0,0 +1,175 @@
1
+ # GroWork 技术架构
2
+
3
+ ## 概述
4
+
5
+ GroWork 是一个文档同步 CLI 工具,将飞书文档、Notion 页面和 Figma 设计稿同步到本地 Markdown 文件。
6
+
7
+ ## 项目结构
8
+
9
+ ```
10
+ src/
11
+ ├── index.ts # CLI 入口
12
+ ├── commands/
13
+ │ ├── init.ts # growork init
14
+ │ ├── sync.ts # growork sync
15
+ │ └── list.ts # growork list
16
+ ├── services/
17
+ │ ├── feishu.ts # 飞书 API 服务
18
+ │ └── notion.ts # Notion API 服务
19
+ └── utils/
20
+ └── config.ts # 配置解析
21
+ ```
22
+
23
+ ## 核心模块
24
+
25
+ ### 1. CLI 入口 (`src/index.ts`)
26
+
27
+ 使用 `commander` 定义命令:
28
+
29
+ - `growork init` - 初始化配置文件
30
+ - `growork sync` - 同步文档,支持 `--ver`、`-f`、`-c` 过滤
31
+ - `growork list` - 列出所有文档
32
+
33
+ ### 2. 配置模块 (`src/utils/config.ts`)
34
+
35
+ **核心类型:**
36
+
37
+ ```typescript
38
+ // 文档输入格式
39
+ type DocInput = string | { url: string; name?: string };
40
+
41
+ // 标准化后的文档
42
+ interface NormalizedDoc {
43
+ url: string;
44
+ name?: string;
45
+ type: 'feishu' | 'notion' | 'figma';
46
+ outputPath: string; // 含 {title} 占位符
47
+ }
48
+ ```
49
+
50
+ **核心函数:**
51
+
52
+ | 函数 | 作用 |
53
+ |-----|------|
54
+ | `loadConfigV2()` | 加载并解析 YAML 配置文件 |
55
+ | `normalizeConfig()` | 将配置转为标准化的 `NormalizedDoc[]` |
56
+ | `inferDocType()` | 根据 URL 推断文档类型 |
57
+ | `sanitizeFileName()` | 处理文件名特殊字符 |
58
+
59
+ ### 3. 同步命令 (`src/commands/sync.ts`)
60
+
61
+ **处理流程:**
62
+
63
+ ```
64
+ loadConfigV2() → normalizeConfig() → 分组处理
65
+ ├── 飞书文档 → FeishuService.getDocumentAsMarkdown()
66
+ ├── Notion 页面 → NotionService.getPageAsMarkdown()
67
+ └── Figma 链接 → 直接生成 design.md
68
+ ```
69
+
70
+ **Figma 处理逻辑:**
71
+
72
+ 同一 feature 下的多个 Figma URL 会合并到一个 `design.md` 文件:
73
+
74
+ ```typescript
75
+ // 按目录分组
76
+ const figmaGroups = new Map<string, { feature: string; urls: string[] }>();
77
+
78
+ // 生成内容
79
+ const content = `# ${feature} 设计稿\n\n${urls.map(u => `- ${u}`).join('\n')}\n`;
80
+ ```
81
+
82
+ ### 4. 飞书服务 (`src/services/feishu.ts`)
83
+
84
+ **职责:** 调用飞书 API 获取文档内容,转换为 Markdown。
85
+
86
+ **核心方法:**
87
+
88
+ | 方法 | 作用 |
89
+ |-----|------|
90
+ | `getDocumentAsMarkdown(url)` | 主入口,返回完整 Markdown |
91
+ | `parseDocumentId(url)` | 解析 URL 中的文档 ID |
92
+ | `resolveDocumentId(url)` | Wiki 链接需要额外解析真实 ID |
93
+ | `getAllBlocks(docId)` | 分页获取所有 block |
94
+ | `blockToMarkdown(block)` | 将单个 block 转为 Markdown |
95
+
96
+ **支持的 block 类型:**
97
+
98
+ | blockType | 类型 | 输出 |
99
+ |-----------|------|------|
100
+ | 2 | Text | 普通文本 |
101
+ | 3-11 | Heading1-9 | `#` ~ `######` |
102
+ | 12 | Bullet | `- item` |
103
+ | 13 | Ordered | `1. item` |
104
+ | 14 | Code | ` ```lang ``` ` |
105
+ | 15 | Quote | `> text` |
106
+ | 17 | TodoList | `- [ ] task` |
107
+ | 18 | Divider | `---` |
108
+ | 19 | Image | `![image](token)` |
109
+ | 31 | Table | Markdown 表格 |
110
+
111
+ ### 5. Notion 服务 (`src/services/notion.ts`)
112
+
113
+ **职责:** 调用 Notion API 获取页面内容,使用 `notion-to-md` 库转换。
114
+
115
+ **核心方法:**
116
+
117
+ | 方法 | 作用 |
118
+ |-----|------|
119
+ | `getPageAsMarkdown(url)` | 主入口,返回完整 Markdown |
120
+ | `parsePageId(url)` | 从 URL 提取 page ID |
121
+ | `setupCustomTransformers()` | 自定义 child_database 转换 |
122
+
123
+ **数据库处理:**
124
+
125
+ 内嵌数据库(child_database)会自动转为 Markdown 表格,支持的字段类型:
126
+
127
+ - title, rich_text, select, multi_select
128
+ - number, checkbox, date, url, files
129
+
130
+ ## 数据流
131
+
132
+ ```
133
+ ┌─────────────────┐
134
+ │ growork.config │ YAML 配置文件
135
+ │ .yaml │
136
+ └────────┬────────┘
137
+ │ loadConfigV2()
138
+
139
+ ┌─────────────────┐
140
+ │ GroworkConfigV2 │ 原始配置对象
141
+ └────────┬────────┘
142
+ │ normalizeConfig(options)
143
+
144
+ ┌─────────────────┐
145
+ │ NormalizedDoc[] │ 标准化文档列表
146
+ └────────┬────────┘
147
+
148
+ ┌────┴────┬──────────┐
149
+ ▼ ▼ ▼
150
+ Feishu Notion Figma
151
+ │ │ │
152
+ ▼ ▼ ▼
153
+ ┌─────────────────────────────┐
154
+ │ Markdown 文件写入本地 │
155
+ └─────────────────────────────┘
156
+ ```
157
+
158
+ ## 技术栈
159
+
160
+ | 用途 | 依赖 |
161
+ |-----|------|
162
+ | CLI 框架 | commander |
163
+ | 飞书 API | @larksuiteoapi/node-sdk |
164
+ | Notion API | @notionhq/client |
165
+ | Notion 转 MD | notion-to-md |
166
+ | YAML 解析 | yaml |
167
+ | 终端颜色 | chalk |
168
+ | 构建工具 | tsup |
169
+
170
+ ## 设计原则
171
+
172
+ 1. **最少代码** - 不提前抽象,只实现当前需求
173
+ 2. **扁平结构** - 目录层级不超过 2 层
174
+ 3. **错误自然抛出** - 在顶层统一处理,不过度防御
175
+ 4. **依赖成熟库** - 不重复造轮子
@@ -0,0 +1,67 @@
1
+ # 开发者说
2
+
3
+ ## AI 时代的开发范式
4
+
5
+ GroWork 是一个 **零手写代码** 的项目。从第一行代码到现在,所有实现都由 AI 完成,人只负责写文档和提示词。
6
+
7
+ ## 核心洞察:后端最贴合 AI
8
+
9
+ 后端开发是目前最适合 AI 全流程接管的领域:
10
+
11
+ - **输入输出明确** - API 契约清晰,不涉及主观审美
12
+ - **逻辑可验证** - 单元测试可以客观判断对错
13
+ - **文档驱动** - PRD、API 文档天然适合作为 AI 的上下文
14
+
15
+ 相比之下,前端涉及 UI/UX 审美判断,AI 难以完全替代人的决策。
16
+
17
+ ## AI All-in-One 闭环
18
+
19
+ ```
20
+ PRD 文档 → 架构设计 → 测试用例 → 代码实现 → Code Review → 发布
21
+ ↑ │
22
+ └──────────────────── 迭代反馈 ←────────────────────────┘
23
+ ```
24
+
25
+ 每个环节都由 AI 执行,人只在关键节点做决策。
26
+
27
+ ## 多 Tab 工作流
28
+
29
+ 将 Claude Code 分为多个独立 Tab,各司其职:
30
+
31
+ | Tab | 职责 |
32
+ | ---------------- | ---------------------------------- |
33
+ | **doc** | 生成/更新文档(PRD、架构、README) |
34
+ | **code** | 编写实现代码 |
35
+ | **test** | 编写和运行测试用例 |
36
+ | **review** | 代码审查,检查规范和潜在问题 |
37
+ | **git** | 提交、推送、PR 管理 |
38
+
39
+ 这种分离让每个 Tab 保持专注的上下文,避免单一会话过载。
40
+
41
+ ## 人的角色转变
42
+
43
+ 在这个模式下,开发者的工作变成了:
44
+
45
+ 1. **写文档** - PRD 描述需求,架构文档描述设计
46
+ 2. **写提示词** - 精准表达意图,引导 AI 输出
47
+ 3. **做决策** - 在多个方案中选择,把控方向
48
+ 4. **验收成果** - 确认 AI 的输出符合预期
49
+
50
+ 代码不再是手写的,而是「生成」的。
51
+
52
+ ## 实践心得
53
+
54
+ 1. **文档质量决定代码质量** - 垃圾文档进,垃圾代码出
55
+ 2. **小步快跑** - 每次只让 AI 做一件小事,而不是一次性生成大量代码
56
+ 3. **测试先行** - 先让 AI 写测试用例,再写实现,AI 更容易做对
57
+ 4. **持续 Review** - AI 会犯错,定期审查避免技术债累积
58
+
59
+ ## 展望
60
+
61
+ 这只是开始。随着 AI 能力提升,未来的开发模式会更加极致:
62
+
63
+ - 需求直接生成可运行的代码
64
+ - 自动修复 bug,自动优化性能
65
+ - 人只需要描述「想要什么」,而不是「怎么做」
66
+
67
+ GroWork 本身就是这个未来的一个小小实验。
@@ -0,0 +1,86 @@
1
+ # GroWork 1.0.0 PRD
2
+
3
+ ## 概述
4
+
5
+ | 项目 | 内容 |
6
+ |-----|-----|
7
+ | 产品名称 | GroWork |
8
+ | 版本 | 1.0.0 |
9
+ | 产品形态 | CLI 命令行工具 |
10
+ | 技术栈 | Node.js / TypeScript |
11
+
12
+ ## 背景与目标
13
+
14
+ ### 背景
15
+
16
+ AI Agent 开发时代,PRD、设计稿、接口文档、技术方案分散在 Lark/Notion/Figma 等平台,无法在一个工程内闭环。
17
+
18
+ ### 目标
19
+
20
+ 实现产品开发全流程文档在一个文件夹下闭环,让 AI Agent 一次性获取完整上下文。
21
+
22
+ ## 用户场景
23
+
24
+ 使用 Claude Code 等 Agent 工具的开发者,希望同步远程文档到本地,让 AI 直接读取进行开发。
25
+
26
+ ## 功能需求
27
+
28
+ ### 功能列表
29
+
30
+ | 命令 | 说明 | 状态 |
31
+ |-----|-----|-----|
32
+ | `growork init` | 生成配置文件和目录结构 | ✅ |
33
+ | `growork sync` | 同步所有配置的文档 | ✅ |
34
+ | `growork sync <name>` | 同步指定文档 | ✅ |
35
+ | `growork list` | 列出所有文档及状态 | ✅ |
36
+
37
+ ### 支持的文档类型
38
+
39
+ | 类型 | 说明 |
40
+ |-----|-----|
41
+ | `feishu` | Lark 文档和知识库 |
42
+ | `notion` | Notion 页面(含内嵌数据库) |
43
+
44
+ ### 核心流程
45
+
46
+ ```
47
+ growork init → 编辑配置文件 → growork sync → AI 读取 docs/ 目录
48
+ ```
49
+
50
+ ## 技术设计
51
+
52
+ ### 技术选型
53
+
54
+ | 用途 | 选择 |
55
+ |-----|-----|
56
+ | CLI 框架 | Commander.js |
57
+ | Lark SDK | @larksuiteoapi/node-sdk |
58
+ | Notion SDK | @notionhq/client + notion-to-md |
59
+ | 配置解析 | yaml |
60
+ | 构建工具 | tsup |
61
+
62
+ ### 项目结构
63
+
64
+ ```
65
+ src/
66
+ ├── index.ts # CLI 入口
67
+ ├── commands/ # 命令实现
68
+ ├── services/ # Lark/Notion API
69
+ └── utils/ # 配置解析
70
+ ```
71
+
72
+ ## 验收标准
73
+
74
+ - [x] 能同步 Lark 文档和知识库到本地 Markdown
75
+ - [x] 能同步 Notion 页面到本地 Markdown
76
+ - [x] 支持批量同步和单个同步
77
+ - [x] 单文档同步 < 5 秒
78
+
79
+ ## 开放问题
80
+
81
+ - [ ] 是否支持增量同步?
82
+ - [ ] 凭证是否改用环境变量?
83
+
84
+ ## 后续版本
85
+
86
+ 1.1.0 已发布,引入版本和 feature 层级结构,详见 [prd-1.1.0.md](./prd-1.1.0.md)