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.
@@ -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)
@@ -0,0 +1,215 @@
1
+ # GroWork 1.1.0 PRD
2
+
3
+ ## 概述
4
+
5
+ | 项目 | 内容 |
6
+ | -------- | ----------------------------- |
7
+ | 产品名称 | GroWork |
8
+ | 版本 | 1.1.0 |
9
+ | 状态 | ✅ 已完成 |
10
+ | 产品形态 | CLI |
11
+ | 核心改动 | 引入版本和 feature 层级结构 |
12
+
13
+ ## 背景与目标
14
+
15
+ ### 背景
16
+
17
+ 当前 1.0.0 配置是扁平的 docs 列表,没有版本和 features 概念,不便于按功能模块组织文档。实际开发中,每个功能都是按 feature 闭环的,包含 PRD、设计、接口、测试等多种文档。
18
+
19
+ ### 目标
20
+
21
+ 1. 引入 **版本 → feature → 文档类型** 的层级结构
22
+ 2. 简化配置,最简情况只需写 URL
23
+ 3. 支持全局文档(不归属版本)
24
+
25
+ ## 用户场景
26
+
27
+ 使用 Claude Code 等 Agent 工具的开发者,希望同步远程文档到本地,让 AI 直接读取进行开发。文档按版本和功能模块组织,便于 AI 理解项目结构。
28
+
29
+ ## 功能需求
30
+
31
+ ### 文档类型定义
32
+
33
+ | 类型 | 说明 |
34
+ | -------- | -------------------------- |
35
+ | `prd` | 产品需求文档 |
36
+ | `design` | 设计稿/交互文档 |
37
+ | `api` | 接口/技术方案 |
38
+ | `test` | 测试用例 |
39
+ | `custom` | 全局文档,不归属任何版本 |
40
+
41
+ ### 配置文件格式
42
+
43
+ ```yaml
44
+ # growork.config.yaml
45
+ feishu:
46
+ appId: "cli_xxx"
47
+ appSecret: "xxx"
48
+ domain: "feishu" # feishu(中国版) 或 lark(国际版)
49
+
50
+ notion:
51
+ token: "ntn_xxx"
52
+
53
+ # 输出根目录(默认 "docs")
54
+ outputDir: "docs"
55
+
56
+ # 全局文档(不跟版本)
57
+ custom:
58
+ - https://feishu.cn/xxx # 最简写法,name 从文档标题获取
59
+ - url: https://notion.so/yyy
60
+ name: 技术架构 # 可选:自定义名称
61
+
62
+ # 版本化文档
63
+ versions:
64
+ v1.2:
65
+ 用户登录: # feature 名称
66
+ prd:
67
+ - https://feishu.cn/xxx
68
+ design:
69
+ - https://feishu.cn/yyy
70
+ api:
71
+ - https://feishu.cn/zzz
72
+ test:
73
+ - https://feishu.cn/aaa
74
+
75
+ # 简单 feature 可不分类
76
+ 小优化:
77
+ - https://feishu.cn/bbb
78
+ ```
79
+
80
+ ### 输出目录结构
81
+
82
+ ```
83
+ docs/
84
+ ├── custom/ # 全局文档
85
+ │ ├── 项目总览.md
86
+ │ └── 技术架构.md
87
+ └── v1.2/
88
+ ├── 用户登录/
89
+ │ ├── prd/
90
+ │ │ └── {文档标题}.md
91
+ │ ├── design/
92
+ │ │ └── {文档标题}.md
93
+ │ ├── api/
94
+ │ │ └── {文档标题}.md
95
+ │ └── test/
96
+ │ └── {文档标题}.md
97
+ └── 小优化/
98
+ └── {文档标题}.md
99
+ ```
100
+
101
+ ### 默认值规则
102
+
103
+ | 字段 | 默认值 |
104
+ | --------- | --------------------------------------------------- |
105
+ | outputDir | `docs` |
106
+ | name | 从文档标题自动获取,自动去除特殊字符 |
107
+ | output | `{outputDir}/{version}/{feature}/{type}/{name}.md` |
108
+ | type | 自动从 URL 推断(feishu.cn/larksuite.com → feishu,notion.so/notion.site → notion)|
109
+
110
+ ### 文件名处理
111
+
112
+ 文档标题作为文件名时,自动去除以下特殊字符:
113
+ - 文件系统保留字符:`/ \ : * ? " < > |`
114
+ - 空格替换为 `-`
115
+ - 连续的 `-` 合并为单个
116
+ - 首尾 `-` 去除
117
+
118
+ 示例:`用户登录 / 注册流程 (v1.0)` → `用户登录-注册流程-v1.0.md`
119
+
120
+ ### 命令列表
121
+
122
+ | 命令 | 说明 | 状态 |
123
+ |-----|-----|-----|
124
+ | `growork init` | 生成 1.1.0 配置文件模板 | ✅ |
125
+ | `growork sync` | 同步所有文档 | ✅ |
126
+ | `growork sync --ver <version>` | 只同步指定版本 | ✅ |
127
+ | `growork sync -f <feature>` | 只同步指定 feature | ✅ |
128
+ | `growork sync -c` | 只同步全局文档 | ✅ |
129
+ | `growork list` | 列出所有文档(按层级展示) | ✅ |
130
+ | `growork migrate` | 将 1.0.0 配置迁移到 1.1.0 | ✅ |
131
+
132
+ ### 同步命令参数
133
+
134
+ ```bash
135
+ growork sync # 同步全部
136
+ growork sync --ver v1.2 # 只同步指定版本(避免与 --version 冲突)
137
+ growork sync -f 用户登录 # 只同步指定 feature
138
+ growork sync -c # 只同步全局文档
139
+ ```
140
+
141
+ > 注:使用 `--ver` 而非 `--version` 是因为 commander 保留了 `--version` 用于显示版本号
142
+
143
+ ### 核心流程
144
+
145
+ 1. 用户编辑 `growork.config.yaml`
146
+ 2. 运行 `growork sync` 命令
147
+ 3. 工具解析配置,按层级同步文档
148
+ 4. 文档输出到对应目录,name 从远程文档标题获取
149
+
150
+ ## 技术设计
151
+
152
+ ### 配置解析
153
+
154
+ ```typescript
155
+ // 文档输入支持字符串(URL)或对象形式
156
+ type DocInput = string | { url: string; name?: string }
157
+
158
+ // feature 可以是数组(简单形式)或分类对象
159
+ type FeatureValue = DocInput[] | {
160
+ prd?: DocInput[]
161
+ design?: DocInput[]
162
+ api?: DocInput[]
163
+ test?: DocInput[]
164
+ }
165
+
166
+ interface GroworkConfigV2 {
167
+ feishu?: {
168
+ appId: string
169
+ appSecret: string
170
+ domain?: 'feishu' | 'lark'
171
+ }
172
+ notion?: { token: string }
173
+ outputDir?: string // 输出根目录,默认 "docs"
174
+ custom?: DocInput[]
175
+ versions?: {
176
+ [version: string]: {
177
+ [feature: string]: FeatureValue
178
+ }
179
+ }
180
+ }
181
+
182
+ // 同步选项
183
+ interface SyncOptions {
184
+ version?: string
185
+ feature?: string
186
+ custom?: boolean
187
+ }
188
+ ```
189
+
190
+ ### 兼容性
191
+
192
+ - 1.1.0 配置格式与 1.0.0 不兼容
193
+ - ✅ 提供 `growork migrate` 命令将 1.0.0 配置迁移到 1.1.0
194
+ - 迁移时自动备份原配置到 `growork.config.yaml.v1.backup`
195
+
196
+ ## 验收标准
197
+
198
+ - [x] 支持 custom 全局文档同步
199
+ - [x] 支持 versions 层级配置解析
200
+ - [x] 支持 prd/design/api/test 四种文档类型
201
+ - [x] 支持简写(只写 URL)和完整写法
202
+ - [x] name 默认从文档标题获取(从 Markdown 内容提取 H1 标题)
203
+ - [x] 输出目录按 `版本/feature/类型/` 结构
204
+ - [x] `--ver` 参数过滤指定版本
205
+ - [x] `-f/--feature` 参数过滤指定功能
206
+ - [x] `-c/--custom` 参数只同步全局文档
207
+ - [x] `growork migrate` 命令支持 1.0.0 到 1.1.0 迁移
208
+ - [x] `growork list` 命令按层级展示文档列表
209
+ - [x] 支持自定义输出根目录(outputDir 配置)
210
+
211
+ ## 开放问题
212
+
213
+ - [ ] 是否需要支持多个配置文件?
214
+ - [ ] 是否支持增量同步(仅同步有变更的文档)?
215
+ - [ ] 凭证是否改用环境变量?
@@ -0,0 +1,65 @@
1
+ # [产品名称] v[版本] PRD
2
+
3
+ ## 概述
4
+
5
+ | 项目 | 内容 |
6
+ |-----|-----|
7
+ | 产品名称 | xxx |
8
+ | 版本 | v1.0 |
9
+ | 产品形态 | CLI / Web / App |
10
+
11
+ ## 背景与目标
12
+
13
+ ### 背景
14
+
15
+ 为什么做?当前痛点是什么?
16
+
17
+ ### 目标
18
+
19
+ 做成什么样?成功标准是什么?
20
+
21
+ ## 用户场景
22
+
23
+ 谁在什么场景下用?解决什么问题?
24
+
25
+ ## 功能需求
26
+
27
+ ### 功能列表
28
+
29
+ | 功能 | 说明 | 优先级 |
30
+ |-----|-----|-------|
31
+ | 功能A | 做什么 | P0 |
32
+ | 功能B | 做什么 | P1 |
33
+
34
+ ### 核心流程
35
+
36
+ ```
37
+ 用户操作 → 系统处理 → 输出结果
38
+ ```
39
+
40
+ ## 技术设计
41
+
42
+ ### 技术选型
43
+
44
+ | 用途 | 选择 |
45
+ |-----|-----|
46
+ | 语言 | xxx |
47
+ | 框架 | xxx |
48
+
49
+ ### 项目结构
50
+
51
+ ```
52
+ project/
53
+ ├── src/
54
+ └── docs/
55
+ ```
56
+
57
+ ## 验收标准
58
+
59
+ - [ ] 场景1:预期结果
60
+ - [ ] 场景2:预期结果
61
+
62
+ ## 开放问题
63
+
64
+ - [ ] 待确认问题1
65
+ - [ ] 待确认问题2
@@ -1,43 +1,41 @@
1
- # Growork 配置文件
1
+ # Growork v2.0 配置文件(从 v1.0 迁移)
2
2
 
