tb-order-sync 0.3.1 → 0.4.1

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,42 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.4.1] - 2026-03-19
6
+
7
+ ### Added
8
+
9
+ - Added standard GitHub Release packaging workflow for macOS and Windows.
10
+ - Added complete distribution package output including startup scripts, `.env.example`, `快速开始.txt`, and `公司同事使用说明.md`.
11
+
12
+ ### Changed
13
+
14
+ - Changed release packaging to produce platform-specific archives:
15
+ - `tb-order-sync-macos-x64-<version>.zip`
16
+ - `tb-order-sync-windows-x64-<version>.zip`
17
+
18
+ ## [0.4.0] - 2026-03-19
19
+
20
+ ### Added
21
+
22
+ - Added startup self-check in `tb check`, including state directory write validation and live Tencent Docs A/B sheet read checks.
23
+ - Added Tencent Docs URL parsing in setup so users can paste full sheet links and auto-fill `File ID` / `Sheet ID`.
24
+ - Added login auto-start support via `tb daemon autostart-enable|status|disable` for Windows Task Scheduler and macOS LaunchAgent.
25
+ - Added `state/last_run.json` execution summaries for manual and scheduled runs.
26
+ - Added packaged `快速开始.txt` to distribution outputs for non-technical users.
27
+
28
+ ### Changed
29
+
30
+ - Changed no-argument startup to enter `setup` automatically when required runtime config is missing.
31
+ - Changed config validation so `TENCENT_CLIENT_SECRET` is now optional for the current live Tencent Docs runtime path.
32
+ - Changed dashboard and daemon status output to surface recent run status and login auto-start state.
33
+ - Changed launcher and README onboarding to emphasize `tb check` as the first runtime self-check after setup.
34
+
35
+ ### Fixed
36
+
37
+ - Fixed Tencent Docs rate-limit handling by retrying `400007 Requests Over Limit` responses with exponential backoff.
38
+ - Fixed repeated batch writes being too aggressive by slowing inter-batch pacing.
39
+ - Fixed user-facing error reporting so failed tasks now include clearer cause hints in CLI output.
40
+
5
41
  ## [0.3.1] - 2026-03-19
6
42
 
7
43
  ### Added
package/README.md CHANGED
@@ -4,130 +4,113 @@
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 包:
57
+ 首次运行说明:
58
+ - 如果本机还没有完整配置,直接执行 `tb` 会自动进入 `setup`
59
+ - `tb check` 会执行启动自检,确认状态目录、A 表、B 表是否可用
50
60
 
51
- ```bash
52
- npm pack
53
- npm install -g ./tb-order-sync-0.3.1.tgz
54
- ```
55
-
56
- ### 方式一:一键启动(推荐)
61
+ ### 启动方式
57
62
 
58
- **无需任何技术背景,双击即用。**
63
+ | 方式 | 适用场景 | 入口 |
64
+ |------|----------|------|
65
+ | `tb` 命令 | 开发、运维、长期使用 | `tb` / `tb setup` / `tb all` |
66
+ | 双击启动 | 非技术用户 | `启动.bat` / `启动.command` |
67
+ | Python 兼容入口 | 调试或源码环境 | `python main.py` |
68
+ | 打包分发 | 免安装交付 | `dist/sync_service/` |
59
69
 
60
- | 平台 | 操作 |
61
- |------|------|
62
- | **Windows** | 双击 `启动.bat` |
63
- | **macOS** | 双击 `启动.command` |
70
+ GitHub Release 现已提供标准完整分发包:
71
+ - Windows: `tb-order-sync-windows-x64-<version>.zip`
72
+ - macOS: `tb-order-sync-macos-x64-<version>.zip`
64
73
 
65
- 首次运行会自动完成:
66
- 1. 检测 / 下载 Python 环境(如果没有)
67
- 2. 安装依赖
68
- 3. 进入 Rich 控制台
69
- 4. 在控制台中继续配置、执行任务、管理守护进程
74
+ ### 常用命令速查
70
75
 
71
76
  ```bash
72
- tb
73
- # 或直接双击启动脚本
74
- # 默认进入 Rich 交互式控制台
75
- ```
76
-
77
- ### 方式二:命令行
78
-
79
- ```bash
80
- # 推荐入口
77
+ # 控制台
81
78
  tb
82
79
  tb menu
83
80
 
84
- # 交互式配置
81
+ # 配置
85
82
  tb setup
86
- tb config
87
-
88
- # 验证配置
89
83
  tb check
90
- tb setup --check
91
84
 
92
- # 执行任务
85
+ # 执行
93
86
  tb all
94
87
  tb gp
95
88
  tb rm
96
89
  tb all --dry-run
97
90
  tb all --mode full
98
- tb gp --mode full
99
91
 
100
- # 定时调度
92
+ # 调度 / 守护
101
93
  tb start
102
- tb schedule
103
-
104
- # 后台守护
105
94
  tb daemon start
106
95
  tb daemon status
107
96
  tb daemon logs --lines 80
108
97
  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
98
+ tb daemon autostart-enable
99
+ tb daemon autostart-status
100
+ tb daemon autostart-disable
116
101
  ```
