workflow-cli 0.1.0__py3-none-any.whl

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.
wfcli/lark.py ADDED
@@ -0,0 +1,260 @@
1
+ """lark-cli 封装层 - 与飞书交互的唯一通道
2
+
3
+ 所有对飞书云盘的操作都通过此模块完成,内部调用 lark-cli 命令行工具。
4
+ 调用方只需关心 Python 数据结构和返回值,无需了解 lark-cli 的命令行细节。
5
+ """
6
+
7
+ import json
8
+ import subprocess
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ import click
13
+
14
+
15
+ class LarkError(Exception):
16
+ """lark-cli 调用异常"""
17
+ pass
18
+
19
+
20
+ class LarkClient:
21
+ """lark-cli 封装客户端
22
+
23
+ 所有方法均以 dict/list 返回,错误统一抛出 LarkError。
24
+ """
25
+
26
+ def __init__(self, timeout: int = 30):
27
+ self.timeout = timeout
28
+
29
+ def _run(self, cmd: list[str], expect_json: bool = True) -> Any:
30
+ """执行 lark-cli 命令,返回解析后的 JSON 或 stdout"""
31
+ try:
32
+ result = subprocess.run(
33
+ cmd,
34
+ capture_output=True,
35
+ text=True,
36
+ timeout=self.timeout,
37
+ )
38
+ except subprocess.TimeoutExpired:
39
+ raise LarkError(f"命令超时({self.timeout}s): {' '.join(cmd)}")
40
+ except FileNotFoundError:
41
+ raise LarkError("未找到 lark-cli,请先安装: npm install -g @anthropic/lark-cli")
42
+
43
+ if result.returncode != 0:
44
+ err_msg = result.stderr.strip() or result.stdout.strip() or "未知错误"
45
+ raise LarkError(f"lark-cli 执行失败: {err_msg}\n命令: {' '.join(cmd)}")
46
+
47
+ output = result.stdout.strip()
48
+ if not output:
49
+ return {} if expect_json else ""
50
+
51
+ if expect_json:
52
+ try:
53
+ return json.loads(output)
54
+ except json.JSONDecodeError:
55
+ # 部分 lark-cli 命令返回的不是纯 JSON,尝试提取 JSON 块
56
+ for line in output.split("\n"):
57
+ line = line.strip()
58
+ if line.startswith("{") or line.startswith("["):
59
+ try:
60
+ return json.loads(line)
61
+ except json.JSONDecodeError:
62
+ continue
63
+ raise LarkError(f"无法解析 lark-cli 返回的 JSON:\n{output[:500]}")
64
+
65
+ return output
66
+
67
+ # ── 云盘文件列表 ──────────────────────────────────────────────
68
+
69
+ def drive_list_files(self, folder_token: str, page_size: int = 200) -> list[dict]:
70
+ """列出文件夹下的所有文件/子文件夹
71
+
72
+ Args:
73
+ folder_token: 父文件夹 token
74
+ page_size: 每页数量,最大 200
75
+
76
+ Returns:
77
+ 文件列表,每项包含 name, token, type 等字段
78
+ """
79
+ params = json.dumps({"folder_token": folder_token, "page_size": page_size})
80
+ cmd = [
81
+ "lark-cli", "drive", "files", "list",
82
+ "--params", params,
83
+ "--as", "user",
84
+ ]
85
+ data = self._run(cmd)
86
+ # lark-cli 返回的结构可能是 {"data": {"files": [...]}} 或直接 {"files": [...]}
87
+ if isinstance(data, dict):
88
+ files = (
89
+ data.get("data", {}).get("files")
90
+ or data.get("files")
91
+ or data.get("data", {}).get("items")
92
+ or []
93
+ )
94
+ return files
95
+ return data if isinstance(data, list) else []
96
+
97
+ # ── 创建文件夹 ────────────────────────────────────────────────
98
+
99
+ def drive_create_folder(self, name: str, parent_token: str) -> str:
100
+ """在指定父文件夹下创建子文件夹
101
+
102
+ Args:
103
+ name: 文件夹名称
104
+ parent_token: 父文件夹 token
105
+
106
+ Returns:
107
+ 新建文件夹的 folder_token
108
+ """
109
+ cmd = [
110
+ "lark-cli", "drive", "+create-folder",
111
+ "--name", name,
112
+ "--folder-token", parent_token,
113
+ "--as", "user",
114
+ ]
115
+ data = self._run(cmd)
116
+ if isinstance(data, dict):
117
+ token = (
118
+ data.get("data", {}).get("token")
119
+ or data.get("token")
120
+ or data.get("data", {}).get("folder_token")
121
+ or ""
122
+ )
123
+ if not token:
124
+ raise LarkError(f"创建文件夹成功但未返回 token: {data}")
125
+ return token
126
+ raise LarkError(f"创建文件夹返回异常: {data}")
127
+
128
+ # ── 上传文件 ──────────────────────────────────────────────────
129
+
130
+ def drive_upload(
131
+ self,
132
+ local_path: str | Path,
133
+ folder_token: str | None = None,
134
+ file_token: str | None = None,
135
+ ) -> str:
136
+ """上传本地文件到飞书云盘
137
+
138
+ Args:
139
+ local_path: 本地文件路径
140
+ folder_token: 目标文件夹 token(新文件上传时使用)
141
+ file_token: 已有文件 token(覆盖上传时使用)
142
+
143
+ Returns:
144
+ 上传后的 file_token
145
+ """
146
+ local_path = Path(local_path)
147
+ if not local_path.exists():
148
+ raise LarkError(f"本地文件不存在: {local_path}")
149
+
150
+ cmd = [
151
+ "lark-cli", "drive", "+upload",
152
+ "--file", str(local_path),
153
+ "--as", "user",
154
+ ]
155
+ if file_token:
156
+ cmd.extend(["--file-token", file_token])
157
+ elif folder_token:
158
+ cmd.extend(["--folder-token", folder_token])
159
+ else:
160
+ raise LarkError("上传文件时必须指定 folder_token 或 file_token")
161
+
162
+ # lark-cli drive +download --output 必须为相对路径
163
+ # upload 不需要,但 cd 到文件所在目录更安全
164
+ cwd = local_path.parent
165
+ try:
166
+ result = subprocess.run(
167
+ cmd,
168
+ capture_output=True,
169
+ text=True,
170
+ timeout=self.timeout,
171
+ cwd=str(cwd),
172
+ )
173
+ except subprocess.TimeoutExpired:
174
+ raise LarkError(f"上传超时({self.timeout}s): {local_path.name}")
175
+
176
+ if result.returncode != 0:
177
+ err_msg = result.stderr.strip() or result.stdout.strip()
178
+ raise LarkError(f"上传失败: {err_msg}")
179
+
180
+ # 尝试从输出中提取 file_token
181
+ try:
182
+ data = json.loads(result.stdout.strip())
183
+ token = (
184
+ data.get("data", {}).get("file_token")
185
+ or data.get("file_token")
186
+ or data.get("data", {}).get("token")
187
+ or ""
188
+ )
189
+ return token
190
+ except json.JSONDecodeError:
191
+ return ""
192
+
193
+ # ── 下载文件 ──────────────────────────────────────────────────
194
+
195
+ def drive_download(self, file_token: str, output_dir: str | Path = ".") -> Path:
196
+ """从飞书云盘下载文件
197
+
198
+ Args:
199
+ file_token: 文件的 file_token
200
+ output_dir: 输出目录
201
+
202
+ Returns:
203
+ 下载后的本地文件路径
204
+ """
205
+ output_dir = Path(output_dir)
206
+ output_dir.mkdir(parents=True, exist_ok=True)
207
+
208
+ # lark-cli drive +download --output 必须为相对路径
209
+ # 先 cd 到目标目录再执行
210
+ cmd = [
211
+ "lark-cli", "drive", "+download",
212
+ "--file-token", file_token,
213
+ "--as", "user",
214
+ ]
215
+ try:
216
+ result = subprocess.run(
217
+ cmd,
218
+ capture_output=True,
219
+ text=True,
220
+ timeout=self.timeout,
221
+ cwd=str(output_dir),
222
+ )
223
+ except subprocess.TimeoutExpired:
224
+ raise LarkError(f"下载超时({self.timeout}s): {file_token}")
225
+
226
+ if result.returncode != 0:
227
+ err_msg = result.stderr.strip() or result.stdout.strip()
228
+ raise LarkError(f"下载失败: {err_msg}")
229
+
230
+ # 从输出中推断下载的文件名
231
+ output = result.stdout.strip()
232
+ try:
233
+ data = json.loads(output)
234
+ filename = data.get("data", {}).get("file_name") or data.get("file_name") or ""
235
+ except json.JSONDecodeError:
236
+ # 从输出文本中提取文件名
237
+ filename = ""
238
+ for line in output.split("\n"):
239
+ if "file_name" in line or "saved" in line.lower():
240
+ filename = line.split(":")[-1].strip()
241
+ break
242
+
243
+ if filename:
244
+ return output_dir / filename
245
+ # 如果无法确定文件名,返回目录下最新修改的文件
246
+ files = sorted(output_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True)
247
+ return files[0] if files else output_dir
248
+
249
+
250
+ # ── 全局单例 ──────────────────────────────────────────────────────
251
+
252
+ _client: LarkClient | None = None
253
+
254
+
255
+ def get_client() -> LarkClient:
256
+ """获取 LarkClient 全局单例"""
257
+ global _client
258
+ if _client is None:
259
+ _client = LarkClient()
260
+ return _client
wfcli/main.py ADDED
@@ -0,0 +1,29 @@
1
+ """CLI 入口 - 公司工作流工具"""
2
+
3
+ import click
4
+
5
+ from wfcli import __version__
6
+ from wfcli.updater import try_auto_update
7
+ from wfcli.commands.workspace import workspace
8
+ from wfcli.commands.doc import doc
9
+ from wfcli.commands.sync import sync
10
+ from wfcli.commands.config_cmd import config
11
+ from wfcli.commands.info import info
12
+
13
+
14
+ @click.group()
15
+ @click.version_option(version=__version__, prog_name="wf-cli")
16
+ def cli():
17
+ """公司工作流 CLI 工具 - 提升日常工作效率"""
18
+ try_auto_update()
19
+
20
+
21
+ cli.add_command(workspace)
22
+ cli.add_command(doc)
23
+ cli.add_command(sync)
24
+ cli.add_command(config)
25
+ cli.add_command(info)
26
+
27
+
28
+ if __name__ == "__main__":
29
+ cli()
wfcli/updater.py ADDED
@@ -0,0 +1,78 @@
1
+ """静默自动更新模块
2
+
3
+ 策略:
4
+ - 每天最多尝试一次更新(缓存文件 ~/.wfcli/.last_update)
5
+ - subprocess 调用 pip install --upgrade wfcli,15 秒超时
6
+ - 成功打印一行提示,失败静默忽略,绝不阻塞用户命令执行
7
+ """
8
+
9
+ import subprocess
10
+ import sys
11
+ from datetime import datetime
12
+ from pathlib import Path
13
+
14
+ from wfcli import config as cfg
15
+
16
+ _CACHE_DIR = cfg.CONFIG_DIR
17
+ _LAST_UPDATE_FILE = _CACHE_DIR / ".last_update"
18
+ _UPDATE_TIMEOUT = 15 # 秒
19
+
20
+
21
+ def try_auto_update() -> None:
22
+ """尝试静默自动更新,失败则静默跳过,不影响后续命令执行"""
23
+ try:
24
+ # 检查是否启用了自动更新
25
+ if not cfg.get("update.enabled", True):
26
+ return
27
+
28
+ # 检查间隔(天)
29
+ interval = cfg.get("update.check_interval_days", 1)
30
+ if not _should_check(interval):
31
+ return
32
+
33
+ # 记录本次检查时间
34
+ _CACHE_DIR.mkdir(parents=True, exist_ok=True)
35
+ _LAST_UPDATE_FILE.write_text(datetime.now().isoformat())
36
+
37
+ # 尝试升级
38
+ result = subprocess.run(
39
+ [
40
+ sys.executable, "-m", "pip", "install",
41
+ "--upgrade", "wf-cli",
42
+ "--quiet",
43
+ ],
44
+ capture_output=True,
45
+ text=True,
46
+ timeout=_UPDATE_TIMEOUT,
47
+ )
48
+
49
+ if result.returncode == 0 and "Successfully installed" in result.stdout:
50
+ # 提取新版本号
51
+ for line in result.stdout.split("\n"):
52
+ if "wfcli" in line.lower():
53
+ click_echo(f" 已自动升级到最新版本: {line.strip()}")
54
+ return
55
+ click_echo(" 已自动升级到最新版本")
56
+
57
+ except Exception:
58
+ # 网络超时、权限问题、pip 不存在等,全部静默忽略
59
+ pass
60
+
61
+
62
+ def _should_check(interval_days: int) -> bool:
63
+ """判断是否应该检查更新"""
64
+ if not _LAST_UPDATE_FILE.exists():
65
+ return True
66
+ try:
67
+ last_str = _LAST_UPDATE_FILE.read_text().strip()
68
+ last_check = datetime.fromisoformat(last_str)
69
+ days_passed = (datetime.now() - last_check).days
70
+ return days_passed >= interval_days
71
+ except Exception:
72
+ return True
73
+
74
+
75
+ def click_echo(msg: str) -> None:
76
+ """延迟导入 click 以避免启动时循环依赖"""
77
+ import click
78
+ click.echo(msg)
@@ -0,0 +1 @@
1
+ """utils 工具包"""
wfcli/utils/display.py ADDED
@@ -0,0 +1,66 @@
1
+ """终端输出格式化 - 基于 rich 的美化输出"""
2
+
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from rich.panel import Panel
6
+ from rich.text import Text
7
+
8
+ console = Console()
9
+
10
+
11
+ def print_success(msg: str) -> None:
12
+ """打印成功信息"""
13
+ console.print(f"[green]✓[/green] {msg}")
14
+
15
+
16
+ def print_error(msg: str) -> None:
17
+ """打印错误信息"""
18
+ console.print(f"[red]✗[/red] {msg}")
19
+
20
+
21
+ def print_warning(msg: str) -> None:
22
+ """打印警告信息"""
23
+ console.print(f"[yellow]![/yellow] {msg}")
24
+
25
+
26
+ def print_info(msg: str) -> None:
27
+ """打印普通信息"""
28
+ console.print(f"[blue]i[/blue] {msg}")
29
+
30
+
31
+ def print_table(headers: list[str], rows: list[list[str]], title: str = "") -> None:
32
+ """打印格式化表格"""
33
+ table = Table(title=title, show_lines=False, pad_edge=False)
34
+ for h in headers:
35
+ table.add_column(h, style="cyan")
36
+ for row in rows:
37
+ table.add_row(*row)
38
+ console.print(table)
39
+
40
+
41
+ def print_file_list(files: list[dict], title: str = "文件列表") -> None:
42
+ """打印文件列表"""
43
+ headers = ["名称", "类型", "Token"]
44
+ rows = []
45
+ for f in files:
46
+ name = f.get("name", "")
47
+ ftype = "文件夹" if f.get("type") == "folder" else "文件"
48
+ token = f.get("token", "")[:20] + "..." if len(f.get("token", "")) > 20 else f.get("token", "")
49
+ rows.append([name, ftype, token])
50
+ print_table(headers, rows, title)
51
+
52
+
53
+ def print_panel(content: str, title: str = "", style: str = "blue") -> None:
54
+ """打印面板"""
55
+ panel = Panel(content, title=title, border_style=style)
56
+ console.print(panel)
57
+
58
+
59
+ def print_step(step: int, total: int, msg: str) -> None:
60
+ """打印步骤进度"""
61
+ console.print(f"[dim][{step}/{total}][/dim] {msg}")
62
+
63
+
64
+ def print_divider() -> None:
65
+ """打印分隔线"""
66
+ console.rule()
wfcli/utils/fs.py ADDED
@@ -0,0 +1,142 @@
1
+ """本地文件系统操作工具"""
2
+
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ def find_workspace_root(start: Path | None = None) -> Path | None:
9
+ """从 start 向上查找包含 WORKSPACE.md 的目录
10
+
11
+ Args:
12
+ start: 起始目录,默认为当前工作目录
13
+
14
+ Returns:
15
+ 工作空间根目录路径,未找到返回 None
16
+ """
17
+ current = start or Path.cwd()
18
+ current = current.resolve()
19
+ while True:
20
+ if (current / "WORKSPACE.md").exists():
21
+ return current
22
+ parent = current.parent
23
+ if parent == current:
24
+ return None
25
+ current = parent
26
+
27
+
28
+ def require_workspace_root() -> Path:
29
+ """获取工作空间根目录,不存在则报错退出"""
30
+ import click
31
+ root = find_workspace_root()
32
+ if root is None:
33
+ raise click.ClickException(
34
+ "当前目录不在任何工作空间中。\n"
35
+ "请先 cd 到工作空间目录,或使用 'wfcli workspace pull' 下载工作空间。"
36
+ )
37
+ return root
38
+
39
+
40
+ def parse_workspace_md(path: Path) -> dict[str, Any]:
41
+ """解析 WORKSPACE.md,提取结构化信息
42
+
43
+ Returns:
44
+ {
45
+ "name": str, # 工作空间名称
46
+ "description": str, # 简介
47
+ "workflow_link": str, # workflow.md 飞书链接
48
+ "directories": [...], # 目录结构
49
+ "templates": {...}, # 模板索引 {类型: 模板路径}
50
+ "naming_rules": str, # 命名规范
51
+ }
52
+ """
53
+ content = path.read_text(encoding="utf-8")
54
+ result: dict[str, Any] = {
55
+ "name": "",
56
+ "description": "",
57
+ "workflow_link": "",
58
+ "directories": [],
59
+ "templates": {},
60
+ "naming_rules": "",
61
+ "raw_content": content,
62
+ }
63
+
64
+ # 提取飞书链接
65
+ link_match = re.search(r"https?://[^\s)]+feishu[^\s)]*", content)
66
+ if link_match:
67
+ result["workflow_link"] = link_match.group(0)
68
+
69
+ # 提取工作空间名称(通常在第一个 # 标题或开头描述中)
70
+ title_match = re.search(r"^#\s+(.+)", content, re.MULTILINE)
71
+ if title_match:
72
+ result["name"] = title_match.group(1).strip()
73
+
74
+ # 提取目录结构(代码块中的树形结构)
75
+ tree_match = re.search(r"```\n(.*?)```", content, re.DOTALL)
76
+ if tree_match:
77
+ tree_text = tree_match.group(1)
78
+ result["directories"] = _parse_tree(tree_text)
79
+
80
+ # 提取模板索引表
81
+ result["templates"] = _parse_template_table(content)
82
+
83
+ return result
84
+
85
+
86
+ def _parse_tree(tree_text: str) -> list[dict]:
87
+ """解析树形目录结构文本"""
88
+ dirs = []
89
+ for line in tree_text.strip().split("\n"):
90
+ # 匹配如 "01_会议纪要" 或 "├── 01_会议纪要" 格式
91
+ match = re.search(r"(\d{2}_\S+)", line)
92
+ if match:
93
+ name = match.group(1).strip()
94
+ # 判断层级
95
+ depth = 0
96
+ if "│" in line or "├" in line or "└" in line:
97
+ depth = line.count("│") + (1 if "├" in line or "└" in line else 0)
98
+ dirs.append({"name": name, "depth": depth})
99
+ return dirs
100
+
101
+
102
+ def _parse_template_table(content: str) -> dict[str, str]:
103
+ """从 Markdown 表格中提取模板索引"""
104
+ templates = {}
105
+ # 匹配表格行: | 文档类型 | 模板路径 | ...
106
+ for line in content.split("\n"):
107
+ cells = [c.strip() for c in line.split("|")]
108
+ if len(cells) >= 3 and not line.startswith("|---") and not line.startswith("| :"):
109
+ doc_type = cells[1].strip()
110
+ template_path = cells[2].strip()
111
+ if doc_type and template_path and doc_type != "文档类型" and template_path != "模板路径":
112
+ templates[doc_type] = template_path
113
+ return templates
114
+
115
+
116
+ def ensure_dir(path: Path) -> Path:
117
+ """确保目录存在,不存在则创建"""
118
+ path.mkdir(parents=True, exist_ok=True)
119
+ return path
120
+
121
+
122
+ def list_local_dirs(root: Path) -> list[Path]:
123
+ """列出工作空间下的一级目录"""
124
+ return sorted(
125
+ [d for d in root.iterdir() if d.is_dir() and not d.name.startswith(".")],
126
+ key=lambda p: p.name,
127
+ )
128
+
129
+
130
+ def list_local_files(directory: Path, pattern: str = "*") -> list[Path]:
131
+ """列出目录下的文件"""
132
+ return sorted(
133
+ [f for f in directory.glob(pattern) if f.is_file()],
134
+ key=lambda p: p.name,
135
+ )
136
+
137
+
138
+ def read_template(template_path: Path) -> str:
139
+ """读取模板文件内容"""
140
+ if not template_path.exists():
141
+ raise FileNotFoundError(f"模板文件不存在: {template_path}")
142
+ return template_path.read_text(encoding="utf-8")
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: workflow-cli
3
+ Version: 0.1.0
4
+ Summary: 公司工作流 CLI 工具 —— 提升日常工作效率
5
+ Author-email: TODO <TODO@example.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/TODO/workflow-cli
8
+ Project-URL: Repository, https://github.com/TODO/workflow-cli
9
+ Project-URL: Issues, https://github.com/TODO/workflow-cli/issues
10
+ Keywords: cli,workflow,feishu,lark
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: click>=8.0
25
+ Requires-Dist: rich>=13.0
26
+ Provides-Extra: toml
27
+ Requires-Dist: tomli>=2.0; extra == "toml"
28
+ Requires-Dist: tomli-w>=1.0; extra == "toml"
29
+ Dynamic: license-file
30
+
31
+ # wf-cli
32
+
33
+ 公司工作流 CLI 工具 —— 提升日常工作效率的命令行助手。
34
+
35
+ ## 功能
36
+
37
+ | 命令 | 说明 |
38
+ |------|------|
39
+ | `wf-cli workspace` | 管理工作空间(创建/下载/新增文档类型/更新模板) |
40
+ | `wf-cli doc` | 基于模板交互式生成文档 |
41
+ | `wf-cli sync` | 与飞书云盘双向同步文件 |
42
+ | `wf-cli config` | 查看和修改 CLI 配置 |
43
+ | `wf-cli info` | 显示版本和当前工作环境状态 |
44
+
45
+ ## 安装
46
+
47
+ ```bash
48
+ pip install workflow-cli
49
+ ```
50
+
51
+ ## 前置依赖
52
+
53
+ - **Python >= 3.10**
54
+ - **[lark-cli](https://www.npmjs.com/package/lark-cli)**(可选,飞书云盘集成需要):`npm install -g lark-cli`
55
+
56
+ > 💡 即使没有安装 lark-cli,`wf-cli info` 等命令仍可正常运行,仅飞书相关操作会友好提示。
57
+
58
+ ## 快速开始
59
+
60
+ ```bash
61
+ # 查看帮助
62
+ wf-cli --help
63
+
64
+ # 查看当前状态
65
+ wf-cli info
66
+
67
+ # 查看默认配置
68
+ wf-cli config show
69
+
70
+ # 修改飞书根目录 token
71
+ wf-cli config set feishu.root_folder_token <your-token>
72
+
73
+ # 列出云盘上的工作空间
74
+ wf-cli workspace list
75
+
76
+ # 下载工作空间到本地
77
+ wf-cli workspace pull
78
+
79
+ # 生成文档
80
+ wf-cli doc generate
81
+ ```
82
+
83
+ ## 配置
84
+
85
+ 配置文件位于 `~/.wfcli/config.toml`(TOML 格式),首次运行时自动生成默认配置。
86
+
87
+ 可配置项:
88
+
89
+ | 配置项 | 说明 | 默认值 |
90
+ |--------|------|--------|
91
+ | `feishu.root_folder_token` | 飞书云盘工作空间根目录 token | 已预设 |
92
+ | `workspace.default_local_dir` | 默认本地下载路径 | `~/workspaces` |
93
+ | `update.enabled` | 是否启用自动更新 | `true` |
94
+ | `update.check_interval_days` | 更新检查间隔(天) | `1` |
95
+
96
+ ## Python 3.10 用户注意
97
+
98
+ Python 3.10 不内置 `tomllib`,请安装时带上 `toml` 额外依赖:
99
+
100
+ ```bash
101
+ pip install workflow-cli[toml]
102
+ ```
103
+
104
+ Python 3.11+ 用户无需额外操作。
105
+
106
+ ## 许可证
107
+
108
+ MIT