3
- # 飞书应用凭证
4
3
  feishu:
5
4
  appId: "cli_a9f679f7d8389ed1"
6
5
  appSecret: "M75JzaxTI8kAn50Ss1GgEbBVAIP551RX"
7
- domain: "lark" # 使用国际版 Lark
6
+ domain: "lark"
8
7
 
9
- # Notion 凭证
10
8
  notion:
11
9
  token: "ntn_B47151172375xyBGfoq9heNvlzfhvgSBQymC4zO7f997XO"
12
10
 
13
- # 文档同步配置
14
- docs:
15
- # 五牌阵需求文档 (飞书)
16
- - name: prd-5spread
17
- type: feishu
18
- url: "https://romangic.sg.larksuite.com/wiki/UrBKwmmJNizGlzkoFU3l90wxgOd"
19
- output: "test/prd-5spread.md"
11
+ # 输出根目录
12
+ outputDir: "test_export"
20
13
 
21
- # 后端AI文档 (飞书)
22
- - name: backend-ai
23
- type: feishu
24
- url: "https://romangic.sg.larksuite.com/wiki/ZiCuw4GnXiBsPnkN159lNE0mgLe"
25
- output: "test/backend-ai.md"
14
+ # docs 列表(可手动整理到 versions 中)
15
+ custom:
16
+ - url: "https://romangic.sg.larksuite.com/wiki/UrBKwmmJNizGlzkoFU3l90wxgOd"
17
+ name: "prd-5spread"
18
+ - url: "https://romangic.sg.larksuite.com/wiki/ZiCuw4GnXiBsPnkN159lNE0mgLe"
19
+ name: "backend-ai"
20
+ - url: "https://romangic.sg.larksuite.com/wiki/EB9NwMUP2ipF0Pk0SZBlXOS5gcb"
21
+ name: "push"
22
+ - url: "https://www.notion.so/2f91190afab5806aa853c073a24edd28"
23
+ name: "push-prd-notion"
24
+ - url: "https://romangic.sg.larksuite.com/wiki/QtUuwKpXyiFmAckrIaOlqRDNgJe"
25
+ name: "backend-api"
26
+ - url: "https://www.notion.so/2f91190afab580a8acf1daf80ae02ac6"
27
+ name: "test-cases"
26
28
 