117
102
 
118
- ### 方式三:打包为 exe 分发
103
+ ### 双击启动
119
104
 
120
- ```bash
121
- # 构建(在目标平台上执行)
122
- pip install pyinstaller
123
- python build.py --clean
105
+ | 平台 | 文件 |
106
+ |------|------|
107
+ | Windows | `启动.bat` |
108
+ | macOS | `启动.command` |
124
109
 
125
- # 产物在 dist/sync_service/ 目录
126
- # 连同 启动.bat(Windows)或 启动.command(macOS)一起分发
127
- # 用户无需安装 Python
128
- ```
110
+ 首次运行会自动补齐 Python 运行环境;如果本机尚未配置,会直接进入配置向导。
129
111
 
130
- ## API 获取指引
112
+ <a id="api-guide"></a>
113
+ ## 🔐 API 获取指引
131
114
 
132
115
  ### 腾讯文档 API
133
116
 
@@ -140,8 +123,9 @@ python build.py --clean
140
123
  4. 再回到本项目执行 `tb setup`
141
124
  - 当前说明:
142
125
  - 本项目 MVP 目前依赖你手工提供有效 `Access Token`
143
- - `Open ID` 先作为可选项保留
144
- - 腾讯文档实际 endpoint / request schema 代码里仍有 `TODO / NEED_VERIFY`
126
+ - 当前运行链路要求 `Client ID + Open ID + Access Token`
127
+ - `Client Secret` 目前保留为可选项,后续接自动刷新 token 时再使用
128
+ - 在线表格 v3 读写链路已经完成真实联调验证
145
129
 
146
130
  ### 飞书 API
147
131
 
@@ -157,7 +141,8 @@ python build.py --clean
157
141
  - 本项目里的飞书 connector 目前还是 skeleton
158
142
  - 现阶段向导里先采集配置,为后续 C 表接入做准备
159
143
 
160
- ## 业务规则
144
+ <a id="business-rules"></a>
145
+ ## 🧠 业务规则
161
146
 
162
147
  ### A 表结构(订单表)
163
148
 
@@ -191,11 +176,11 @@ python build.py --clean
191
176
 
192
177
  ### 退款匹配
193
178
 
194
- - B 表 A 列单号存在于 A 表 H 列 → I 列写入 `进入退款流程`
179
+ - B 表 A 列单号存在于 A 表 H 列 → I 列写入 `已退款`
195
180
  - 不存在 → 清空 I 列(同步取消)
196
- - 可选:`ENABLE_STYLE_UPDATE=true` 时额外设置行背景色标红
181
+ - `ENABLE_STYLE_UPDATE=true` 时会把匹配行整行改成红色文字
197
182
 
198
- ## 项目结构
183
+ ## 🗂️ 项目结构
199
184
 
200
185
  ```
201
186
  tb-order-sync/
@@ -255,7 +240,8 @@ tb-order-sync/
255
240
  └── test_refund_match_service.py # 6 tests
256
241
  ```
257
242
 
258
- ## 架构设计
243
+ <a id="architecture"></a>
244
+ ## 🧩 架构设计
259
245
 
260
246
  ```
261
247
  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
@@ -280,14 +266,15 @@ tb-order-sync/
280
266
  - **配置化** — 列映射、业务文案、运行模式全部可通过 `.env` 配置
281
267
  - **可扩展** — 新增 D 表、E 表时无需重构主程序
282
268
 
283
- ## 增量策略
269
+ ## 🔄 增量策略
284
270
 
285
271
  | 目标 | 策略 | 存储 |
286
272
  |------|------|------|
287
273
  | A 表毛利 | 对每行关键字段 (C/D/E/F/H) 生成 MD5 指纹,仅处理指纹变化行 | `state/sync_state.json` |
288
- | B 表退款 | 对退款单号集合生成整体 hash,集合不变则跳过 | `state/sync_state.json` |
274
+ | B 表退款 | 同时比较退款集合 hash 和 A 表单号/退款状态扫描 hash,避免漏处理 A 表变化 | `state/sync_state.json` |
289
275
 
290
- ## 守护进程
276
+ <a id="daemon"></a>
277
+ ## 🛡️ 守护进程
291
278
 
292
279
  - `tb daemon start`
293
280
  - 在后台启动定时调度进程
@@ -300,18 +287,32 @@ tb-order-sync/
300
287
  - 停止后台调度
301
288
  - `tb daemon restart`
302
289
  - 重启后台调度
290
+ - `tb daemon autostart-enable`
291
+ - 启用当前用户登录自启
292
+ - `tb daemon autostart-status`
293
+ - 查看登录自启状态
294
+ - `tb daemon autostart-disable`
295
+ - 关闭登录自启
303
296
 
304
297
  后台控制台输出默认写入:
305
298
  - `state/scheduler.console.log`
299
+ - `state/last_run.json`
306
300
 
307
- ## 配置说明
301
+ 启动建议:
302
+ - 先执行 `tb check`,确认配置和连接正常
303
+ - 再执行 `tb daemon start`
304
+ - 如需电脑登录后自动运行,再执行 `tb daemon autostart-enable`
305
+
306
+ <a id="configuration"></a>
307
+ ## ⚙️ 配置说明
308
308
 
309
309
  运行 `tb setup` 可交互式完成所有配置。也可手动编辑 `.env`:
310
310
 
311
311
  ```ini
