tb-order-sync 0.4.2 → 0.4.5

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
@@ -16,8 +16,12 @@ TENCENT_OPEN_ID=
16
16
  TENCENT_ACCESS_TOKEN=
17
17
  TENCENT_A_FILE_ID=
18
18
  TENCENT_A_SHEET_ID=
19
+ # 可选:填写后会在 A 表文件中按标题关键字自动选择“最新月份”工作表
20
+ TENCENT_A_SHEET_NAME_KEYWORD=
19
21
  TENCENT_B_FILE_ID=
20
22
  TENCENT_B_SHEET_ID=
23
+ # 可选:填写后会在 B 表文件中按标题关键字自动选择“最新月份”工作表
24
+ TENCENT_B_SHEET_NAME_KEYWORD=
21
25
 
22
26
  # ── 飞书配置(预留) ─────────────────────────────────────
23
27
  FEISHU_APP_ID=
package/CHANGELOG.md CHANGED
@@ -2,6 +2,59 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.4.5] - 2026-03-20
6
+
7
+ ### 新增
8
+
9
+ - 在 CLI 首页和配置向导的 LOGO 右下角新增版本号展示,便于直接确认当前运行版本。
10
+
11
+ ### 变更
12
+
13
+ - 变更发布构建内容:打包产物现在会一并包含 `package.json`,保证 macOS / Windows 分发包中的 CLI 也能正确显示版本号。
14
+ - 变更 README 的分发包说明,统一 macOS 资产命名为 `tb-order-sync-macos-x64-<version>.zip`。
15
+
16
+ ### 修复
17
+
18
+ - 修复 `tb setup` 链接选择菜单的残留交互问题:现在支持 `0`、空回车、`/skip`、`q` 等跳过方式,并在输入流中断时安全退出,不再抛出 `EOFError` 栈追踪。
19
+
20
+ ## [0.4.4] - 2026-03-20
21
+
22
+ ### 修复
23
+
24
+ - 修复 `tb setup` 在 Windows 环境下输入不稳定的问题:配置向导现统一使用原生 `input()` 读取,避免官网链接打开后无法继续输入、无法输入字母数字、无法粘贴的问题。
25
+ - 修复 `tb setup` 的官网链接打开交互体验:保持数字菜单方式,支持跳过、打开全部、打开单个链接,并增强 Windows 下的浏览器启动回退逻辑。
26
+ - 修复 Windows 双击 `启动.bat` 可能直接闪退的问题:补上缺失的 `:menu` 标签,并修正 Python / EXE 命令拼接与失败停留逻辑。
27
+
28
+ ## [0.4.3] - 2026-03-20
29
+
30
+ ### 新增
31
+
32
+ - 新增按工作表标题关键字自动选择“最新月份” sheet 的能力,可分别作用于 A 表和 B 表。
33
+ - 新增工作表月份解析工具,支持以下标题格式:
34
+ - `2026年3月毛利率`
35
+ - `2026-03 毛利率`
36
+ - `2026/03 客户退款`
37
+ - `3月毛利率`
38
+ - 新增 `TENCENT_A_SHEET_NAME_KEYWORD` / `TENCENT_B_SHEET_NAME_KEYWORD` 两个可选配置项。
39
+ - 新增月份选择相关单元测试,覆盖同年和跨年场景。
40
+
41
+ ### 变更
42
+
43
+ - 变更毛利计算与退款匹配服务:当配置了 sheet 标题关键字后,运行时会优先选择匹配关键字的最新月份工作表,而不是只依赖固定 `sheet_id`。
44
+ - 变更 `tb setup` 与 `tb check`:配置向导和启动自检现已支持并展示自动月表选择逻辑。
45
+ - 变更 CLI 控制台视觉风格:首页新增 LOGO、状态徽章和更统一的配色。
46
+ - 变更 CLI 次级页面视觉风格:执行结果、失败详情、守护结果、后台日志、配置未完成提示,统一为一致的模态面板样式。
47
+
48
+ ### 修复
49
+
50
+ - 修复跨年月份选择歧义:当标题中包含年份时,优先按“年 + 月”判断最新工作表,而不是只比较月份数字。
51
+ - 修复本地 CLI 体验不一致问题,使首页与二级页面在视觉上保持统一。
52
+ - 修复 `tb setup` 中官网链接打开交互不明确的问题:现已改为数字菜单,支持打开全部、打开单个链接或暂时跳过。
53
+ - 修复配置向导中凭证输入不可直接粘贴的问题:`Client ID` / `Open ID` / `Access Token` 等输入现改为可见输入,支持直接粘贴。
54
+ - 修复配置向导中无法暂时跳过某一项的问题:现可输入 `/skip` 暂时跳过,后续再补充配置。
55
+ - 修复 Windows 双击 `启动.bat` 可能直接闪退的问题:补上缺失的 `:menu` 标签,并修正 Python / EXE 启动命令拼接方式。
56
+ - 修复 Windows 打包环境下 setup 输入不稳定的问题:现将配置向导输入统一切回原生 `input()`,避免链接打开后无法继续输入或无法粘贴。
57
+
5
58
  ## [0.4.2] - 2026-03-19
