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/.claude/commands/review.md +62 -0
- package/.claude/settings.local.json +2 -1
- package/README.md +108 -105
- package/claude.md +103 -2
- package/dist/index.js +211 -71
- package/docs/architecture.md +175 -0
- package/docs/developer-notes.md +67 -0
- package/docs/prd-1.0.0.md +86 -0
- package/docs/prd-1.1.1.md +215 -0
- package/docs/prd-1.1.2.md +136 -0
- package/docs/prd-template.md +65 -0
- package/growork.config.yaml +36 -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 +69 -26
- package/src/index.ts +13 -4
- package/src/utils/config.ts +159 -40
- package/tests/config.test.ts +383 -3
- package/tests/feishu.test.ts +187 -0
- package/tests/notion.test.ts +81 -0
- package/tests/sync.test.ts +65 -0
- 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/dist/index.js
CHANGED
|
@@ -19,7 +19,83 @@ function getConfigPath() {
|
|
|
19
19
|
function configExists() {
|
|
20
20
|
return fs.existsSync(getConfigPath());
|
|
21
21
|
}
|
|
22
|
-
function
|
|
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.
|
|
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
|
-
# \
|
|
57
|
-
docs
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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/
|
|
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
|
-
|
|
597
|
-
const
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
613
|
-
|
|
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
|
-
|
|
714
|
+
content = await notionService.getPageAsMarkdown(doc.url);
|
|
624
715
|
}
|
|
625
|
-
const
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
642
|
-
|
|
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}/${
|
|
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 =
|
|
658
|
-
const cwd = process.cwd();
|
|
764
|
+
const config = loadConfigV2();
|
|
659
765
|
console.log(chalk3.blue("\u{1F4CB} \u6587\u6863\u5217\u8868\n"));
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
console.log(
|
|
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
|
-
|
|
672
|
-
|
|
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("
|
|
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
|
|
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 | `` |
|
|
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)
|