312
312
  # 腾讯文档凭证
313
313
  TENCENT_CLIENT_ID=your_client_id
314
- TENCENT_CLIENT_SECRET=your_client_secret
314
+ TENCENT_CLIENT_SECRET=
315
+ TENCENT_OPEN_ID=your_open_id
315
316
  TENCENT_ACCESS_TOKEN=your_access_token
316
317
  TENCENT_A_FILE_ID=a_table_file_id
317
318
  TENCENT_A_SHEET_ID=a_table_sheet_id
@@ -323,7 +324,7 @@ GROSS_PROFIT_MODE=incremental # incremental | full
323
324
  REFUND_MATCH_MODE=incremental # incremental | full
324
325
  TASK_INTERVAL_MINUTES=10 # 定时调度间隔
325
326
  DRY_RUN=false # true = 模拟执行不写入
326
- ENABLE_STYLE_UPDATE=false # true = 退款行标红
327
+ ENABLE_STYLE_UPDATE=true # true = 退款行标红
327
328
 
328
329
  # 列映射(可自定义)
329
330
  A_COL_PRODUCT_PRICE=C
@@ -336,12 +337,18 @@ A_COL_REFUND_STATUS=I
336
337
  B_COL_ORDER_NO=A
337
338
  ```
338
339
 
340
+ 补充说明:
341
+ - `tb setup` 支持直接粘贴腾讯文档完整链接,自动拆出 `File ID / Sheet ID`
342
+ - `tb check` 会做启动自检,不只是看 `.env` 是否存在
343
+ - 当前退款高亮效果是“整行红色文字”,不是背景填充
344
+
339
345
  完整配置项见 [.env.example](.env.example)。
340
346
 
341
- ## 测试
347
+ <a id="testing"></a>
348
+ ## 🧪 测试
342
349
 
343
350
  ```bash
344
- # 运行全部测试(35 tests)
351
+ # 运行全部测试(49 tests)
345
352
  pytest tests/ -v
346
353
 
347
354
  # 单独运行
@@ -350,7 +357,7 @@ pytest tests/test_gross_profit_service.py -v
350
357
  pytest tests/test_refund_match_service.py -v
351
358
  ```
352
359
 
353
- ## 技术栈
360
+ ## 🧱 技术栈
354
361
 
355
362
  | 组件 | 用途 |
356
363
  |------|------|
@@ -364,15 +371,15 @@ pytest tests/test_refund_match_service.py -v
364
371
  | PyInstaller | 打包为可执行文件 |
365
372
  | pytest | 单元测试 |
366
373
 
367
- ## 部署方式对比
374
+ ## 📦 部署方式对比
368
375
 
369
376
  | 方式 | 需要 Python | 适用场景 |
370
377
  |------|------------|---------|
371
- | 双击启动脚本(源码) | 自动下载 | 开发/内部使用 |
372
- | 打包 exe 分发 | 不需要 | 给非技术用户 |
378
+ | 双击启动脚本(源码) | 自动下载 / 初始化 | 开发/内部使用 |
379
+ | 打包分发(Windows / macOS) | 不需要 | 给非技术用户 |
373
380
  | 命令行直接运行 | 需要 | 开发者 |
374
381
 
375
- ## Roadmap
382
+ ## 🗺️ Roadmap
376
383
 
377
384
  - [x] 毛利自动计算
378
385
  - [x] 退款自动匹配
@@ -385,12 +392,12 @@ pytest tests/test_refund_match_service.py -v
385
392
  - [ ] 腾讯文档 OAuth2 token 自动刷新
386
393
  - [ ] 腾讯文档行样式 API 验证
387
394
 
388
- ## 已知待确认项
395
+ ## ⚠️ 已知待确认项
389
396
 
390
397
  - 腾讯文档 Open API 的实际 endpoint 和 request/response 格式(代码中标注 `TODO / NEED_VERIFY`)
391
398
  - 腾讯文档是否支持通过 API 设置单元格样式
392
399
  - OAuth2 token 自动刷新流程
393
400
 
394
- ## License
401
+ ## 📄 License
395
402
 
396
403
  MIT
package/build.py CHANGED
@@ -17,6 +17,13 @@ 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
+ "公司同事使用说明.md",
26
+ ]
20
27
 
21
28
 
22
29
  def clean() -> None:
@@ -49,10 +56,10 @@ def build() -> None:
49
56
 
50
57
  output = DIST_DIR / "sync_service"
51
58
  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")
59
+ for name in DIST_RUNTIME_FILES:
60
+ source = ROOT / name
61
+ if source.exists():
62
+ shutil.copy2(source, output / name)
56
63
 
57
64
  # Create state directory
58
65
  (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