6
59
 
7
60
  ### Added
package/README.md CHANGED
@@ -78,7 +78,7 @@ tb daemon status
78
78
 
79
79
  GitHub Release 现已提供标准完整分发包:
80
80
  - Windows: `tb-order-sync-windows-x64-<version>.zip`
81
- - macOS: `tb-order-sync-macos-bootstrap-<version>.zip`
81
+ - macOS: `tb-order-sync-macos-x64-<version>.zip`
82
82
 
83
83
  ### 常用命令速查
84
84
 
@@ -327,8 +327,10 @@ TENCENT_OPEN_ID=your_open_id
327
327
  TENCENT_ACCESS_TOKEN=your_access_token
328
328
  TENCENT_A_FILE_ID=a_table_file_id
329
329
  TENCENT_A_SHEET_ID=a_table_sheet_id
330
+ TENCENT_A_SHEET_NAME_KEYWORD=毛利率
330
331
  TENCENT_B_FILE_ID=b_table_file_id
331
332
  TENCENT_B_SHEET_ID=b_table_sheet_id
333
+ TENCENT_B_SHEET_NAME_KEYWORD=客户退款
332
334
 
333
335
  # 运行模式
334
336
  GROSS_PROFIT_MODE=incremental # incremental | full
@@ -350,6 +352,7 @@ B_COL_ORDER_NO=A
350
352
 
351
353
  补充说明:
352
354
  - `tb setup` 支持直接粘贴腾讯文档完整链接,自动拆出 `File ID / Sheet ID`
355
+ - 如果填写 `TENCENT_A_SHEET_NAME_KEYWORD` / `TENCENT_B_SHEET_NAME_KEYWORD`,系统会在对应文件里自动选取标题中匹配关键字的“最新月份”工作表
353
356
  - `tb check` 会做启动自检,不只是看 `.env` 是否存在
354
357
  - 当前退款高亮效果是“整行红色文字”,不是背景填充
355
358
 
package/build.py CHANGED
@@ -18,6 +18,7 @@ SPEC_FILE = ROOT / "sync_service.spec"
18
18
  DIST_DIR = ROOT / "dist"
19
19
  BUILD_DIR = ROOT / "build"