27
- # Push 需求文档 (飞书)
28
- - name: push
29
- type: feishu
30
- url: "https://romangic.sg.larksuite.com/wiki/EB9NwMUP2ipF0Pk0SZBlXOS5gcb"
31
- output: "test/push.md"
32
-
33
- # Push PRD (Notion)
34
- - name: push-prd-notion
35
- type: notion
36
- url: "https://www.notion.so/2f91190afab5806aa853c073a24edd28"
37
- output: "test/push-prd-notion.md"
38
-
39
- # 后端接口文档 (飞书)
40
- - name: backend-api
41
- type: feishu
42
- url: "https://romangic.sg.larksuite.com/wiki/QtUuwKpXyiFmAckrIaOlqRDNgJe"
43
- output: "test/backend-api.md"
29
+ # 版本化文档
30
+ versions:
31
+ v1.0:
32
+ 五牌阵:
33
+ prd:
34
+ - "https://romangic.sg.larksuite.com/wiki/UrBKwmmJNizGlzkoFU3l90wxgOd"
35
+ api:
36
+ - "https://romangic.sg.larksuite.com/wiki/ZiCuw4GnXiBsPnkN159lNE0mgLe"
37
+ Push通知:
38
+ prd:
39
+ - "https://romangic.sg.larksuite.com/wiki/EB9NwMUP2ipF0Pk0SZBlXOS5gcb"
40
+ test:
41
+ - "https://www.notion.so/2f91190afab580a8acf1daf80ae02ac6"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "growork",
3
- "version": "1.0.1",
3
+ "version": "1.1.1",
4
4
  "description": "将飞书文档同步到本地,为 AI Agent 提供完整上下文",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -4,11 +4,7 @@ import chalk from 'chalk';
