tb-order-sync 0.3.1 → 0.4.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 +3 -2
- package/CHANGELOG.md +23 -0
- package/README.md +107 -104
- package/build.py +10 -4
- package/cli/commands.py +66 -6
- package/cli/dashboard.py +40 -9
- package/cli/setup.py +87 -35
- package/config/settings.py +2 -2
- package/connectors/tencent_docs.py +247 -137
- package/models/state_models.py +4 -1
- package/models/task_models.py +15 -0
- package/package.json +5 -2
- package/services/daemon_service.py +134 -0
- package/services/gross_profit_service.py +15 -1
- package/services/refund_match_service.py +52 -9
- package/services/scheduler_service.py +27 -2
- package/services/state_service.py +25 -0
- package/sync_service.spec +1 -0
- package/utils/retry.py +20 -2
- package//345/277/253/351/200/237/345/274/200/345/247/213.txt +31 -0
package/.env.example
CHANGED
|
@@ -10,6 +10,7 @@ STATE_DIR=state
|
|
|
10
10
|
|
|
11
11
|
# ── 腾讯文档配置 ──────────────────────────────────────────
|
|
12
12
|
TENCENT_CLIENT_ID=
|
|
13
|
+
# 当前运行链路可不填;如后续接 OAuth 自动刷新可再补齐
|
|
13
14
|
TENCENT_CLIENT_SECRET=
|
|
14
15
|
TENCENT_OPEN_ID=
|
|
15
16
|
TENCENT_ACCESS_TOKEN=
|
|
@@ -33,7 +34,7 @@ STARTUP_JITTER_SECONDS=15
|
|
|
33
34
|
WRITE_BATCH_SIZE=100
|
|
34
35
|
RETRY_TIMES=3
|
|
35
36
|
DRY_RUN=false
|
|
36
|
-
ENABLE_STYLE_UPDATE=
|
|
37
|
+
ENABLE_STYLE_UPDATE=true
|
|
37
38
|
|
|
38
39
|
# ── 列映射配置 ────────────────────────────────────────────
|
|
39
40
|
A_COL_PRODUCT_PRICE=C
|
|
@@ -46,5 +47,5 @@ A_COL_REFUND_STATUS=I
|
|
|
46
47
|
B_COL_ORDER_NO=A
|
|
47
48
|
|
|
48
49
|
# ── 业务文案 ──────────────────────────────────────────────
|
|
49
|
-
REFUND_STATUS_TEXT
|
|
50
|
+
REFUND_STATUS_TEXT=已退款
|
|
50
51
|
DATA_ERROR_TEXT=数据异常
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-03-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added startup self-check in `tb check`, including state directory write validation and live Tencent Docs A/B sheet read checks.
|
|
10
|
+
- Added Tencent Docs URL parsing in setup so users can paste full sheet links and auto-fill `File ID` / `Sheet ID`.
|
|
11
|
+
- Added login auto-start support via `tb daemon autostart-enable|status|disable` for Windows Task Scheduler and macOS LaunchAgent.
|
|
12
|
+
- Added `state/last_run.json` execution summaries for manual and scheduled runs.
|
|
13
|
+
- Added packaged `快速开始.txt` to distribution outputs for non-technical users.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Changed no-argument startup to enter `setup` automatically when required runtime config is missing.
|
|
18
|
+
- Changed config validation so `TENCENT_CLIENT_SECRET` is now optional for the current live Tencent Docs runtime path.
|
|
19
|
+
- Changed dashboard and daemon status output to surface recent run status and login auto-start state.
|
|
20
|
+
- Changed launcher and README onboarding to emphasize `tb check` as the first runtime self-check after setup.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Fixed Tencent Docs rate-limit handling by retrying `400007 Requests Over Limit` responses with exponential backoff.
|
|
25
|
+
- Fixed repeated batch writes being too aggressive by slowing inter-batch pacing.
|
|
26
|
+
- Fixed user-facing error reporting so failed tasks now include clearer cause hints in CLI output.
|
|
27
|
+
|
|
5
28
|
## [0.3.1] - 2026-03-19
|
|
6
29
|
|
|
7
30
|
### Added
|
package/README.md
CHANGED
|
@@ -4,130 +4,109 @@
|
|
|
4
4
|
[](https://github.com/SOULRAi/tb-order-sync)
|
|
5
5
|
[](https://github.com/SOULRAi/tb-order-sync/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
**多表格同步与退款标记服务**
|
|
7
|
+
**多表格同步与退款标记服务**
|
|
8
|
+
面向订单运营场景的轻量自动化服务,负责同步云表格、计算毛利、标记退款状态,并支持后台守护运行。
|
|
8
9
|
|
|
9
|
-
>
|
|
10
|
+
> 支持 `tb` 短命令、Rich 控制台、启动自检、守护进程、登录自启、Windows / macOS 一键启动,以及后续接入飞书 C 表的扩展路径。
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
**快速导航**
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
- [🚀 快速开始](#quick-start)
|
|
15
|
+
- [✨ 核心能力](#capabilities)
|
|
16
|
+
- [🔐 API 获取指引](#api-guide)
|
|
17
|
+
- [🧠 业务规则](#business-rules)
|
|
18
|
+
- [🧩 架构设计](#architecture)
|
|
19
|
+
- [🛡️ 守护进程](#daemon)
|
|
20
|
+
- [⚙️ 配置说明](#configuration)
|
|
21
|
+
- [🧪 测试](#testing)
|
|
14
22
|
|
|
15
|
-
|
|
23
|
+
<a id="capabilities"></a>
|
|
24
|
+
## ✨ 核心能力
|
|
25
|
+
|
|
26
|
+
| 能力 | 说明 | 状态 |
|
|
16
27
|
|------|------|------|
|
|
17
|
-
| 毛利自动计算 | 读取 A
|
|
18
|
-
| 退款自动匹配 | B 表退款单号匹配 A
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## 快速开始
|
|
31
|
-
|
|
32
|
-
###
|
|
28
|
+
| 💰 毛利自动计算 | 读取 A 表字段,按固定业务公式回写毛利列 | ✅ 已实现 |
|
|
29
|
+
| 🔁 退款自动匹配 | B 表退款单号匹配 A 表订单,写入退款状态列 | ✅ 已实现 |
|
|
30
|
+
| 🧾 数据异常标记 | 非法数字直接写入 `数据异常`,并记录日志 | ✅ 已实现 |
|
|
31
|
+
| ⚡ 增量同步 | 基于行指纹和退款集合 hash,仅处理变化数据 | ✅ 已实现 |
|
|
32
|
+
| 🧨 全量重建 | 支持忽略缓存,对全表重新扫描和回写 | ✅ 已实现 |
|
|
33
|
+
| 🖥️ Rich 控制台 | 无参启动即进入交互式控制台首页 | ✅ 已实现 |
|
|
34
|
+
| 🩺 启动自检 | 配置检查 + 状态目录写入 + 腾讯文档 A/B 表读取 | ✅ 已实现 |
|
|
35
|
+
| 🛡️ 守护进程 | 支持后台启动、停止、状态检查、日志查看 | ✅ 已实现 |
|
|
36
|
+
| 🔌 登录自启 | 支持 Windows 任务计划 / macOS LaunchAgent | ✅ 已实现 |
|
|
37
|
+
| 📦 一键分发 | 支持双击脚本、npm launcher、PyInstaller 打包 | ✅ 已实现 |
|
|
38
|
+
| 🐦 飞书预留接口 | C 表同步抽象层和 connector skeleton 已预留 | 🔲 骨架已预留 |
|
|
39
|
+
|
|
40
|
+
<a id="quick-start"></a>
|
|
41
|
+
## 🚀 快速开始
|
|
42
|
+
|
|
43
|
+
### 推荐命令
|
|
33
44
|
|
|
34
45
|
```bash
|
|
35
|
-
|
|
36
|
-
npm link
|
|
46
|
+
npm install -g tb-order-sync
|
|
37
47
|
|
|
38
|
-
# 之后直接使用
|
|
39
48
|
tb
|
|
40
|
-
tb menu
|
|
41
49
|
tb setup
|
|
50
|
+
tb check
|
|
51
|
+
tb all --dry-run
|
|
42
52
|
tb all
|
|
43
|
-
tb gp
|
|
44
|
-
tb rm
|
|
45
53
|
tb daemon start
|
|
46
54
|
tb daemon status
|
|
47
55
|
```
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
npm pack
|
|
53
|
-
npm install -g ./tb-order-sync-0.3.1.tgz
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### 方式一:一键启动(推荐)
|
|
57
|
-
|
|
58
|
-
**无需任何技术背景,双击即用。**
|
|
59
|
-
|
|
60
|
-
| 平台 | 操作 |
|
|
61
|
-
|------|------|
|
|
62
|
-
| **Windows** | 双击 `启动.bat` |
|
|
63
|
-
| **macOS** | 双击 `启动.command` |
|
|
57
|
+
首次运行说明:
|
|
58
|
+
- 如果本机还没有完整配置,直接执行 `tb` 会自动进入 `setup`
|
|
59
|
+
- `tb check` 会执行启动自检,确认状态目录、A 表、B 表是否可用
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
1. 检测 / 下载 Python 环境(如果没有)
|
|
67
|
-
2. 安装依赖
|
|
68
|
-
3. 进入 Rich 控制台
|
|
69
|
-
4. 在控制台中继续配置、执行任务、管理守护进程
|
|
61
|
+
### 启动方式
|
|
70
62
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
| 方式 | 适用场景 | 入口 |
|
|
64
|
+
|------|----------|------|
|
|
65
|
+
| `tb` 命令 | 开发、运维、长期使用 | `tb` / `tb setup` / `tb all` |
|
|
66
|
+
| 双击启动 | 非技术用户 | `启动.bat` / `启动.command` |
|
|
67
|
+
| Python 兼容入口 | 调试或源码环境 | `python main.py` |
|
|
68
|
+
| 打包分发 | 免安装交付 | `dist/sync_service/` |
|
|
76
69
|
|
|
77
|
-
###
|
|
70
|
+
### 常用命令速查
|
|
78
71
|
|
|
79
72
|
```bash
|
|
80
|
-
#
|
|
73
|
+
# 控制台
|
|
81
74
|
tb
|
|
82
75
|
tb menu
|
|
83
76
|
|
|
84
|
-
#
|
|
77
|
+
# 配置
|
|
85
78
|
tb setup
|
|
86
|
-
tb config
|
|
87
|
-
|
|
88
|
-
# 验证配置
|
|
89
79
|
tb check
|
|
90
|
-
tb setup --check
|
|
91
80
|
|
|
92
|
-
#
|
|
81
|
+
# 执行
|
|
93
82
|
tb all
|
|
94
83
|
tb gp
|
|
95
84
|
tb rm
|
|
96
85
|
tb all --dry-run
|
|
97
86
|
tb all --mode full
|
|
98
|
-
tb gp --mode full
|
|
99
87
|
|
|
100
|
-
#
|
|
88
|
+
# 调度 / 守护
|
|
101
89
|
tb start
|
|
102
|
-
tb schedule
|
|
103
|
-
|
|
104
|
-
# 后台守护
|
|
105
90
|
tb daemon start
|
|
106
91
|
tb daemon status
|
|
107
92
|
tb daemon logs --lines 80
|
|
108
93
|
tb daemon stop
|
|
109
|
-
tb daemon
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
python main.py
|
|
113
|
-
python main.py run all
|
|
114
|
-
python main.py run gross-profit
|
|
115
|
-
python main.py run refund-match
|
|
94
|
+
tb daemon autostart-enable
|
|
95
|
+
tb daemon autostart-status
|
|
96
|
+
tb daemon autostart-disable
|
|
116
97
|
```
|
|
117
98
|
|
|
118
|
-
###
|
|
99
|
+
### 双击启动
|
|
119
100
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
101
|
+
| 平台 | 文件 |
|
|
102
|
+
|------|------|
|
|
103
|
+
| Windows | `启动.bat` |
|
|
104
|
+
| macOS | `启动.command` |
|
|
124
105
|
|
|
125
|
-
|
|
126
|
-
# 连同 启动.bat(Windows)或 启动.command(macOS)一起分发
|
|
127
|
-
# 用户无需安装 Python
|
|
128
|
-
```
|
|
106
|
+
首次运行会自动补齐 Python 运行环境;如果本机尚未配置,会直接进入配置向导。
|
|
129
107
|
|
|
130
|
-
|
|
108
|
+
<a id="api-guide"></a>
|
|
109
|
+
## 🔐 API 获取指引
|
|
131
110
|
|
|
132
111
|
### 腾讯文档 API
|
|
133
112
|
|
|
@@ -140,8 +119,9 @@ python build.py --clean
|
|
|
140
119
|
4. 再回到本项目执行 `tb setup`
|
|
141
120
|
- 当前说明:
|
|
142
121
|
- 本项目 MVP 目前依赖你手工提供有效 `Access Token`
|
|
143
|
-
- `Open ID`
|
|
144
|
-
-
|
|
122
|
+
- 当前运行链路要求 `Client ID + Open ID + Access Token`
|
|
123
|
+
- `Client Secret` 目前保留为可选项,后续接自动刷新 token 时再使用
|
|
124
|
+
- 在线表格 v3 读写链路已经完成真实联调验证
|
|
145
125
|
|
|
146
126
|
### 飞书 API
|
|
147
127
|
|
|
@@ -157,7 +137,8 @@ python build.py --clean
|
|
|
157
137
|
- 本项目里的飞书 connector 目前还是 skeleton
|
|
158
138
|
- 现阶段向导里先采集配置,为后续 C 表接入做准备
|
|
159
139
|
|
|
160
|
-
|
|
140
|
+
<a id="business-rules"></a>
|
|
141
|
+
## 🧠 业务规则
|
|
161
142
|
|
|
162
143
|
### A 表结构(订单表)
|
|
163
144
|
|
|
@@ -191,11 +172,11 @@ python build.py --clean
|
|
|
191
172
|
|
|
192
173
|
### 退款匹配
|
|
193
174
|
|
|
194
|
-
- B 表 A 列单号存在于 A 表 H 列 → I 列写入
|
|
175
|
+
- B 表 A 列单号存在于 A 表 H 列 → I 列写入 `已退款`
|
|
195
176
|
- 不存在 → 清空 I 列(同步取消)
|
|
196
|
-
-
|
|
177
|
+
- `ENABLE_STYLE_UPDATE=true` 时会把匹配行整行改成红色文字
|
|
197
178
|
|
|
198
|
-
## 项目结构
|
|
179
|
+
## 🗂️ 项目结构
|
|
199
180
|
|
|
200
181
|
```
|
|
201
182
|
tb-order-sync/
|
|
@@ -255,7 +236,8 @@ tb-order-sync/
|
|
|
255
236
|
└── test_refund_match_service.py # 6 tests
|
|
256
237
|
```
|
|
257
238
|
|
|
258
|
-
|
|
239
|
+
<a id="architecture"></a>
|
|
240
|
+
## 🧩 架构设计
|
|
259
241
|
|
|
260
242
|
```
|
|
261
243
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
@@ -280,14 +262,15 @@ tb-order-sync/
|
|
|
280
262
|
- **配置化** — 列映射、业务文案、运行模式全部可通过 `.env` 配置
|
|
281
263
|
- **可扩展** — 新增 D 表、E 表时无需重构主程序
|
|
282
264
|
|
|
283
|
-
## 增量策略
|
|
265
|
+
## 🔄 增量策略
|
|
284
266
|
|
|
285
267
|
| 目标 | 策略 | 存储 |
|
|
286
268
|
|------|------|------|
|
|
287
269
|
| A 表毛利 | 对每行关键字段 (C/D/E/F/H) 生成 MD5 指纹,仅处理指纹变化行 | `state/sync_state.json` |
|
|
288
|
-
| B 表退款 |
|
|
270
|
+
| B 表退款 | 同时比较退款集合 hash 和 A 表单号/退款状态扫描 hash,避免漏处理 A 表变化 | `state/sync_state.json` |
|
|
289
271
|
|
|
290
|
-
|
|
272
|
+
<a id="daemon"></a>
|
|
273
|
+
## 🛡️ 守护进程
|
|
291
274
|
|
|
292
275
|
- `tb daemon start`
|
|
293
276
|
- 在后台启动定时调度进程
|
|
@@ -300,18 +283,32 @@ tb-order-sync/
|
|
|
300
283
|
- 停止后台调度
|
|
301
284
|
- `tb daemon restart`
|
|
302
285
|
- 重启后台调度
|
|
286
|
+
- `tb daemon autostart-enable`
|
|
287
|
+
- 启用当前用户登录自启
|
|
288
|
+
- `tb daemon autostart-status`
|
|
289
|
+
- 查看登录自启状态
|
|
290
|
+
- `tb daemon autostart-disable`
|
|
291
|
+
- 关闭登录自启
|
|
303
292
|
|
|
304
293
|
后台控制台输出默认写入:
|
|
305
294
|
- `state/scheduler.console.log`
|
|
295
|
+
- `state/last_run.json`
|
|
296
|
+
|
|
297
|
+
启动建议:
|
|
298
|
+
- 先执行 `tb check`,确认配置和连接正常
|
|
299
|
+
- 再执行 `tb daemon start`
|
|
300
|
+
- 如需电脑登录后自动运行,再执行 `tb daemon autostart-enable`
|
|
306
301
|
|
|
307
|
-
|
|
302
|
+
<a id="configuration"></a>
|
|
303
|
+
## ⚙️ 配置说明
|
|
308
304
|
|
|
309
305
|
运行 `tb setup` 可交互式完成所有配置。也可手动编辑 `.env`:
|
|
310
306
|
|
|
311
307
|
```ini
|
|
312
308
|
# 腾讯文档凭证
|
|
313
309
|
TENCENT_CLIENT_ID=your_client_id
|
|
314
|
-
TENCENT_CLIENT_SECRET=
|
|
310
|
+
TENCENT_CLIENT_SECRET=
|
|
311
|
+
TENCENT_OPEN_ID=your_open_id
|
|
315
312
|
TENCENT_ACCESS_TOKEN=your_access_token
|
|
316
313
|
TENCENT_A_FILE_ID=a_table_file_id
|
|
317
314
|
TENCENT_A_SHEET_ID=a_table_sheet_id
|
|
@@ -323,7 +320,7 @@ GROSS_PROFIT_MODE=incremental # incremental | full
|
|
|
323
320
|
REFUND_MATCH_MODE=incremental # incremental | full
|
|
324
321
|
TASK_INTERVAL_MINUTES=10 # 定时调度间隔
|
|
325
322
|
DRY_RUN=false # true = 模拟执行不写入
|
|
326
|
-
ENABLE_STYLE_UPDATE=
|
|
323
|
+
ENABLE_STYLE_UPDATE=true # true = 退款行标红
|
|
327
324
|
|
|
328
325
|
# 列映射(可自定义)
|
|
329
326
|
A_COL_PRODUCT_PRICE=C
|
|
@@ -336,12 +333,18 @@ A_COL_REFUND_STATUS=I
|
|
|
336
333
|
B_COL_ORDER_NO=A
|
|
337
334
|
```
|
|
338
335
|
|
|
336
|
+
补充说明:
|
|
337
|
+
- `tb setup` 支持直接粘贴腾讯文档完整链接,自动拆出 `File ID / Sheet ID`
|
|
338
|
+
- `tb check` 会做启动自检,不只是看 `.env` 是否存在
|
|
339
|
+
- 当前退款高亮效果是“整行红色文字”,不是背景填充
|
|
340
|
+
|
|
339
341
|
完整配置项见 [.env.example](.env.example)。
|
|
340
342
|
|
|
341
|
-
|
|
343
|
+
<a id="testing"></a>
|
|
344
|
+
## 🧪 测试
|
|
342
345
|
|
|
343
346
|
```bash
|
|
344
|
-
# 运行全部测试(
|
|
347
|
+
# 运行全部测试(49 tests)
|
|
345
348
|
pytest tests/ -v
|
|
346
349
|
|
|
347
350
|
# 单独运行
|
|
@@ -350,7 +353,7 @@ pytest tests/test_gross_profit_service.py -v
|
|
|
350
353
|
pytest tests/test_refund_match_service.py -v
|
|
351
354
|
```
|
|
352
355
|
|
|
353
|
-
## 技术栈
|
|
356
|
+
## 🧱 技术栈
|
|
354
357
|
|
|
355
358
|
| 组件 | 用途 |
|
|
356
359
|
|------|------|
|
|
@@ -364,15 +367,15 @@ pytest tests/test_refund_match_service.py -v
|
|
|
364
367
|
| PyInstaller | 打包为可执行文件 |
|
|
365
368
|
| pytest | 单元测试 |
|
|
366
369
|
|
|
367
|
-
## 部署方式对比
|
|
370
|
+
## 📦 部署方式对比
|
|
368
371
|
|
|
369
372
|
| 方式 | 需要 Python | 适用场景 |
|
|
370
373
|
|------|------------|---------|
|
|
371
|
-
| 双击启动脚本(源码) | 自动下载 | 开发/内部使用 |
|
|
372
|
-
|
|
|
374
|
+
| 双击启动脚本(源码) | 自动下载 / 初始化 | 开发/内部使用 |
|
|
375
|
+
| 打包分发(Windows / macOS) | 不需要 | 给非技术用户 |
|
|
373
376
|
| 命令行直接运行 | 需要 | 开发者 |
|
|
374
377
|
|
|
375
|
-
## Roadmap
|
|
378
|
+
## 🗺️ Roadmap
|
|
376
379
|
|
|
377
380
|
- [x] 毛利自动计算
|
|
378
381
|
- [x] 退款自动匹配
|
|
@@ -385,12 +388,12 @@ pytest tests/test_refund_match_service.py -v
|
|
|
385
388
|
- [ ] 腾讯文档 OAuth2 token 自动刷新
|
|
386
389
|
- [ ] 腾讯文档行样式 API 验证
|
|
387
390
|
|
|
388
|
-
## 已知待确认项
|
|
391
|
+
## ⚠️ 已知待确认项
|
|
389
392
|
|
|
390
393
|
- 腾讯文档 Open API 的实际 endpoint 和 request/response 格式(代码中标注 `TODO / NEED_VERIFY`)
|
|
391
394
|
- 腾讯文档是否支持通过 API 设置单元格样式
|
|
392
395
|
- OAuth2 token 自动刷新流程
|
|
393
396
|
|
|
394
|
-
## License
|
|
397
|
+
## 📄 License
|
|
395
398
|
|
|
396
399
|
MIT
|
package/build.py
CHANGED
|
@@ -17,6 +17,12 @@ ROOT = Path(__file__).resolve().parent
|
|
|
17
17
|
SPEC_FILE = ROOT / "sync_service.spec"
|
|
18
18
|
DIST_DIR = ROOT / "dist"
|
|
19
19
|
BUILD_DIR = ROOT / "build"
|
|
20
|
+
DIST_RUNTIME_FILES = [
|
|
21
|
+
".env.example",
|
|
22
|
+
"启动.bat",
|
|
23
|
+
"启动.command",
|
|
24
|
+
"快速开始.txt",
|
|
25
|
+
]
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
def clean() -> None:
|
|
@@ -49,10 +55,10 @@ def build() -> None:
|
|
|
49
55
|
|
|
50
56
|
output = DIST_DIR / "sync_service"
|
|
51
57
|
if output.exists():
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
for name in DIST_RUNTIME_FILES:
|
|
59
|
+
source = ROOT / name
|
|
60
|
+
if source.exists():
|
|
61
|
+
shutil.copy2(source, output / name)
|
|
56
62
|
|
|
57
63
|
# Create state directory
|
|
58
64
|
(output / "state").mkdir(exist_ok=True)
|
package/cli/commands.py
CHANGED
|
@@ -4,10 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
6
|
import sys
|
|
7
|
+
from datetime import datetime
|
|
7
8
|
from typing import TYPE_CHECKING, Optional
|
|
8
9
|
|
|
9
10
|
from config.settings import Settings, SyncMode, get_settings
|
|
10
|
-
from models.task_models import TaskResult
|
|
11
|
+
from models.task_models import RunSummary, TaskResult
|
|
11
12
|
from services.daemon_service import DaemonService
|
|
12
13
|
from services.state_service import StateService
|
|
13
14
|
from utils.logger import get_logger, setup_logging
|
|
@@ -54,7 +55,7 @@ def has_required_runtime_config(settings: Settings) -> bool:
|
|
|
54
55
|
"""Return whether Tencent runtime essentials are configured."""
|
|
55
56
|
required = [
|
|
56
57
|
settings.tencent_client_id,
|
|
57
|
-
settings.
|
|
58
|
+
settings.tencent_open_id,
|
|
58
59
|
settings.tencent_access_token,
|
|
59
60
|
settings.tencent_a_file_id,
|
|
60
61
|
settings.tencent_a_sheet_id,
|
|
@@ -101,6 +102,12 @@ def execute_tasks(
|
|
|
101
102
|
_print_result(result)
|
|
102
103
|
results.append(result)
|
|
103
104
|
|
|
105
|
+
if results:
|
|
106
|
+
try:
|
|
107
|
+
state_svc.save_last_run(_build_run_summary(results, trigger="manual"))
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
logger.warning("Failed to save last run summary: %s", exc)
|
|
110
|
+
|
|
104
111
|
return results
|
|
105
112
|
|
|
106
113
|
|
|
@@ -137,9 +144,11 @@ def cmd_daemon(args: argparse.Namespace, settings: Settings) -> None:
|
|
|
137
144
|
daemon = DaemonService(settings)
|
|
138
145
|
action = args.daemon_action
|
|
139
146
|
|
|
140
|
-
if action
|
|
147
|
+
if action in {"start", "restart", "autostart-enable"}:
|
|
141
148
|
if not _ensure_runtime_config(settings):
|
|
142
149
|
return
|
|
150
|
+
|
|
151
|
+
if action == "start":
|
|
143
152
|
status = daemon.start(force=getattr(args, "force", False))
|
|
144
153
|
logger.info(status.message)
|
|
145
154
|
elif action == "stop":
|
|
@@ -153,12 +162,58 @@ def cmd_daemon(args: argparse.Namespace, settings: Settings) -> None:
|
|
|
153
162
|
logger.info(status.message)
|
|
154
163
|
if status.running:
|
|
155
164
|
logger.info("daemon pid=%s log=%s", status.pid, status.log_file)
|
|
165
|
+
autostart = daemon.autostart_status()
|
|
166
|
+
logger.info("%s (%s)", autostart.message, autostart.target)
|
|
167
|
+
_print_last_run_summary(settings)
|
|
156
168
|
elif action == "logs":
|
|
157
169
|
content = daemon.read_log_tail(lines=args.lines)
|
|
158
170
|
if content:
|
|
159
171
|
print(content, end="")
|
|
160
172
|
else:
|
|
161
173
|
logger.info("No daemon log output yet: %s", daemon.log_file)
|
|
174
|
+
elif action == "autostart-enable":
|
|
175
|
+
status = daemon.enable_autostart()
|
|
176
|
+
logger.info(status.message)
|
|
177
|
+
elif action == "autostart-disable":
|
|
178
|
+
status = daemon.disable_autostart()
|
|
179
|
+
logger.info(status.message)
|
|
180
|
+
elif action == "autostart-status":
|
|
181
|
+
status = daemon.autostart_status()
|
|
182
|
+
logger.info("%s (%s)", status.message, status.target)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _build_run_summary(results: list[TaskResult], *, trigger: str) -> RunSummary:
|
|
186
|
+
finished_values = [item.finished_at for item in results if item.finished_at is not None]
|
|
187
|
+
success = all(item.success for item in results)
|
|
188
|
+
message = None
|
|
189
|
+
if not success:
|
|
190
|
+
errors = [f"{item.task_name.value}: {item.error_message}" for item in results if item.error_message]
|
|
191
|
+
message = " | ".join(errors) if errors else "存在任务失败,请检查日志。"
|
|
192
|
+
return RunSummary(
|
|
193
|
+
trigger=trigger,
|
|
194
|
+
success=success,
|
|
195
|
+
started_at=min((item.started_at for item in results), default=datetime.now()),
|
|
196
|
+
finished_at=max(finished_values) if finished_values else None,
|
|
197
|
+
task_count=len(results),
|
|
198
|
+
rows_read=sum(item.rows_read for item in results),
|
|
199
|
+
rows_changed=sum(item.rows_changed for item in results),
|
|
200
|
+
rows_error=sum(item.rows_error for item in results),
|
|
201
|
+
tasks=results,
|
|
202
|
+
message=message,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _print_last_run_summary(settings: Settings) -> None:
|
|
207
|
+
summary = _build_state_service(settings).load_last_run(quiet=True)
|
|
208
|
+
if summary is None:
|
|
209
|
+
return
|
|
210
|
+
status = "成功" if summary.success else "失败"
|
|
211
|
+
logger.info(
|
|
212
|
+
"最近一次执行:%s trigger=%s read=%d changed=%d errors=%d",
|
|
213
|
+
status, summary.trigger, summary.rows_read, summary.rows_changed, summary.rows_error,
|
|
214
|
+
)
|
|
215
|
+
if summary.message:
|
|
216
|
+
logger.info("最近失败信息:%s", summary.message)
|
|
162
217
|
|
|
163
218
|
|
|
164
219
|
def _print_result(result: TaskResult) -> None:
|
|
@@ -168,6 +223,8 @@ def _print_result(result: TaskResult) -> None:
|
|
|
168
223
|
status, result.task_name.value, result.rows_read,
|
|
169
224
|
result.rows_changed, result.rows_error, result.dry_run,
|
|
170
225
|
)
|
|
226
|
+
if result.error_message:
|
|
227
|
+
logger.error("%s failed: %s", result.task_name.value, result.error_message)
|
|
171
228
|
|
|
172
229
|
|
|
173
230
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -206,6 +263,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
206
263
|
daemon_sub.add_parser("status", help="查看后台守护状态")
|
|
207
264
|
daemon_logs = daemon_sub.add_parser("logs", help="查看后台日志")
|
|
208
265
|
daemon_logs.add_argument("--lines", type=int, default=40, help="显示最后 N 行日志")
|
|
266
|
+
daemon_sub.add_parser("autostart-enable", help="启用登录自启")
|
|
267
|
+
daemon_sub.add_parser("autostart-disable", help="停用登录自启")
|
|
268
|
+
daemon_sub.add_parser("autostart-status", help="查看登录自启状态")
|
|
209
269
|
|
|
210
270
|
setup_parser = sub.add_parser("setup", aliases=["config"], help="交互式配置向导")
|
|
211
271
|
setup_parser.add_argument("--check", action="store_true", help="验证当前配置状态")
|
|
@@ -219,12 +279,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
219
279
|
def main(argv: list[str] | None = None) -> None:
|
|
220
280
|
"""CLI main entry point."""
|
|
221
281
|
argv = list(sys.argv[1:] if argv is None else argv)
|
|
222
|
-
if not argv:
|
|
223
|
-
argv = ["menu"]
|
|
224
|
-
|
|
225
282
|
settings = get_settings()
|
|
226
283
|
setup_logging(level=settings.log_level, log_dir=settings.state_dir)
|
|
227
284
|
|
|
285
|
+
if not argv:
|
|
286
|
+
argv = ["menu"] if has_required_runtime_config(settings) else ["setup"]
|
|
287
|
+
|
|
228
288
|
parser = build_parser()
|
|
229
289
|
args = parser.parse_args(argv)
|
|
230
290
|
|