20
20
  DIST_RUNTIME_FILES = [
21
+ "package.json",
21
22
  ".env.example",
22
23
  "启动.bat",
23
24
  "启动.command",
package/cli/dashboard.py CHANGED
@@ -14,10 +14,25 @@ from rich.panel import Panel
14
14
  from rich.table import Table
15
15
  from rich.text import Text
16
16
 
17
- from config.settings import Settings, get_settings
17
+ from config.settings import APP_VERSION, Settings, get_settings
18
18
  from services.daemon_service import DaemonService
19
19
  from services.state_service import StateService
20
20
 
21
+ _LOGO_LINES = [
22
+ ("████████╗██████╗ ██████╗ ██████╗ ██████╗ ███████╗██████╗", "bold #8ecae6"),
23
+ ("╚══██╔══╝██╔══██╗ ██╔═══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗", "bold #6ccff6"),
24
+ (" ██║ ██████╔╝ ██║ ██║██████╔╝██║ ██║█████╗ ██████╔╝", "bold #38bdf8"),
25
+ (" ██║ ██╔══██╗ ██║ ██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗", "bold #22d3ee"),
26
+ (" ██║ ██████╔╝ ╚██████╔╝██║ ██║██████╔╝███████╗██║ ██║", "bold #2dd4bf"),
27
+ (" ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝", "bold #86efac"),
28
+ ]
29
+ _MODAL_ICONS = {
30
+ "success": ("●", "#10b981"),
31
+ "warning": ("●", "#f59e0b"),
32
+ "error": ("●", "#ef4444"),
33
+ "info": ("●", "#38bdf8"),
34
+ }
35
+
21
36
 
22
37
  class DashboardApp:
23
38
  """Interactive terminal UI for daily operations."""
@@ -57,38 +72,35 @@ class DashboardApp:
57
72
  last_run = self._state_svc.load_last_run(quiet=True)
58
73
  autostart_status = self._daemon.autostart_status()
59
74
 
60
- title = Text("多表格同步与退款标记服务", style="bold white")
61
- subtitle = Text("Scheduler Console", style="bold #8ecae6")
62
- header = Panel(
63
- Align.center(Group(title, subtitle)),
64
- border_style="#219ebc",
65
- box=box.HEAVY,
66
- padding=(1, 2),
67
- )
75
+ header = self._build_header(daemon_status, last_run)
68
76
 
69
77
  runtime_panel = Panel(
70
78
  self._build_runtime_table(),
71
79
  title="[bold #023047]运行配置[/bold #023047]",
72
80
  border_style="#8ecae6",
73
81
  box=box.ROUNDED,
82
+ padding=(1, 2),
74
83
  )
75
84
  daemon_panel = Panel(
76
85
  self._build_daemon_table(daemon_status, autostart_status),
77
86
  title="[bold #023047]守护进程[/bold #023047]",
78
87
  border_style="#90be6d" if daemon_status.running else "#f4a261",
79
88
  box=box.ROUNDED,
89
+ padding=(1, 2),
80
90
  )
81
91
  state_panel = Panel(
82
92
  self._build_state_table(state, last_run),
83
93
  title="[bold #023047]同步状态[/bold #023047]",
84
94
  border_style="#ffb703",
85
95
  box=box.ROUNDED,
96
+ padding=(1, 2),
86
97
  )
87
98
  config_panel = Panel(
88
99
  self._build_config_table(),
89
100
  title="[bold #023047]接入状态[/bold #023047]",
90
101
  border_style="#fb8500" if self._is_config_ready() else "#d62828",
91
102
  box=box.ROUNDED,
103
+ padding=(1, 2),
92
104
  )
93
105
 
94
106
  actions = Panel(
@@ -96,6 +108,7 @@ class DashboardApp:
96
108
  title="[bold #023047]操作台[/bold #023047]",
97
109
  border_style="#219ebc",
98
110
  box=box.ROUNDED,
111
+ padding=(1, 2),
99
112
  )
100
113
 
101
114
  footer = Panel(
@@ -116,6 +129,63 @@ class DashboardApp:
116
129
  footer,
117
130
  )
118
131
 
132
+ def _build_header(self, daemon_status, last_run) -> Panel:
133
+ logo = Text(justify="center")
134
+ for line, style in _LOGO_LINES:
135
+ logo.append(line, style=style)
136
+ logo.append("\n")
137
+ logo.append("Tencent Docs Order Sync Console", style="bold #e0fbfc")
138
+
139
+ hero = Group(
140
+ Align.center(logo),
141
+ Align.right(self._build_version_badge()),
142
+ )
143
+
144
+ badges = Table.grid(expand=True)
145
+ badges.add_column(justify="center")
146
+ badges.add_column(justify="center")
147
+ badges.add_column(justify="center")
148
+ badges.add_row(
149
+ self._build_badge("运行模式", f"{self._settings.gross_profit_mode.value} / {self._settings.refund_match_mode.value}", "#0ea5e9"),
150
+ self._build_badge("守护状态", "运行中" if daemon_status.running else "未运行", "#10b981" if daemon_status.running else "#f59e0b"),
151
+ self._build_badge("最近结果", self._last_run_label(last_run), "#22c55e" if last_run and last_run.success else "#ef4444" if last_run else "#64748b"),
152
+ )
153
+
154
+ subtitle = Text("多表格同步与退款标记服务", style="bold white", justify="center")
155
+ hint = Text("输入编号执行任务,所有结果与失败原因会在控制台内直接返回", style="dim", justify="center")
156
+
157
+ body = Group(
158
+ hero,
159
+ Align.center(subtitle),
160
+ Align.center(hint),
161
+ badges,
162
+ )
163
+ return Panel(
164
+ body,
165
+ border_style="#219ebc",
166
+ box=box.HEAVY,
167
+ padding=(1, 2),
168
+ )
169
+
170
+ @staticmethod
171
+ def _build_badge(label: str, value: str, color: str) -> Panel:
172
+ inner = Table.grid(padding=(0, 1))
173
+ inner.add_column(justify="center")
174
+ inner.add_row(Text(label, style="bold white"))
175
+ inner.add_row(Text(value, style="bold white"))
176
+ return Panel(inner, border_style=color, box=box.ROUNDED, padding=(0, 1))
177
+
178
+ @staticmethod
179
+ def _build_version_badge() -> Panel:
180
+ text = Text(f"v{APP_VERSION}", style="bold #0f172a", justify="center")
181
+ return Panel(text, border_style="#94d2bd", box=box.ROUNDED, padding=(0, 2), title="版本")
182
+
183
+ @staticmethod
184
+ def _last_run_label(last_run) -> str:
185
+ if last_run is None:
186
+ return "暂无记录"
187
+ return "成功" if last_run.success else "失败"
188
+
119
189
  def _build_runtime_table(self) -> Table:
120
190
  table = Table(box=None, show_header=False, pad_edge=False)
121
191
  table.add_column(style="bold white")
@@ -168,9 +238,9 @@ class DashboardApp:
168
238
  return table
169
239
 
170
240
  def _build_action_table(self) -> Table:
171
- table = Table(box=box.SIMPLE_HEAVY, expand=True)
172
- table.add_column("编号", justify="center", style="bold #219ebc", width=6)
173
- table.add_column("动作", style="bold white")
241
+ table = Table(box=box.SIMPLE_HEAVY, expand=True, row_styles=["none", "dim"])
242
+ table.add_column("编号", justify="center", style="bold #38bdf8", width=6)
243
+ table.add_column("动作", style="bold white", width=18)
174
244
  table.add_column("说明", style="#023047")
175
245
  table.add_row("1", "执行全部任务", "毛利计算 + 退款匹配")
176
246
  table.add_row("2", "模拟执行", "全部任务 dry-run,不写入表格")
@@ -228,7 +298,7 @@ class DashboardApp:
228
298
  from cli.commands import execute_tasks
229
299
 
230
300
  results = execute_tasks(self._settings, task, dry_run=dry_run)
231
- table = Table(box=box.SIMPLE_HEAVY, expand=True)
301
+ table = Table(box=box.SIMPLE_HEAVY, expand=True, row_styles=["none", "dim"])
232
302
  table.add_column("任务", style="bold cyan")
233
303
  table.add_column("结果", justify="center")
234
304
  table.add_column("读取", justify="right")
@@ -251,8 +321,19 @@ class DashboardApp:
251
321
  failure_table.add_column("失败原因", style="white")
252
322
  for item in failures:
253
323
  failure_table.add_row(item.task_name.value, item.error_message or "")
254
- body = Group(table, Panel(failure_table, title="失败详情", border_style="red", box=box.ROUNDED))
255
- self._pause_with_panel(Panel(body, title="执行结果", border_style="#219ebc", box=box.ROUNDED))
324
+ body = Group(
325
+ self._build_modal_summary("执行完成,但存在失败项", style="error"),
326
+ table,
327
+ Panel(failure_table, title="失败详情", border_style="red", box=box.ROUNDED, padding=(1, 2)),
328
+ )
329
+ self._pause_with_panel(body, title="执行结果", border_style="#ef4444")
330
+ return
331
+
332
+ body = Group(
333
+ self._build_modal_summary("执行完成,结果已落地", style="success"),
334
+ table,
335
+ )
336
+ self._pause_with_panel(body, title="执行结果", border_style="#219ebc")
256
337
 
257
338
  def _daemon_action(self, action: str) -> None:
258
339
  if action in {"start", "autostart-enable"} and not self._ensure_config():
@@ -271,14 +352,47 @@ class DashboardApp:
271
352
  else:
272
353
  status = self._daemon.autostart_status()
273
354
 
274
- self._pause_with_panel(Panel(status.message, title="守护结果", border_style="#90be6d", box=box.ROUNDED))
355
+ style = "success"
356
+ border = "#90be6d"
357
+ if "失败" in status.message or "未找到" in status.message:
358
+ style = "error"
359
+ border = "#ef4444"
360
+ elif "未启用" in status.message or "未运行" in status.message:
361
+ style = "warning"
362
+ border = "#f59e0b"
363
+
364
+ body = Group(
365
+ self._build_modal_summary(status.message, style=style),
366
+ self._build_kv_table({
367
+ "动作": action,
368
+ "目标": getattr(status, "target", "") or "-",
369
+ }),
370
+ )
371
+ self._pause_with_panel(body, title="守护结果", border_style=border)
275
372
 
276
373
  def _show_log_tail(self) -> None:
277
374
  content = self._daemon.read_log_tail(lines=40)
278
- body = content if content else "后台日志暂时为空。"
279
- self._pause_with_panel(
280
- Panel(body, title=f"后台日志 · {self._daemon.log_file.name}", border_style="#ffb703", box=box.ROUNDED)
281
- )
375
+ if content:
376
+ log_panel = Panel(
377
+ Text(content, style="#e5e7eb"),
378
+ title=f"后台日志 · {self._daemon.log_file.name}",
379
+ border_style="#ffb703",
380
+ box=box.ROUNDED,
381
+ padding=(1, 2),
382
+ )
383
+ body = Group(
384
+ self._build_modal_summary("以下为最近 40 行后台日志", style="info"),
385
+ log_panel,
386
+ )
387
+ else:
388
+ body = Group(
389
+ self._build_modal_summary("后台日志暂时为空", style="warning"),
390
+ self._build_kv_table({
391
+ "日志文件": self._daemon.log_file.name,
392
+ "日志目录": str(self._daemon.log_file.parent),
393
+ }),
394
+ )
395
+ self._pause_with_panel(body, title="后台日志", border_style="#ffb703")
282
396
 
283
397
  def _run_setup(self, *, check: bool) -> None:
284
398
  from cli.setup import cmd_setup
@@ -300,14 +414,14 @@ class DashboardApp:
300
414
  def _ensure_config(self) -> bool:
301
415
  if self._is_config_ready():
302
416
  return True
303
- self._pause_with_panel(
304
- Panel(
305
- "腾讯文档必填配置尚未完成,请先运行配置向导。",
306
- title="配置未完成",
307
- border_style="red",
308
- box=box.ROUNDED,
309
- )
417
+ body = Group(
418
+ self._build_modal_summary("腾讯文档必填配置尚未完成", style="error"),
419
+ self._build_kv_table({
420
+ "建议动作": "先运行 tb setup",
421
+ "后续检查": "配置完成后运行 tb check",
422
+ }),
310
423
  )
424
+ self._pause_with_panel(body, title="配置未完成", border_style="#ef4444")
311
425
  return False
312
426
 
313
427
  def _is_config_ready(self) -> bool:
@@ -322,11 +436,35 @@ class DashboardApp:
322
436
  ]
323
437
  return all(bool(value.strip()) for value in fields)
324
438
 
325
- def _pause_with_panel(self, panel: Panel) -> None:
439
+ def _pause_with_panel(self, body, *, title: str, border_style: str) -> None:
326
440
  self.console.clear()
327
- self.console.print(panel)
441
+ self.console.print(
442
+ Panel(
443
+ body,
444
+ title=title,
445
+ border_style=border_style,
446
+ box=box.ROUNDED,
447
+ padding=(1, 2),
448
+ )
449
+ )
328
450
  self._wait()
329
451
 
452
+ def _build_modal_summary(self, message: str, *, style: str) -> Panel:
453
+ icon, color = _MODAL_ICONS[style]
454
+ text = Text(justify="center")
455
+ text.append(f"{icon} ", style=f"bold {color}")
456
+ text.append(message, style="bold white")
457
+ return Panel(text, border_style=color, box=box.ROUNDED, padding=(0, 1))
458
+
459
+ @staticmethod
460
+ def _build_kv_table(rows: dict[str, str]) -> Table:
461
+ table = Table(box=box.SIMPLE_HEAVY, expand=True, row_styles=["none", "dim"])
462
+ table.add_column("项目", style="bold cyan", width=18)
463
+ table.add_column("内容", style="white")
464
+ for key, value in rows.items():
465
+ table.add_row(key, value)
466
+ return table
467
+
330
468
  def _wait(self) -> None:
331
469
  self.console.input("[dim]按回车返回控制台[/dim]")
332
470