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 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,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
  [![GitHub Repo](https://img.shields.io/badge/GitHub-SOULRAi%2Ftb--order--sync-181717?logo=github)](https://github.com/SOULRAi/tb-order-sync)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/SOULRAi/tb-order-sync/blob/main/LICENSE)
6
6
 
7
- **多表格同步与退款标记服务** — 自动同步腾讯文档 / 飞书多表格数据,计算毛利并标记退款订单。
7
+ **多表格同步与退款标记服务**
8
+ 面向订单运营场景的轻量自动化服务,负责同步云表格、计算毛利、标记退款状态,并支持后台守护运行。
8
9
 
9
- > 一键启动、零配置门槛、支持 Windows / macOS 双平台,可打包为独立 exe 免安装分发。
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
- | 增量同步 | 基于行指纹 (MD5) 的变更检测,只处理变化行 | ✅ 已实现 |
20
- | 全量同步 | 忽略缓存,全表重新计算 | ✅ 已实现 |
21
- | 定时调度 | APScheduler 定时执行,支持 jitter 防冲突 | ✅ 已实现 |
22
- | 交互式配置向导 | `setup` 命令一站式引导配置所有参数,并可直接打开官方文档链接 | ✅ 已实现 |
23
- | Rich 控制台 | 无参启动即进入可视化 CLI 控制台 | ✅ 已实现 |
24
- | 进程守护 | 支持后台启动 / 停止 / 状态 / 日志查看 | ✅ 已实现 |
25
- | 一键启动脚本 | 双击即用,自动初始化环境,无需手动装 Python | ✅ 已实现 |
26
- | 打包为 exe | PyInstaller 打包,免安装分发 | ✅ 已实现 |
27
- | dry-run 模式 | 模拟执行不写入,安全验证 | 已实现 |
28
- | 飞书 C 表同步 | C 表 → A 表数据同步 | 🔲 骨架已预留 |
29
-
30
- ## 快速开始
31
-
32
- ### 方式零:安装 `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
+ ### 推荐命令
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
- 如果你要生成可分发的 npm 包:
50
-
51
- ```bash
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
- ```bash
72
- tb
73
- # 或直接双击启动脚本
74
- # 默认进入 Rich 交互式控制台
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 restart
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
- ### 方式三:打包为 exe 分发
99
+ ### 双击启动
119
100
 
120
- ```bash
121
- # 构建(在目标平台上执行)
122
- pip install pyinstaller
123
- python build.py --clean
101
+ | 平台 | 文件 |
102
+ |------|------|
103
+ | Windows | `启动.bat` |
104
+ | macOS | `启动.command` |
124
105
 
125
- # 产物在 dist/sync_service/ 目录
126
- # 连同 启动.bat(Windows)或 启动.command(macOS)一起分发
127
- # 用户无需安装 Python
128
- ```
106
+ 首次运行会自动补齐 Python 运行环境;如果本机尚未配置,会直接进入配置向导。
129
107
 
130
- ## API 获取指引
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
- - 腾讯文档实际 endpoint / request schema 代码里仍有 `TODO / NEED_VERIFY`
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
- - 可选:`ENABLE_STYLE_UPDATE=true` 时额外设置行背景色标红
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 表退款 | 对退款单号集合生成整体 hash,集合不变则跳过 | `state/sync_state.json` |
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=your_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=false # true = 退款行标红
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
- # 运行全部测试(35 tests)
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
- | 打包 exe 分发 | 不需要 | 给非技术用户 |
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
- # 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