tb-order-sync 0.4.2 → 0.4.6
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 +4 -0
- package/CHANGELOG.md +69 -0
- package/README.md +9 -6
- package/build.py +1 -0
- package/cli/dashboard.py +167 -29
- package/cli/setup.py +300 -148
- package/config/settings.py +23 -0
- package/connectors/tencent_docs.py +87 -0
- package/package.json +1 -1
- package/services/gross_profit_service.py +23 -6
- package/services/refund_match_service.py +43 -10
- package/sync_service.spec +1 -0
- package/utils/sheet_selector.py +125 -0
- package//345/220/257/345/212/250.bat +25 -6
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,75 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.6] - 2026-03-20
|
|
6
|
+
|
|
7
|
+
### 变更
|
|
8
|
+
|
|
9
|
+
- 简化 `tb setup` 配置向导:链接选择统一改为“输入编号打开,直接回车跳过”,不再显示 `0` 和 `/skip` 等额外选项。
|
|
10
|
+
- 简化腾讯文档凭证录入:向导现只要求填写 `Client ID`、`Open ID`、`Access Token`,不再弹出 `Client Secret` 输入。
|
|
11
|
+
- 简化表格配置:A / B 表现改为直接粘贴完整腾讯文档链接并自动解析,不再要求用户手动输入 `File ID`。
|
|
12
|
+
- 简化配置流程:隐藏飞书配置、运行环境、日志级别、C 表同步模式等当前不用的项目,减少新手初始化负担。
|
|
13
|
+
- 优化中文文案:`A表(订单表)` 改为 `A表(订单表/毛利率表)`,`A/B 表按名称自动选最新月份` 改为 `A/B表表格关键字匹配`。
|
|
14
|
+
- 优化运行模式选择:毛利计算模式和退款匹配模式现改为中文二选一说明,不再直接显示英文枚举。
|
|
15
|
+
|
|
16
|
+
### 修复
|
|
17
|
+
|
|
18
|
+
- 修复配置向导帮助说明过于技术化的问题,改成小白可理解的“打开表格 -> 复制完整链接 -> 粘贴自动解析”流程。
|
|
19
|
+
- 修复 README 与当前向导行为不一致的问题,更新腾讯文档配置说明和最新测试结果。
|
|
20
|
+
|
|
21
|
+
## [0.4.5] - 2026-03-20
|
|
22
|
+
|
|
23
|
+
### 新增
|
|
24
|
+
|
|
25
|
+
- 在 CLI 首页和配置向导的 LOGO 右下角新增版本号展示,便于直接确认当前运行版本。
|
|
26
|
+
|
|
27
|
+
### 变更
|
|
28
|
+
|
|
29
|
+
- 变更发布构建内容:打包产物现在会一并包含 `package.json`,保证 macOS / Windows 分发包中的 CLI 也能正确显示版本号。
|
|
30
|
+
- 变更 README 的分发包说明,统一 macOS 资产命名为 `tb-order-sync-macos-x64-<version>.zip`。
|
|
31
|
+
|
|
32
|
+
### 修复
|
|
33
|
+
|
|
34
|
+
- 修复 `tb setup` 链接选择菜单的残留交互问题:现在支持 `0`、空回车、`/skip`、`q` 等跳过方式,并在输入流中断时安全退出,不再抛出 `EOFError` 栈追踪。
|
|
35
|
+
|
|
36
|
+
## [0.4.4] - 2026-03-20
|
|
37
|
+
|
|
38
|
+
### 修复
|
|
39
|
+
|
|
40
|
+
- 修复 `tb setup` 在 Windows 环境下输入不稳定的问题:配置向导现统一使用原生 `input()` 读取,避免官网链接打开后无法继续输入、无法输入字母数字、无法粘贴的问题。
|
|
41
|
+
- 修复 `tb setup` 的官网链接打开交互体验:保持数字菜单方式,支持跳过、打开全部、打开单个链接,并增强 Windows 下的浏览器启动回退逻辑。
|
|
42
|
+
- 修复 Windows 双击 `启动.bat` 可能直接闪退的问题:补上缺失的 `:menu` 标签,并修正 Python / EXE 命令拼接与失败停留逻辑。
|
|
43
|
+
|
|
44
|
+
## [0.4.3] - 2026-03-20
|
|
45
|
+
|
|
46
|
+
### 新增
|
|
47
|
+
|
|
48
|
+
- 新增按工作表标题关键字自动选择“最新月份” sheet 的能力,可分别作用于 A 表和 B 表。
|
|
49
|
+
- 新增工作表月份解析工具,支持以下标题格式:
|
|
50
|
+
- `2026年3月毛利率`
|
|
51
|
+
- `2026-03 毛利率`
|
|
52
|
+
- `2026/03 客户退款`
|
|
53
|
+
- `3月毛利率`
|
|
54
|
+
- 新增 `TENCENT_A_SHEET_NAME_KEYWORD` / `TENCENT_B_SHEET_NAME_KEYWORD` 两个可选配置项。
|
|
55
|
+
- 新增月份选择相关单元测试,覆盖同年和跨年场景。
|
|
56
|
+
|
|
57
|
+
### 变更
|
|
58
|
+
|
|
59
|
+
- 变更毛利计算与退款匹配服务:当配置了 sheet 标题关键字后,运行时会优先选择匹配关键字的最新月份工作表,而不是只依赖固定 `sheet_id`。
|
|
60
|
+
- 变更 `tb setup` 与 `tb check`:配置向导和启动自检现已支持并展示自动月表选择逻辑。
|
|
61
|
+
- 变更 CLI 控制台视觉风格:首页新增 LOGO、状态徽章和更统一的配色。
|
|
62
|
+
- 变更 CLI 次级页面视觉风格:执行结果、失败详情、守护结果、后台日志、配置未完成提示,统一为一致的模态面板样式。
|
|
63
|
+
|
|
64
|
+
### 修复
|
|
65
|
+
|
|
66
|
+
- 修复跨年月份选择歧义:当标题中包含年份时,优先按“年 + 月”判断最新工作表,而不是只比较月份数字。
|
|
67
|
+
- 修复本地 CLI 体验不一致问题,使首页与二级页面在视觉上保持统一。
|
|
68
|
+
- 修复 `tb setup` 中官网链接打开交互不明确的问题:现已改为数字菜单,支持打开全部、打开单个链接或暂时跳过。
|
|
69
|
+
- 修复配置向导中凭证输入不可直接粘贴的问题:`Client ID` / `Open ID` / `Access Token` 等输入现改为可见输入,支持直接粘贴。
|
|
70
|
+
- 修复配置向导中无法暂时跳过某一项的问题:现可输入 `/skip` 暂时跳过,后续再补充配置。
|
|
71
|
+
- 修复 Windows 双击 `启动.bat` 可能直接闪退的问题:补上缺失的 `:menu` 标签,并修正 Python / EXE 启动命令拼接方式。
|
|
72
|
+
- 修复 Windows 打包环境下 setup 输入不稳定的问题:现将配置向导输入统一切回原生 `input()`,避免链接打开后无法继续输入或无法粘贴。
|
|
73
|
+
|
|
5
74
|
## [0.4.2] - 2026-03-19
|
|
6
75
|
|
|
7
76
|
### 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-
|
|
81
|
+
- macOS: `tb-order-sync-macos-x64-<version>.zip`
|
|
82
82
|
|
|
83
83
|
### 常用命令速查
|
|
84
84
|
|
|
@@ -127,13 +127,12 @@ tb daemon autostart-disable
|
|
|
127
127
|
- 开发者平台: [腾讯文档开放生态](https://docs.qq.com/open/developers/)
|
|
128
128
|
- 建议流程:
|
|
129
129
|
1. 先进入开发者平台创建应用
|
|
130
|
-
2. 在应用详情页获取 `Client ID`
|
|
131
|
-
3.
|
|
130
|
+
2. 在应用详情页获取 `Client ID`
|
|
131
|
+
3. 完成授权后获取 `Open ID` 和 `Access Token`
|
|
132
132
|
4. 再回到本项目执行 `tb setup`
|
|
133
133
|
- 当前说明:
|
|
134
134
|
- 本项目 MVP 目前依赖你手工提供有效 `Access Token`
|
|
135
135
|
- 当前运行链路要求 `Client ID + Open ID + Access Token`
|
|
136
|
-
- `Client Secret` 目前保留为可选项,后续接自动刷新 token 时再使用
|
|
137
136
|
- 在线表格 v3 读写链路已经完成真实联调验证
|
|
138
137
|
|
|
139
138
|
### 飞书 API
|
|
@@ -327,8 +326,10 @@ TENCENT_OPEN_ID=your_open_id
|
|
|
327
326
|
TENCENT_ACCESS_TOKEN=your_access_token
|
|
328
327
|
TENCENT_A_FILE_ID=a_table_file_id
|
|
329
328
|
TENCENT_A_SHEET_ID=a_table_sheet_id
|
|
329
|
+
TENCENT_A_SHEET_NAME_KEYWORD=毛利率
|
|
330
330
|
TENCENT_B_FILE_ID=b_table_file_id
|
|
331
331
|
TENCENT_B_SHEET_ID=b_table_sheet_id
|
|
332
|
+
TENCENT_B_SHEET_NAME_KEYWORD=客户退款
|
|
332
333
|
|
|
333
334
|
# 运行模式
|
|
334
335
|
GROSS_PROFIT_MODE=incremental # incremental | full
|
|
@@ -349,7 +350,9 @@ B_COL_ORDER_NO=A
|
|
|
349
350
|
```
|
|
350
351
|
|
|
351
352
|
补充说明:
|
|
352
|
-
- `tb setup`
|
|
353
|
+
- `tb setup` 会直接要求你粘贴腾讯文档完整链接,并自动解析 `File ID / Sheet ID`
|
|
354
|
+
- `tb setup` 当前只保留腾讯文档最小必填项,不再展示飞书和其他暂时不用的高级配置
|
|
355
|
+
- 如果填写 `TENCENT_A_SHEET_NAME_KEYWORD` / `TENCENT_B_SHEET_NAME_KEYWORD`,系统会在对应文件里自动选取标题中匹配关键字的“最新月份”工作表
|
|
353
356
|
- `tb check` 会做启动自检,不只是看 `.env` 是否存在
|
|
354
357
|
- 当前退款高亮效果是“整行红色文字”,不是背景填充
|
|
355
358
|
|
|
@@ -359,7 +362,7 @@ B_COL_ORDER_NO=A
|
|
|
359
362
|
## 🧪 测试
|
|
360
363
|
|
|
361
364
|
```bash
|
|
362
|
-
#
|
|
365
|
+
# 运行全部测试
|
|
363
366
|
pytest tests/ -v
|
|
364
367
|
|
|
365
368
|
# 单独运行
|
package/build.py
CHANGED
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
|
-
|
|
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 #
|
|
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(
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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,
|
|
439
|
+
def _pause_with_panel(self, body, *, title: str, border_style: str) -> None:
|
|
326
440
|
self.console.clear()
|
|
327
|
-
self.console.print(
|
|
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
|
|