4
4
  import { getConfigPath, configExists, getDefaultConfig } from '../utils/config.js';
5
5
 
6
6
  const DIRS_TO_CREATE = [
7
- 'docs/product',
8
- 'docs/design',
9
- 'docs/api',
10
- 'docs/tech',
11
- 'docs/test',
7
+ 'docs/custom',
12
8
  ];
13
9
 
14
10
  export async function initCommand(): Promise<void> {
@@ -1,7 +1,10 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
1
  import chalk from 'chalk';
4
- import { loadConfig, configExists } from '../utils/config.js';
2
+ import { configExists, loadConfigV2, parseDocInput } from '../utils/config.js';
3
+
4
+ function shortenUrl(url: string, maxLen: number = 40): string {
5
+ if (url.length <= maxLen) return url;
6
+ return url.slice(0, maxLen - 3) + '...';
7
+ }
5
8
 
6
9
  export async function listCommand(): Promise<void> {
7
10
  if (!configExists()) {
@@ -9,29 +12,59 @@ export async function listCommand(): Promise<void> {
9
12
  process.exit(1);
10
13
  }
11
14
 
12
- const config = loadConfig();
13
- const cwd = process.cwd();
15
+ const config = loadConfigV2();
14
16
 
15
17
  console.log(chalk.blue('📋 文档列表\n'));
16
18
 
17
- console.log(chalk.gray(' 名称'.padEnd(18) + '类型'.padEnd(10) + '输出路径'.padEnd(30) + '状态'));
18
- console.log(chalk.gray(' ' + '-'.repeat(70)));
19
+ let totalCount = 0;
19
20
 
20
- for (const doc of config.docs) {
21
- const outputPath = path.join(cwd, doc.output);
22
- const exists = fs.existsSync(outputPath);
21
+ // 显示 custom 文档
22
+ if (config.custom && config.custom.length > 0) {
23
+ console.log(chalk.cyan('📁 custom (全局文档)'));
24
+ for (const input of config.custom) {
25
+ const { url, name } = parseDocInput(input);
26
+ const displayName = name || shortenUrl(url);
27
+ console.log(chalk.gray(` - ${displayName}`));
28
+ totalCount++;
29
+ }
30
+ console.log('');
31
+ }
23
32
 
24
- const status = exists
25
- ? chalk.green('已同步')
26
- : chalk.yellow('未同步');
33
+ // 显示 versions 文档
34
+ if (config.versions) {
35
+ for (const [version, features] of Object.entries(config.versions)) {
36
+ console.log(chalk.cyan(`📁 ${version}`));
27
37
 
28
- const name = ` ${doc.name}`.padEnd(18);
29
- const type = doc.type.padEnd(10);
30
- const output = doc.output.padEnd(30);
38
+ for (const [feature, value] of Object.entries(features)) {
39
+ if (Array.isArray(value)) {
40
+ // 简单 feature
41
+ console.log(chalk.white(` └─ ${feature}`));
42
+ for (const input of value) {
43
+ const { url, name } = parseDocInput(input);
44
+ const displayName = name || shortenUrl(url, 30);
45
+ console.log(chalk.gray(` - ${displayName}`));
46
+ totalCount++;
47
+ }
48
+ } else {
49
+ // 分类型的 feature
50
+ console.log(chalk.white(` └─ ${feature}`));
51
+ for (const docType of ['prd', 'design', 'api', 'test'] as const) {
52
+ const docInputs = value[docType];
53
+ if (!docInputs || docInputs.length === 0) continue;
31
54
 
32
- console.log(`${name}${chalk.gray(type)}${output}${status}`);
55
+ console.log(chalk.yellow(` ${docType}:`));
56
+ for (const input of docInputs) {
57
+ const { url, name } = parseDocInput(input);
58
+ const displayName = name || shortenUrl(url, 25);
59
+ console.log(chalk.gray(` - ${displayName}`));
60
+ totalCount++;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ console.log('');
66
+ }
33
67
  }
34
68
 
35
- console.log('');
36
- console.log(chalk.gray(`共 ${config.docs.length} 个文档配置`));
69
+ console.log(chalk.gray(`共 ${totalCount} 个文档配置`));
37
70
  }
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import chalk from 'chalk';
4
- import { loadConfig } from '../utils/config.js';
4
+ import { loadConfigV2, normalizeConfig, sanitizeFileName, SyncOptions, NormalizedDoc } from '../utils/config.js';
5
5
  import { FeishuService } from '../services/feishu.js';
6
6
  import { NotionService } from '../services/notion.js';
7
7
 
@@ -14,44 +14,58 @@ function clearLine(): void {
14
14
  }
15
15
  }
16
16
 
17
- export async function syncCommand(docName?: string): Promise<void> {
18
- const config = loadConfig();
17
+ function extractTitleFromMarkdown(content: string): string {
18
+ const match = content.match(/^#\s+(.+)$/m);
19
+ return match ? match[1].trim() : '未命名文档';
20
+ }
21
+
22
+ export async function syncCommand(options: SyncOptions = {}): Promise<void> {
23
+ const config = loadConfigV2();
24
+ const docs = normalizeConfig(config, options);
25
+
26
+ if (docs.length === 0) {
27
+ console.log(chalk.yellow('⚠️ 没有找到匹配的文档'));
28
+ return;
29
+ }
30
+
19
31
  const feishuService = config.feishu ? new FeishuService(config.feishu) : null;
20
32
  const notionService = config.notion ? new NotionService(config.notion) : null;
21
33
 
22
- let docsToSync = config.docs;
34
+ // 检查凭证
35
+ const hasFeishuDocs = docs.some(d => d.type === 'feishu');
36
+ const hasNotionDocs = docs.some(d => d.type === 'notion');
23
37
 
24
- if (docName) {
25
- const doc = config.docs.find(d => d.name === docName);
26
- if (!doc) {
27
- console.log(chalk.red(`❌ 未找到名为 "${docName}" 的文档配置`));
28
- console.log(chalk.gray(`可用的文档: ${config.docs.map(d => d.name).join(', ')}`));
29
- process.exit(1);
30
- }
31
- docsToSync = [doc];
38
+ if (hasFeishuDocs && !feishuService) {
39
+ throw new Error('配置文件缺少飞书凭证 (feishu.appId, feishu.appSecret)');
40
+ }
41
+ if (hasNotionDocs && !notionService) {
42
+ throw new Error('配置文件缺少 Notion 凭证 (notion.token)');
32
43
  }
33
44
 
34
45
  console.log(chalk.blue('📄 开始同步文档...\n'));
35
46
 
36
47
  let successCount = 0;
37
48
 
38
- for (const doc of docsToSync) {
39
- process.stdout.write(chalk.gray(` ⏳ ${doc.name.padEnd(15)} ${doc.output}`));
49
+ for (const doc of docs) {
50
+ const displayName = doc.name || doc.url.slice(-20);
51
+ process.stdout.write(chalk.gray(` ⏳ ${displayName}`));
40
52
 
41
53
  try {
42
54
  let content: string;
43
55
  if (doc.type === 'feishu') {
44
- if (!feishuService) throw new Error('飞书服务未配置');
45
- content = await feishuService.getDocumentAsMarkdown(doc.url);
46
- } else if (doc.type === 'notion') {
47
- if (!notionService) throw new Error('Notion 服务未配置');
48
- content = await notionService.getPageAsMarkdown(doc.url);
56
+ content = await feishuService!.getDocumentAsMarkdown(doc.url);
49
57
  } else {
50
- throw new Error(`不支持的文档类型: ${doc.type}`);
58
+ content = await notionService!.getPageAsMarkdown(doc.url);
51
59
  }
52
60
 
53
- const outputPath = path.join(process.cwd(), doc.output);
61
+ // markdown 提取标题
62
+ const title = doc.name || extractTitleFromMarkdown(content);
63
+ const safeTitle = sanitizeFileName(title);
64
+
65
+ // 替换占位符生成最终路径
66
+ const outputPath = path.join(process.cwd(), doc.outputPath.replace('{title}', safeTitle));
54
67
  const outputDir = path.dirname(outputPath);
68
+
55
69
  if (!fs.existsSync(outputDir)) {
56
70
  fs.mkdirSync(outputDir, { recursive: true });
57
71
  }
@@ -59,19 +73,19 @@ export async function syncCommand(docName?: string): Promise<void> {
59
73
  fs.writeFileSync(outputPath, content, 'utf-8');
60
74
 
61
75
  clearLine();
62
- console.log(chalk.green(` ✓ ${doc.name.padEnd(15)} → ${doc.output}`));
76
+ console.log(chalk.green(` ✓ ${safeTitle.padEnd(25)} → ${path.relative(process.cwd(), outputPath)}`));
63
77
  successCount++;
64
78
  } catch (error) {
65
79
  clearLine();
66
80
  const errorMessage = error instanceof Error ? error.message : String(error);
67
- console.log(chalk.red(` ✗ ${doc.name.padEnd(15)} → ${errorMessage}`));
81
+ console.log(chalk.red(` ✗ ${displayName.padEnd(25)} → ${errorMessage}`));
68
82
  }
69
83
  }
70
84
 
71
85
  console.log('');
72
- if (successCount === docsToSync.length) {
73
- console.log(chalk.green(`✅ 同步完成,共 ${docsToSync.length} 个文档`));
86
+ if (successCount === docs.length) {
87
+ console.log(chalk.green(`✅ 同步完成,共 ${docs.length} 个文档`));
74
88
  } else {
75
- console.log(chalk.yellow(`⚠️ 同步完成: ${successCount}/${docsToSync.length} 个文档成功`));
89
+ console.log(chalk.yellow(`⚠️ 同步完成: ${successCount}/${docs.length} 个文档成功`));
76
90
  }
77
91
  }