tb-order-sync 0.3.0 → 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 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=false
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,41 @@
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
+
28
+ ## [0.3.1] - 2026-03-19
29
+
30
+ ### Added
31
+
32
+ - Added README badges for npm, GitHub, and license visibility.
33
+ - Added npm metadata fields for repository, homepage, and issue tracking.
34
+
35
+ ### Changed
36
+
37
+ - Improved npm package description and keywords to better reflect order sync, refund workflow, and spreadsheet automation.
38
+ - Prepared GitHub release metadata for the public `tb-order-sync` repository.
39
+
5
40
  ## [0.3.0] - 2026-03-19
6
41
 
7
42
  ### Added
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SOULRAi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,129 +1,112 @@
1
1
  # TB Order Sync
2
2
 
3
- **多表格同步与退款标记服务** — 自动同步腾讯文档 / 飞书多表格数据,计算毛利并标记退款订单。
3
+ [![npm version](https://img.shields.io/npm/v/tb-order-sync)](https://www.npmjs.com/package/tb-order-sync)
4
+ [![GitHub Repo](https://img.shields.io/badge/GitHub-SOULRAi%2Ftb--order--sync-181717?logo=github)](https://github.com/SOULRAi/tb-order-sync)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/SOULRAi/tb-order-sync/blob/main/LICENSE)
4
6
 
5
- > 一键启动、零配置门槛、支持 Windows / macOS 双平台,可打包为独立 exe 免安装分发。
7
+ **多表格同步与退款标记服务**
8
+ 面向订单运营场景的轻量自动化服务,负责同步云表格、计算毛利、标记退款状态,并支持后台守护运行。
6
9
 
7
- ---
10
+ > 支持 `tb` 短命令、Rich 控制台、启动自检、守护进程、登录自启、Windows / macOS 一键启动,以及后续接入飞书 C 表的扩展路径。
8
11
 
9
- ## 功能概览
12
+ **快速导航**
10
13
 
11
- | 功能 | 说明 | 状态 |
14
+ - [🚀 快速开始](#quick-start)
15
+ - [✨ 核心能力](#capabilities)
16
+ - [🔐 API 获取指引](#api-guide)
17
+ - [🧠 业务规则](#business-rules)
18
+ - [🧩 架构设计](#architecture)
19
+ - [🛡️ 守护进程](#daemon)
20
+ - [⚙️ 配置说明](#configuration)
21
+ - [🧪 测试](#testing)
22
+
23
+ <a id="capabilities"></a>
24
+ ## ✨ 核心能力
25
+
26
+ | 能力 | 说明 | 状态 |
12
27
  |------|------|------|
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` 命令
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
+ ### 推荐命令
29
44
 
30
45
  ```bash
31
- # 在项目根目录执行一次
32
- npm link
46
+ npm install -g tb-order-sync
33
47
 
34
- # 之后直接使用
35
48
  tb
36
- tb menu
37
49
  tb setup
50
+ tb check
51
+ tb all --dry-run
38
52
  tb all
39
- tb gp
40
- tb rm
41
53
  tb daemon start
42
54
  tb daemon status
43
55
  ```
44
56
 
45
- 如果你要生成可分发的 npm 包:
57
+ 首次运行说明:
58
+ - 如果本机还没有完整配置,直接执行 `tb` 会自动进入 `setup`
59
+ - `tb check` 会执行启动自检,确认状态目录、A 表、B 表是否可用
46
60
 
47
- ```bash
48
- npm pack
49
- npm install -g ./tb-order-sync-0.3.0.tgz
50
- ```
51
-
52
- ### 方式一:一键启动(推荐)
61
+ ### 启动方式
53
62
 
54
- **无需任何技术背景,双击即用。**
63
+ | 方式 | 适用场景 | 入口 |
64
+ |------|----------|------|
65
+ | `tb` 命令 | 开发、运维、长期使用 | `tb` / `tb setup` / `tb all` |
66
+ | 双击启动 | 非技术用户 | `启动.bat` / `启动.command` |
67
+ | Python 兼容入口 | 调试或源码环境 | `python main.py` |
68
+ | 打包分发 | 免安装交付 | `dist/sync_service/` |
55
69
 
56
- | 平台 | 操作 |
57
- |------|------|
58
- | **Windows** | 双击 `启动.bat` |
59
- | **macOS** | 双击 `启动.command` |
60
-
61
- 首次运行会自动完成:
62
- 1. 检测 / 下载 Python 环境(如果没有)
63
- 2. 安装依赖
64
- 3. 进入 Rich 控制台
65
- 4. 在控制台中继续配置、执行任务、管理守护进程
70
+ ### 常用命令速查
66
71
 
67
72
  ```bash
68
- tb
69
- # 或直接双击启动脚本
70
- # 默认进入 Rich 交互式控制台
71
- ```
72
-
73
- ### 方式二:命令行
74
-
75
- ```bash
76
- # 推荐入口
73
+ # 控制台
77
74
  tb
78
75
  tb menu
79
76
 
80
- # 交互式配置
77
+ # 配置
81
78
  tb setup
82
- tb config
83
-
84
- # 验证配置
85
79
  tb check
86
- tb setup --check
87
80
 
88
- # 执行任务
81
+ # 执行
89
82
  tb all
90
83
  tb gp
91
84
  tb rm
92
85
  tb all --dry-run
93
86
  tb all --mode full
94
- tb gp --mode full
95
87
 
96
- # 定时调度
88
+ # 调度 / 守护
97
89
  tb start
98
- tb schedule
99
-
100
- # 后台守护
101
90
  tb daemon start
102
91
  tb daemon status
103
92
  tb daemon logs --lines 80
104
93
  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
94
+ tb daemon autostart-enable
95
+ tb daemon autostart-status
96
+ tb daemon autostart-disable
112
97
  ```
113
98
 
114
- ### 方式三:打包为 exe 分发
99
+ ### 双击启动
115
100
 
116
- ```bash
117
- # 构建(在目标平台上执行)
118
- pip install pyinstaller
119
- python build.py --clean
101
+ | 平台 | 文件 |
102
+ |------|------|
103
+ | Windows | `启动.bat` |
104
+ | macOS | `启动.command` |
120
105
 
121
- # 产物在 dist/sync_service/ 目录
122
- # 连同 启动.bat(Windows)或 启动.command(macOS)一起分发
123
- # 用户无需安装 Python
124
- ```
106
+ 首次运行会自动补齐 Python 运行环境;如果本机尚未配置,会直接进入配置向导。
125
107
 
126
- ## API 获取指引
108
+ <a id="api-guide"></a>
109
+ ## 🔐 API 获取指引
127
110
 
128
111
  ### 腾讯文档 API
129
112
 
@@ -136,8 +119,9 @@ python build.py --clean
136
119
  4. 再回到本项目执行 `tb setup`
137
120
  - 当前说明:
138
121
  - 本项目 MVP 目前依赖你手工提供有效 `Access Token`
139
- - `Open ID` 先作为可选项保留
140
- - 腾讯文档实际 endpoint / request schema 代码里仍有 `TODO / NEED_VERIFY`
122
+ - 当前运行链路要求 `Client ID + Open ID + Access Token`
123
+ - `Client Secret` 目前保留为可选项,后续接自动刷新 token 时再使用
124
+ - 在线表格 v3 读写链路已经完成真实联调验证
141
125
 
142
126
  ### 飞书 API
143
127
 
@@ -153,7 +137,8 @@ python build.py --clean
153
137
  - 本项目里的飞书 connector 目前还是 skeleton
154
138
  - 现阶段向导里先采集配置,为后续 C 表接入做准备
155
139
 
156
- ## 业务规则
140
+ <a id="business-rules"></a>
141
+ ## 🧠 业务规则
157
142
 
158
143
  ### A 表结构(订单表)
159
144
 
@@ -187,11 +172,11 @@ python build.py --clean
187
172
 
188
173
  ### 退款匹配
189
174
 
190
- - B 表 A 列单号存在于 A 表 H 列 → I 列写入 `进入退款流程`
175
+ - B 表 A 列单号存在于 A 表 H 列 → I 列写入 `已退款`
191
176
  - 不存在 → 清空 I 列(同步取消)
192
- - 可选:`ENABLE_STYLE_UPDATE=true` 时额外设置行背景色标红
177
+ - `ENABLE_STYLE_UPDATE=true` 时会把匹配行整行改成红色文字
193
178
 
194
- ## 项目结构
179
+ ## 🗂️ 项目结构
195
180
 
196
181
  ```
197
182
  tb-order-sync/
@@ -251,7 +236,8 @@ tb-order-sync/
251
236
  └── test_refund_match_service.py # 6 tests
252
237
  ```
253
238
 
254
- ## 架构设计
239
+ <a id="architecture"></a>
240
+ ## 🧩 架构设计
255
241
 
256
242
  ```
257
243
  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
@@ -276,14 +262,15 @@ tb-order-sync/
276
262
  - **配置化** — 列映射、业务文案、运行模式全部可通过 `.env` 配置
277
263
  - **可扩展** — 新增 D 表、E 表时无需重构主程序
278
264
 
279
- ## 增量策略
265
+ ## 🔄 增量策略
280
266
 
281
267
  | 目标 | 策略 | 存储 |
282
268
  |------|------|------|
283
269
  | A 表毛利 | 对每行关键字段 (C/D/E/F/H) 生成 MD5 指纹,仅处理指纹变化行 | `state/sync_state.json` |
284
- | B 表退款 | 对退款单号集合生成整体 hash,集合不变则跳过 | `state/sync_state.json` |
270
+ | B 表退款 | 同时比较退款集合 hash 和 A 表单号/退款状态扫描 hash,避免漏处理 A 表变化 | `state/sync_state.json` |
285
271
 
286
- ## 守护进程
272
+ <a id="daemon"></a>
273
+ ## 🛡️ 守护进程
287
274
 
288
275
  - `tb daemon start`
289
276
  - 在后台启动定时调度进程
@@ -296,18 +283,32 @@ tb-order-sync/
296
283
  - 停止后台调度
297
284
  - `tb daemon restart`
298
285
  - 重启后台调度
286
+ - `tb daemon autostart-enable`
287
+ - 启用当前用户登录自启
288
+ - `tb daemon autostart-status`
289
+ - 查看登录自启状态
290
+ - `tb daemon autostart-disable`
291
+ - 关闭登录自启
299
292
 
300
293
  后台控制台输出默认写入:
301
294
  - `state/scheduler.console.log`
295
+ - `state/last_run.json`
296
+
297
+ 启动建议:
298
+ - 先执行 `tb check`,确认配置和连接正常
299
+ - 再执行 `tb daemon start`
300
+ - 如需电脑登录后自动运行,再执行 `tb daemon autostart-enable`
302
301
 
303
- ## 配置说明
302
+ <a id="configuration"></a>
303
+ ## ⚙️ 配置说明
304
304
 
305
305
  运行 `tb setup` 可交互式完成所有配置。也可手动编辑 `.env`:
306
306
 
307
307
  ```ini
308
308
  # 腾讯文档凭证
309
309
  TENCENT_CLIENT_ID=your_client_id
310
- TENCENT_CLIENT_SECRET=your_client_secret
310
+ TENCENT_CLIENT_SECRET=
311
+ TENCENT_OPEN_ID=your_open_id
311
312
  TENCENT_ACCESS_TOKEN=your_access_token
312
313
  TENCENT_A_FILE_ID=a_table_file_id
313
314
  TENCENT_A_SHEET_ID=a_table_sheet_id
@@ -319,7 +320,7 @@ GROSS_PROFIT_MODE=incremental # incremental | full
319
320
  REFUND_MATCH_MODE=incremental # incremental | full
320
321
  TASK_INTERVAL_MINUTES=10 # 定时调度间隔
321
322
  DRY_RUN=false # true = 模拟执行不写入
322
- ENABLE_STYLE_UPDATE=false # true = 退款行标红
323
+ ENABLE_STYLE_UPDATE=true # true = 退款行标红
323
324
 
324
325
  # 列映射(可自定义)
325
326
  A_COL_PRODUCT_PRICE=C
@@ -332,12 +333,18 @@ A_COL_REFUND_STATUS=I
332
333
  B_COL_ORDER_NO=A
333
334
  ```
334
335
 
336
+ 补充说明:
337
+ - `tb setup` 支持直接粘贴腾讯文档完整链接,自动拆出 `File ID / Sheet ID`
338
+ - `tb check` 会做启动自检,不只是看 `.env` 是否存在
339
+ - 当前退款高亮效果是“整行红色文字”,不是背景填充
340
+
335
341
  完整配置项见 [.env.example](.env.example)。
336
342
 
337
- ## 测试
343
+ <a id="testing"></a>
344
+ ## 🧪 测试
338
345
 
339
346
  ```bash
340
- # 运行全部测试(35 tests)
347
+ # 运行全部测试(49 tests)
341
348
  pytest tests/ -v
342
349
 
343
350
  # 单独运行
@@ -346,7 +353,7 @@ pytest tests/test_gross_profit_service.py -v
346
353
  pytest tests/test_refund_match_service.py -v
347
354
  ```
348
355
 
349
- ## 技术栈
356
+ ## 🧱 技术栈
350
357
 
351
358
  | 组件 | 用途 |
352
359
  |------|------|
@@ -360,15 +367,15 @@ pytest tests/test_refund_match_service.py -v
360
367
  | PyInstaller | 打包为可执行文件 |
361
368
  | pytest | 单元测试 |
362
369
 
363
- ## 部署方式对比
370
+ ## 📦 部署方式对比
364
371
 
365
372
  | 方式 | 需要 Python | 适用场景 |
366
373
  |------|------------|---------|
367
- | 双击启动脚本(源码) | 自动下载 | 开发/内部使用 |
368
- | 打包 exe 分发 | 不需要 | 给非技术用户 |
374
+ | 双击启动脚本(源码) | 自动下载 / 初始化 | 开发/内部使用 |
375
+ | 打包分发(Windows / macOS) | 不需要 | 给非技术用户 |
369
376
  | 命令行直接运行 | 需要 | 开发者 |
370
377
 
371
- ## Roadmap
378
+ ## 🗺️ Roadmap
372
379
 
373
380
  - [x] 毛利自动计算
374
381
  - [x] 退款自动匹配
@@ -381,12 +388,12 @@ pytest tests/test_refund_match_service.py -v
381
388
  - [ ] 腾讯文档 OAuth2 token 自动刷新
382
389
  - [ ] 腾讯文档行样式 API 验证
383
390
 
384
- ## 已知待确认项
391
+ ## ⚠️ 已知待确认项
385
392
 
386
393
  - 腾讯文档 Open API 的实际 endpoint 和 request/response 格式(代码中标注 `TODO / NEED_VERIFY`)
387
394
  - 腾讯文档是否支持通过 API 设置单元格样式
388
395
  - OAuth2 token 自动刷新流程
389
396
 
390
- ## License
397
+ ## 📄 License
391
398
 
392
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
- # Copy .env.example to dist folder for convenience
53
- example = ROOT / ".env.example"
54
- if example.exists():
55
- shutil.copy2(example, output / ".env.example")
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.tencent_client_secret,
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 == "start":
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