tb-order-sync 0.3.0

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/.env.example ADDED
@@ -0,0 +1,50 @@
1
+ # ============================================================
2
+ # 多表格同步与退款标记服务 — 环境配置
3
+ # 复制此文件为 .env 并填入实际值
4
+ # ============================================================
5
+
6
+ # ── 基础配置 ──────────────────────────────────────────────
7
+ APP_ENV=dev
8
+ LOG_LEVEL=INFO
9
+ STATE_DIR=state
10
+
11
+ # ── 腾讯文档配置 ──────────────────────────────────────────
12
+ TENCENT_CLIENT_ID=
13
+ TENCENT_CLIENT_SECRET=
14
+ TENCENT_OPEN_ID=
15
+ TENCENT_ACCESS_TOKEN=
16
+ TENCENT_A_FILE_ID=
17
+ TENCENT_A_SHEET_ID=
18
+ TENCENT_B_FILE_ID=
19
+ TENCENT_B_SHEET_ID=
20
+
21
+ # ── 飞书配置(预留) ─────────────────────────────────────
22
+ FEISHU_APP_ID=
23
+ FEISHU_APP_SECRET=
24
+ FEISHU_C_FILE_TOKEN=
25
+ FEISHU_C_SHEET_ID=
26
+
27
+ # ── 运行配置 ──────────────────────────────────────────────
28
+ GROSS_PROFIT_MODE=incremental
29
+ REFUND_MATCH_MODE=incremental
30
+ C_SYNC_MODE=incremental
31
+ TASK_INTERVAL_MINUTES=10
32
+ STARTUP_JITTER_SECONDS=15
33
+ WRITE_BATCH_SIZE=100
34
+ RETRY_TIMES=3
35
+ DRY_RUN=false
36
+ ENABLE_STYLE_UPDATE=false
37
+
38
+ # ── 列映射配置 ────────────────────────────────────────────
39
+ A_COL_PRODUCT_PRICE=C
40
+ A_COL_PACKAGING_PRICE=D
41
+ A_COL_FREIGHT=E
42
+ A_COL_CUSTOMER_QUOTE=F
43
+ A_COL_GROSS_PROFIT=G
44
+ A_COL_ORDER_NO=H
45
+ A_COL_REFUND_STATUS=I
46
+ B_COL_ORDER_NO=A
47
+
48
+ # ── 业务文案 ──────────────────────────────────────────────
49
+ REFUND_STATUS_TEXT=进入退款流程
50
+ DATA_ERROR_TEXT=数据异常
package/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## [0.3.0] - 2026-03-19
6
+
7
+ ### Added
8
+
9
+ - Added a Rich-based interactive dashboard with status cards, action panels, and log viewing.
10
+ - Added cross-platform daemon management with `daemon start|stop|restart|status|logs`.
11
+ - Added beginner onboarding links for Tencent Docs API and Feishu API inside the setup wizard.
12
+ - Added optional browser-opening support for official documentation links during setup.
13
+ - Added simplified CLI aliases such as `all`, `gp`, `rm`, `check`, `config`, and `menu`.
14
+ - Added npm launcher packaging with a `tb` binary entry point.
15
+ - Added `.gitignore` for Python build artifacts, runtime state, and local secrets.
16
+
17
+ ### Changed
18
+
19
+ - Renamed the distribution package to `tb-order-sync` to match the order-sync use case and the `tb` launcher.
20
+ - Changed no-argument startup to open the Rich dashboard by default.
21
+ - Changed macOS and Windows launcher scripts to delegate to the unified in-app dashboard.
22
+ - Changed README to document dashboard usage, daemon commands, API onboarding, and npm packaging.
23
+ - Changed CLI command structure to keep backward compatibility while exposing shorter commands.
24
+
25
+ ### Fixed
26
+
27
+ - Fixed heavy eager imports in the CLI so `--help` and setup-related flows return quickly.
28
+ - Fixed dashboard rendering noise by adding a quiet state-load mode.
29
+ - Fixed direct task/scheduler/daemon startup to block clearly when Tencent credentials are incomplete.
package/README.md ADDED
@@ -0,0 +1,392 @@
1
+ # TB Order Sync
2
+
3
+ **多表格同步与退款标记服务** — 自动同步腾讯文档 / 飞书多表格数据,计算毛利并标记退款订单。
4
+
5
+ > 一键启动、零配置门槛、支持 Windows / macOS 双平台,可打包为独立 exe 免安装分发。
6
+
7
+ ---
8
+
9
+ ## 功能概览
10
+
11
+ | 功能 | 说明 | 状态 |
12
+ |------|------|------|
13
+ | 毛利自动计算 | 读取 A 表字段,计算 `毛利 = 客户报价 - 运费 - 包装价格 - 产品价格`,异常数据标记 | ✅ 已实现 |
14
+ | 退款自动匹配 | B 表退款单号匹配 A 表订单,写入退款状态 + 可选整行标红 | ✅ 已实现 |
15
+ | 增量同步 | 基于行指纹 (MD5) 的变更检测,只处理变化行 | ✅ 已实现 |
16
+ | 全量同步 | 忽略缓存,全表重新计算 | ✅ 已实现 |
17
+ | 定时调度 | APScheduler 定时执行,支持 jitter 防冲突 | ✅ 已实现 |
18
+ | 交互式配置向导 | `setup` 命令一站式引导配置所有参数,并可直接打开官方文档链接 | ✅ 已实现 |
19
+ | Rich 控制台 | 无参启动即进入可视化 CLI 控制台 | ✅ 已实现 |
20
+ | 进程守护 | 支持后台启动 / 停止 / 状态 / 日志查看 | ✅ 已实现 |
21
+ | 一键启动脚本 | 双击即用,自动初始化环境,无需手动装 Python | ✅ 已实现 |
22
+ | 打包为 exe | PyInstaller 打包,免安装分发 | ✅ 已实现 |
23
+ | dry-run 模式 | 模拟执行不写入,安全验证 | ✅ 已实现 |
24
+ | 飞书 C 表同步 | C 表 → A 表数据同步 | 🔲 骨架已预留 |
25
+
26
+ ## 快速开始
27
+
28
+ ### 方式零:安装 `tb` 命令
29
+
30
+ ```bash
31
+ # 在项目根目录执行一次
32
+ npm link
33
+
34
+ # 之后直接使用
35
+ tb
36
+ tb menu
37
+ tb setup
38
+ tb all
39
+ tb gp
40
+ tb rm
41
+ tb daemon start
42
+ tb daemon status
43
+ ```
44
+
45
+ 如果你要生成可分发的 npm 包:
46
+
47
+ ```bash
48
+ npm pack
49
+ npm install -g ./tb-order-sync-0.3.0.tgz
50
+ ```
51
+
52
+ ### 方式一:一键启动(推荐)
53
+
54
+ **无需任何技术背景,双击即用。**
55
+
56
+ | 平台 | 操作 |
57
+ |------|------|
58
+ | **Windows** | 双击 `启动.bat` |
59
+ | **macOS** | 双击 `启动.command` |
60
+
61
+ 首次运行会自动完成:
62
+ 1. 检测 / 下载 Python 环境(如果没有)
63
+ 2. 安装依赖
64
+ 3. 进入 Rich 控制台
65
+ 4. 在控制台中继续配置、执行任务、管理守护进程
66
+
67
+ ```bash
68
+ tb
69
+ # 或直接双击启动脚本
70
+ # 默认进入 Rich 交互式控制台
71
+ ```
72
+
73
+ ### 方式二:命令行
74
+
75
+ ```bash
76
+ # 推荐入口
77
+ tb
78
+ tb menu
79
+
80
+ # 交互式配置
81
+ tb setup
82
+ tb config
83
+
84
+ # 验证配置
85
+ tb check
86
+ tb setup --check
87
+
88
+ # 执行任务
89
+ tb all
90
+ tb gp
91
+ tb rm
92
+ tb all --dry-run
93
+ tb all --mode full
94
+ tb gp --mode full
95
+
96
+ # 定时调度
97
+ tb start
98
+ tb schedule
99
+
100
+ # 后台守护
101
+ tb daemon start
102
+ tb daemon status
103
+ tb daemon logs --lines 80
104
+ tb daemon stop
105
+ tb daemon restart
106
+
107
+ # 兼容旧写法
108
+ python main.py
109
+ python main.py run all
110
+ python main.py run gross-profit
111
+ python main.py run refund-match
112
+ ```
113
+
114
+ ### 方式三:打包为 exe 分发
115
+
116
+ ```bash
117
+ # 构建(在目标平台上执行)
118
+ pip install pyinstaller
119
+ python build.py --clean
120
+
121
+ # 产物在 dist/sync_service/ 目录
122
+ # 连同 启动.bat(Windows)或 启动.command(macOS)一起分发
123
+ # 用户无需安装 Python
124
+ ```
125
+
126
+ ## API 获取指引
127
+
128
+ ### 腾讯文档 API
129
+
130
+ - 开发文档入口: [腾讯文档开放平台开发文档](https://docs.qq.com/open/document/app/)
131
+ - 开发者平台: [腾讯文档开放生态](https://docs.qq.com/open/developers/)
132
+ - 建议流程:
133
+ 1. 先进入开发者平台创建应用
134
+ 2. 在应用详情页获取 `Client ID` 和 `Client Secret`
135
+ 3. 按官方 OAuth2 流程获取 `Access Token`
136
+ 4. 再回到本项目执行 `tb setup`
137
+ - 当前说明:
138
+ - 本项目 MVP 目前依赖你手工提供有效 `Access Token`
139
+ - `Open ID` 先作为可选项保留
140
+ - 腾讯文档实际 endpoint / request schema 代码里仍有 `TODO / NEED_VERIFY`
141
+
142
+ ### 飞书 API
143
+
144
+ - 开发者平台: [Feishu Open Platform](https://open.feishu.cn/app)
145
+ - Token 官方文档: [获取 tenant_access_token](https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal)
146
+ - 新手教程: [如何解析和使用动态 Token](https://www.feishu.cn/content/000214591773)
147
+ - 建议流程:
148
+ 1. 在飞书开放平台创建自建应用
149
+ 2. 在应用凭证页获取 `App ID` 和 `App Secret`
150
+ 3. 按官方文档获取 `tenant_access_token`
151
+ 4. 后续接 C 表时再补齐 `File Token` / `Sheet ID`
152
+ - 当前说明:
153
+ - 本项目里的飞书 connector 目前还是 skeleton
154
+ - 现阶段向导里先采集配置,为后续 C 表接入做准备
155
+
156
+ ## 业务规则
157
+
158
+ ### A 表结构(订单表)
159
+
160
+ | 列 | 字段 | 说明 |
161
+ |----|------|------|
162
+ | A | 产品图 | — |
163
+ | B | 订单地址 | — |
164
+ | C | 产品价格 | 参与毛利计算 |
165
+ | D | 包装价格 | 参与毛利计算 |
166
+ | E | 运费 | 参与毛利计算 |
167
+ | F | 客户报价 | 参与毛利计算 |
168
+ | G | 毛利 | **自动计算写入** |
169
+ | H | 单号 | 退款匹配关键字段 |
170
+ | I | 退款状态 | **自动标记写入** |
171
+
172
+ ### B 表结构(退款表)
173
+
174
+ | 列 | 字段 |
175
+ |----|------|
176
+ | A | 单号(用于匹配 A 表 H 列) |
177
+ | B-L | 退货单号、店铺、原因、金额等 |
178
+
179
+ ### 毛利计算
180
+
181
+ ```
182
+ 毛利 = 客户报价(F) - 运费(E) - 包装价格(D) - 产品价格(C)
183
+ ```
184
+
185
+ - 字段为合法数字(int / float / 数字字符串)→ 正常计算
186
+ - 任一字段非法(空值、文本、None)→ 写入 `数据异常`,日志记录详情
187
+
188
+ ### 退款匹配
189
+
190
+ - B 表 A 列单号存在于 A 表 H 列 → I 列写入 `进入退款流程`
191
+ - 不存在 → 清空 I 列(同步取消)
192
+ - 可选:`ENABLE_STYLE_UPDATE=true` 时额外设置行背景色标红
193
+
194
+ ## 项目结构
195
+
196
+ ```
197
+ tb-order-sync/
198
+ ├── main.py # 入口
199
+ ├── build.py # PyInstaller 构建脚本
200
+ ├── sync_service.spec # PyInstaller spec 文件
201
+ ├── package.json # npm bin 包定义(tb)
202
+ ├── CHANGELOG.md # 更新日志
203
+ ├── requirements.txt
204
+ ├── .env.example # 环境变量模板
205
+ ├── tb # 本地 Unix launcher
206
+ ├── tb.cmd # 本地 Windows launcher
207
+ ├── 启动.bat # Windows 一键启动
208
+ ├── 启动.command # macOS 一键启动
209
+ ├── bin/
210
+ │ └── tb.js # npm / node 统一入口
211
+
212
+ ├── config/
213
+ │ ├── settings.py # Pydantic Settings 全局配置
214
+ │ └── mappings.py # 列字母 ↔ 索引映射
215
+
216
+ ├── connectors/
217
+ │ ├── base.py # BaseSheetConnector 抽象接口
218
+ │ ├── tencent_docs.py # 腾讯文档连接器
219
+ │ └── feishu_sheets.py # 飞书连接器(骨架)
220
+
221
+ ├── models/
222
+ │ ├── records.py # OrderRecord / RefundRecord
223
+ │ ├── task_models.py # SyncTaskConfig / TaskResult
224
+ │ └── state_models.py # SyncState / RowFingerprint
225
+
226
+ ├── services/
227
+ │ ├── gross_profit_service.py # 毛利计算服务
228
+ │ ├── refund_match_service.py # 退款匹配服务
229
+ │ ├── c_to_a_sync_service.py # C→A 同步(骨架)
230
+ │ ├── scheduler_service.py # APScheduler 定时调度
231
+ │ ├── daemon_service.py # 跨平台守护进程管理
232
+ │ └── state_service.py # 本地 JSON 状态持久化
233
+
234
+ ├── utils/
235
+ │ ├── logger.py # 日志配置
236
+ │ ├── parser.py # 数值解析 / 单号标准化
237
+ │ ├── diff.py # 行指纹 / 集合 hash
238
+ │ └── retry.py # tenacity 重试装饰器
239
+
240
+ ├── cli/
241
+ │ ├── commands.py # CLI 命令路由
242
+ │ ├── dashboard.py # Rich 控制台首页
243
+ │ └── setup.py # 交互式配置向导
244
+
245
+ ├── state/ # 增量状态文件目录
246
+ │ └── sync_state.json # 自动生成
247
+
248
+ └── tests/
249
+ ├── test_parser.py # 20 tests
250
+ ├── test_gross_profit_service.py # 9 tests
251
+ └── test_refund_match_service.py # 6 tests
252
+ ```
253
+
254
+ ## 架构设计
255
+
256
+ ```
257
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
258
+ │ CLI / 启动 │────▶│ Services │────▶│ Connectors │
259
+ │ commands.py │ │ (业务逻辑) │ │ (平台隔离) │
260
+ └─────────────┘ └──────┬──────┘ └──────┬──────┘
261
+ │ │
262
+ ┌──────▼──────┐ ┌──────▼──────┐
263
+ │ Models │ │ Tencent Docs │
264
+ │ (数据结构) │ │ Feishu │
265
+ └──────┬──────┘ │ (更多...) │
266
+ │ └─────────────┘
267
+ ┌──────▼──────┐
268
+ │ State / Utils│
269
+ │ (状态/工具) │
270
+ └─────────────┘
271
+ ```
272
+
273
+ **核心原则:**
274
+ - **Connector 抽象** — 腾讯文档/飞书通过统一接口 `BaseSheetConnector` 隔离,新增平台只需实现接口
275
+ - **业务与平台分离** — Services 不依赖任何平台 API 细节
276
+ - **配置化** — 列映射、业务文案、运行模式全部可通过 `.env` 配置
277
+ - **可扩展** — 新增 D 表、E 表时无需重构主程序
278
+
279
+ ## 增量策略
280
+
281
+ | 目标 | 策略 | 存储 |
282
+ |------|------|------|
283
+ | A 表毛利 | 对每行关键字段 (C/D/E/F/H) 生成 MD5 指纹,仅处理指纹变化行 | `state/sync_state.json` |
284
+ | B 表退款 | 对退款单号集合生成整体 hash,集合不变则跳过 | `state/sync_state.json` |
285
+
286
+ ## 守护进程
287
+
288
+ - `tb daemon start`
289
+ - 在后台启动定时调度进程
290
+ - 自动写入 `state/scheduler.pid` 和 `state/scheduler.meta.json`
291
+ - `tb daemon status`
292
+ - 查看是否运行中、PID 和日志文件位置
293
+ - `tb daemon logs --lines 40`
294
+ - 查看后台日志尾部
295
+ - `tb daemon stop`
296
+ - 停止后台调度
297
+ - `tb daemon restart`
298
+ - 重启后台调度
299
+
300
+ 后台控制台输出默认写入:
301
+ - `state/scheduler.console.log`
302
+
303
+ ## 配置说明
304
+
305
+ 运行 `tb setup` 可交互式完成所有配置。也可手动编辑 `.env`:
306
+
307
+ ```ini
308
+ # 腾讯文档凭证
309
+ TENCENT_CLIENT_ID=your_client_id
310
+ TENCENT_CLIENT_SECRET=your_client_secret
311
+ TENCENT_ACCESS_TOKEN=your_access_token
312
+ TENCENT_A_FILE_ID=a_table_file_id
313
+ TENCENT_A_SHEET_ID=a_table_sheet_id
314
+ TENCENT_B_FILE_ID=b_table_file_id
315
+ TENCENT_B_SHEET_ID=b_table_sheet_id
316
+
317
+ # 运行模式
318
+ GROSS_PROFIT_MODE=incremental # incremental | full
319
+ REFUND_MATCH_MODE=incremental # incremental | full
320
+ TASK_INTERVAL_MINUTES=10 # 定时调度间隔
321
+ DRY_RUN=false # true = 模拟执行不写入
322
+ ENABLE_STYLE_UPDATE=false # true = 退款行标红
323
+
324
+ # 列映射(可自定义)
325
+ A_COL_PRODUCT_PRICE=C
326
+ A_COL_PACKAGING_PRICE=D
327
+ A_COL_FREIGHT=E
328
+ A_COL_CUSTOMER_QUOTE=F
329
+ A_COL_GROSS_PROFIT=G
330
+ A_COL_ORDER_NO=H
331
+ A_COL_REFUND_STATUS=I
332
+ B_COL_ORDER_NO=A
333
+ ```
334
+
335
+ 完整配置项见 [.env.example](.env.example)。
336
+
337
+ ## 测试
338
+
339
+ ```bash
340
+ # 运行全部测试(35 tests)
341
+ pytest tests/ -v
342
+
343
+ # 单独运行
344
+ pytest tests/test_parser.py -v
345
+ pytest tests/test_gross_profit_service.py -v
346
+ pytest tests/test_refund_match_service.py -v
347
+ ```
348
+
349
+ ## 技术栈
350
+
351
+ | 组件 | 用途 |
352
+ |------|------|
353
+ | Python 3.11+ | 运行环境 |
354
+ | pydantic / pydantic-settings | 数据模型 / 配置管理 |
355
+ | httpx | HTTP 客户端 |
356
+ | tenacity | 失败重试 |
357
+ | APScheduler | 定时任务调度 |
358
+ | rich | CLI 交互界面 |
359
+ | python-dotenv | 环境变量加载 |
360
+ | PyInstaller | 打包为可执行文件 |
361
+ | pytest | 单元测试 |
362
+
363
+ ## 部署方式对比
364
+
365
+ | 方式 | 需要 Python | 适用场景 |
366
+ |------|------------|---------|
367
+ | 双击启动脚本(源码) | 自动下载 | 开发/内部使用 |
368
+ | 打包 exe 分发 | 不需要 | 给非技术用户 |
369
+ | 命令行直接运行 | 需要 | 开发者 |
370
+
371
+ ## Roadmap
372
+
373
+ - [x] 毛利自动计算
374
+ - [x] 退款自动匹配
375
+ - [x] 增量 / 全量模式
376
+ - [x] 交互式配置向导
377
+ - [x] 一键启动脚本(Windows + macOS)
378
+ - [x] PyInstaller 打包
379
+ - [x] 定时调度 + dry-run
380
+ - [ ] 飞书 C 表 → A 表同步
381
+ - [ ] 腾讯文档 OAuth2 token 自动刷新
382
+ - [ ] 腾讯文档行样式 API 验证
383
+
384
+ ## 已知待确认项
385
+
386
+ - 腾讯文档 Open API 的实际 endpoint 和 request/response 格式(代码中标注 `TODO / NEED_VERIFY`)
387
+ - 腾讯文档是否支持通过 API 设置单元格样式
388
+ - OAuth2 token 自动刷新流程
389
+
390
+ ## License
391
+
392
+ MIT
package/bin/tb.js ADDED
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { spawn, spawnSync } = require("child_process");
6
+
7
+ const root = path.resolve(__dirname, "..");
8
+ const args = process.argv.slice(2);
9
+
10
+ function exists(target) {
11
+ try {
12
+ return fs.existsSync(target);
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ function runChecked(command, commandArgs, options = {}) {
19
+ const result = spawnSync(command, commandArgs, {
20
+ cwd: root,
21
+ stdio: "inherit",
22
+ ...options,
23
+ });
24
+
25
+ if (result.error) {
26
+ return { ok: false, error: result.error };
27
+ }
28
+ if (typeof result.status === "number" && result.status !== 0) {
29
+ return { ok: false, code: result.status };
30
+ }
31
+ return { ok: true };
32
+ }
33
+
34
+ function findSystemPython() {
35
+ const candidates = process.platform === "win32"
36
+ ? [
37
+ { command: "py", prefixArgs: ["-3"] },
38
+ { command: "python", prefixArgs: [] },
39
+ ]
40
+ : [
41
+ { command: "python3.14", prefixArgs: [] },
42
+ { command: "python3.13", prefixArgs: [] },
43
+ { command: "python3.12", prefixArgs: [] },
44
+ { command: "python3.11", prefixArgs: [] },
45
+ { command: "python3", prefixArgs: [] },
46
+ { command: "python", prefixArgs: [] },
47
+ ];
48
+
49
+ for (const candidate of candidates) {
50
+ const probe = runChecked(candidate.command, [...candidate.prefixArgs, "--version"], {
51
+ stdio: "ignore",
52
+ });
53
+ if (probe.ok) {
54
+ return candidate;
55
+ }
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function getVenvPython() {
61
+ if (process.platform === "win32") {
62
+ return path.join(root, ".venv", "Scripts", "python.exe");
63
+ }
64
+ return path.join(root, ".venv", "bin", "python");
65
+ }
66
+
67
+ function getBundledExecutable() {
68
+ const candidates = process.platform === "win32"
69
+ ? [
70
+ path.join(root, "sync_service.exe"),
71
+ path.join(root, "dist", "sync_service", "sync_service.exe"),
72
+ ]
73
+ : [
74
+ path.join(root, "sync_service"),
75
+ path.join(root, "dist", "sync_service", "sync_service"),
76
+ ];
77
+
78
+ return candidates.find(exists) || null;
79
+ }
80
+
81
+ function ensurePythonRuntime() {
82
+ const venvPython = getVenvPython();
83
+ if (exists(venvPython)) {
84
+ return { command: venvPython, prefixArgs: [] };
85
+ }
86
+
87
+ const systemPython = findSystemPython();
88
+ if (!systemPython) {
89
+ console.error("tb: 未找到可用的 Python 3.11+,也没有现成的打包可执行文件。");
90
+ console.error("tb: 请先安装 Python,或使用 build.py 构建可执行文件。");
91
+ process.exit(1);
92
+ }
93
+
94
+ console.log("tb: 正在初始化 Python 虚拟环境...");
95
+ let result = runChecked(systemPython.command, [...systemPython.prefixArgs, "-m", "venv", ".venv"]);
96
+ if (!result.ok) {
97
+ console.error("tb: 创建虚拟环境失败。");
98
+ process.exit(1);
99
+ }
100
+
101
+ const freshVenvPython = getVenvPython();
102
+ console.log("tb: 正在安装 Python 依赖...");
103
+ result = runChecked(freshVenvPython, ["-m", "pip", "install", "-q", "-r", "requirements.txt"]);
104
+ if (!result.ok) {
105
+ console.error("tb: 安装依赖失败。");
106
+ process.exit(1);
107
+ }
108
+
109
+ return { command: freshVenvPython, prefixArgs: [] };
110
+ }
111
+
112
+ function launch(command, commandArgs) {
113
+ const child = spawn(command, commandArgs, {
114
+ cwd: root,
115
+ stdio: "inherit",
116
+ env: process.env,
117
+ });
118
+
119
+ child.on("error", (error) => {
120
+ console.error(`tb: 启动失败: ${error.message}`);
121
+ process.exit(1);
122
+ });
123
+
124
+ child.on("exit", (code) => {
125
+ process.exit(code ?? 0);
126
+ });
127
+ }
128
+
129
+ const mainEntry = path.join(root, "main.py");
130
+ const preferBundled = process.env.TB_PREFER_BUNDLED === "1" || !exists(mainEntry);
131
+ const bundledExecutable = getBundledExecutable();
132
+
133
+ if (preferBundled && bundledExecutable) {
134
+ launch(bundledExecutable, args);
135
+ } else if (exists(mainEntry)) {
136
+ const python = ensurePythonRuntime();
137
+ launch(python.command, [...python.prefixArgs, mainEntry, ...args]);
138
+ } else if (bundledExecutable) {
139
+ launch(bundledExecutable, args);
140
+ } else {
141
+ console.error("tb: 未找到 main.py,也未找到可执行分发产物。");
142
+ process.exit(1);
143
+ }
package/build.py ADDED
@@ -0,0 +1,91 @@
1
+ """Cross-platform build script for packaging sync_service into an executable.
2
+
3
+ Usage:
4
+ python build.py # Build for current platform
5
+ python build.py --clean # Clean build artifacts first
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import shutil
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ ROOT = Path(__file__).resolve().parent
17
+ SPEC_FILE = ROOT / "sync_service.spec"
18
+ DIST_DIR = ROOT / "dist"
19
+ BUILD_DIR = ROOT / "build"
20
+
21
+
22
+ def clean() -> None:
23
+ """Remove previous build artifacts."""
24
+ for d in (DIST_DIR, BUILD_DIR):
25
+ if d.exists():
26
+ print(f"Cleaning {d} ...")
27
+ shutil.rmtree(d)
28
+ print("Clean done.")
29
+
30
+
31
+ def ensure_pyinstaller() -> None:
32
+ """Install PyInstaller if not present."""
33
+ try:
34
+ import PyInstaller # noqa: F401
35
+ except ImportError:
36
+ print("Installing PyInstaller ...")
37
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "pyinstaller"])
38
+
39
+
40
+ def build() -> None:
41
+ """Run PyInstaller with the spec file."""
42
+ ensure_pyinstaller()
43
+ print(f"\nBuilding from {SPEC_FILE} ...")
44
+ subprocess.check_call([
45
+ sys.executable, "-m", "PyInstaller",
46
+ str(SPEC_FILE),
47
+ "--noconfirm",
48
+ ])
49
+
50
+ output = DIST_DIR / "sync_service"
51
+ if output.exists():
52
+ # Copy .env.example to dist folder for convenience
53
+ example = ROOT / ".env.example"
54
+ if example.exists():
55
+ shutil.copy2(example, output / ".env.example")
56
+
57
+ # Create state directory
58
+ (output / "state").mkdir(exist_ok=True)
59
+
60
+ print(f"\n{'=' * 50}")
61
+ print(f"Build complete!")
62
+ print(f"Output: {output}")
63
+ print(f"{'=' * 50}")
64
+ print(f"\nNext steps:")
65
+ print(f" 1. cd {output}")
66
+ if sys.platform == "win32":
67
+ print(f" 2. sync_service.exe setup")
68
+ print(f" 3. sync_service.exe run all --dry-run")
69
+ print(f" 4. sync_service.exe run all")
70
+ else:
71
+ print(f" 2. ./sync_service setup")
72
+ print(f" 3. ./sync_service run all --dry-run")
73
+ print(f" 4. ./sync_service run all")
74
+ else:
75
+ print("Build may have failed — dist/sync_service not found")
76
+ sys.exit(1)
77
+
78
+
79
+ def main() -> None:
80
+ parser = argparse.ArgumentParser(description="Build sync_service executable")
81
+ parser.add_argument("--clean", action="store_true", help="Clean build artifacts before building")
82
+ args = parser.parse_args()
83
+
84
+ if args.clean:
85
+ clean()
86
+
87
+ build()
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
File without changes