tb-order-sync 0.4.1 → 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 +4 -0
- package/CHANGELOG.md +67 -0
- package/README.md +15 -1
- package/bin/postinstall.js +21 -0
- package/bin/runtime.js +228 -0
- package/bin/tb.js +11 -108
- package/build.py +1 -0
- package/cli/dashboard.py +167 -29
- package/cli/setup.py +209 -28
- package/config/settings.py +73 -7
- package/connectors/tencent_docs.py +87 -0
- package/package.json +3 -2
- package/services/daemon_service.py +12 -4
- 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//345/277/253/351/200/237/345/274/200/345/247/213.txt +16 -9
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tb-order-sync",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Tencent Docs order sync
|
|
3
|
+
"version": "0.4.5",
|
|
4
|
+
"description": "Tencent Docs order sync CLI with one-command npm install, gross-profit automation, refund marking, self-check, and daemon mode",
|
|
5
5
|
"bin": {
|
|
6
6
|
"tb": "bin/tb.js"
|
|
7
7
|
},
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"启动.command"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
+
"postinstall": "node bin/postinstall.js",
|
|
29
30
|
"tb": "node bin/tb.js",
|
|
30
31
|
"check": "node bin/tb.js check",
|
|
31
32
|
"pack:local": "npm pack"
|
|
@@ -14,7 +14,7 @@ from dataclasses import dataclass
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Any
|
|
16
16
|
|
|
17
|
-
from config.settings import
|
|
17
|
+
from config.settings import APP_HOME, PACKAGE_ROOT, Settings
|
|
18
18
|
from utils.logger import get_logger
|
|
19
19
|
|
|
20
20
|
logger = get_logger(__name__)
|
|
@@ -131,10 +131,14 @@ class DaemonService:
|
|
|
131
131
|
log_handle = self._log_path.open("ab")
|
|
132
132
|
try:
|
|
133
133
|
spawn_kwargs: dict[str, Any] = {
|
|
134
|
-
"cwd": str(
|
|
134
|
+
"cwd": str(PACKAGE_ROOT),
|
|
135
135
|
"stdin": subprocess.DEVNULL,
|
|
136
136
|
"stdout": log_handle,
|
|
137
137
|
"stderr": log_handle,
|
|
138
|
+
"env": {
|
|
139
|
+
**os.environ,
|
|
140
|
+
"TB_HOME": str(APP_HOME),
|
|
141
|
+
},
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
if os.name == "nt":
|
|
@@ -332,7 +336,7 @@ class DaemonService:
|
|
|
332
336
|
def _build_spawn_command(self) -> list[str]:
|
|
333
337
|
if getattr(sys, "frozen", False):
|
|
334
338
|
return [str(Path(sys.executable).resolve()), "schedule"]
|
|
335
|
-
return [sys.executable, str(
|
|
339
|
+
return [sys.executable, str(PACKAGE_ROOT / "main.py"), "schedule"]
|
|
336
340
|
|
|
337
341
|
def _autostart_command_line(self) -> str:
|
|
338
342
|
return subprocess.list2cmdline(self._build_spawn_command())
|
|
@@ -355,7 +359,11 @@ class DaemonService:
|
|
|
355
359
|
" <array>\n"
|
|
356
360
|
f"{args}\n"
|
|
357
361
|
" </array>\n"
|
|
358
|
-
|
|
362
|
+
" <key>EnvironmentVariables</key>\n"
|
|
363
|
+
" <dict>\n"
|
|
364
|
+
f" <key>TB_HOME</key>\n <string>{APP_HOME}</string>\n"
|
|
365
|
+
" </dict>\n"
|
|
366
|
+
f" <key>WorkingDirectory</key>\n <string>{PACKAGE_ROOT}</string>\n"
|
|
359
367
|
" <key>RunAtLoad</key>\n <true/>\n"
|
|
360
368
|
" <key>KeepAlive</key>\n <false/>\n"
|
|
361
369
|
f" <key>StandardOutPath</key>\n <string>{self._log_path}</string>\n"
|
|
@@ -22,6 +22,7 @@ from services.state_service import StateService
|
|
|
22
22
|
from utils.diff import row_fingerprint
|
|
23
23
|
from utils.logger import get_logger
|
|
24
24
|
from utils.parser import normalize_order_no, parse_number
|
|
25
|
+
from utils.sheet_selector import ResolvedSheetTarget, resolve_latest_month_sheet
|
|
25
26
|
|
|
26
27
|
logger = get_logger(__name__)
|
|
27
28
|
|
|
@@ -55,7 +56,8 @@ class GrossProfitService:
|
|
|
55
56
|
|
|
56
57
|
try:
|
|
57
58
|
state = self._state_svc.load()
|
|
58
|
-
|
|
59
|
+
a_target = self._resolve_a_target()
|
|
60
|
+
rows = self._read_a_table(a_target.sheet_id)
|
|
59
61
|
result.rows_read = len(rows)
|
|
60
62
|
if not rows:
|
|
61
63
|
logger.warning("A table returned 0 data rows")
|
|
@@ -69,7 +71,7 @@ class GrossProfitService:
|
|
|
69
71
|
result.rows_error = errors
|
|
70
72
|
|
|
71
73
|
if updates and not dry_run:
|
|
72
|
-
self._write_updates(updates)
|
|
74
|
+
self._write_updates(a_target.sheet_id, updates)
|
|
73
75
|
state.last_run_at = datetime.now()
|
|
74
76
|
self._state_svc.save(state)
|
|
75
77
|
|
|
@@ -89,14 +91,29 @@ class GrossProfitService:
|
|
|
89
91
|
|
|
90
92
|
# ── Internal ───────────────────────────────────────────────────────────
|
|
91
93
|
|
|
92
|
-
def _read_a_table(self) -> list[list[Any]]:
|
|
94
|
+
def _read_a_table(self, sheet_id: str) -> list[list[Any]]:
|
|
93
95
|
rows = self._conn.read_rows(
|
|
94
96
|
self._settings.tencent_a_file_id,
|
|
95
|
-
|
|
97
|
+
sheet_id,
|
|
96
98
|
)
|
|
97
99
|
# Skip header (row 0)
|
|
98
100
|
return rows[1:] if rows else []
|
|
99
101
|
|
|
102
|
+
def _resolve_a_target(self) -> ResolvedSheetTarget:
|
|
103
|
+
target = resolve_latest_month_sheet(
|
|
104
|
+
self._conn, # type: ignore[arg-type]
|
|
105
|
+
file_id=self._settings.tencent_a_file_id,
|
|
106
|
+
fallback_sheet_id=self._settings.tencent_a_sheet_id,
|
|
107
|
+
title_keyword=self._settings.tencent_a_sheet_name_keyword,
|
|
108
|
+
)
|
|
109
|
+
if target.source != "fixed":
|
|
110
|
+
logger.info(
|
|
111
|
+
"A 表自动选择最新月份工作表: %s (%s)",
|
|
112
|
+
target.title or target.sheet_id,
|
|
113
|
+
target.sheet_id,
|
|
114
|
+
)
|
|
115
|
+
return target
|
|
116
|
+
|
|
100
117
|
def _parse_rows(self, rows: list[list[Any]]) -> list[OrderRecord]:
|
|
101
118
|
m = self._map
|
|
102
119
|
records: list[OrderRecord] = []
|
|
@@ -167,10 +184,10 @@ class GrossProfitService:
|
|
|
167
184
|
|
|
168
185
|
return updates, changed, errors
|
|
169
186
|
|
|
170
|
-
def _write_updates(self, updates: list[CellUpdate]) -> None:
|
|
187
|
+
def _write_updates(self, sheet_id: str, updates: list[CellUpdate]) -> None:
|
|
171
188
|
self._conn.batch_update(
|
|
172
189
|
self._settings.tencent_a_file_id,
|
|
173
|
-
|
|
190
|
+
sheet_id,
|
|
174
191
|
updates,
|
|
175
192
|
batch_size=self._settings.write_batch_size,
|
|
176
193
|
)
|
|
@@ -23,6 +23,7 @@ from services.state_service import StateService
|
|
|
23
23
|
from utils.diff import row_fingerprint, set_hash
|
|
24
24
|
from utils.logger import get_logger
|
|
25
25
|
from utils.parser import normalize_order_no
|
|
26
|
+
from utils.sheet_selector import ResolvedSheetTarget, resolve_latest_month_sheet
|
|
26
27
|
|
|
27
28
|
logger = get_logger(__name__)
|
|
28
29
|
|
|
@@ -59,14 +60,16 @@ class RefundMatchService:
|
|
|
59
60
|
|
|
60
61
|
try:
|
|
61
62
|
state = self._state_svc.load()
|
|
63
|
+
a_target = self._resolve_a_target()
|
|
64
|
+
b_target = self._resolve_b_target()
|
|
62
65
|
|
|
63
66
|
# 1. Build refund set from B table
|
|
64
|
-
refund_set = self._build_refund_set()
|
|
67
|
+
refund_set = self._build_refund_set(b_target.sheet_id)
|
|
65
68
|
new_refund_hash = set_hash(list(refund_set))
|
|
66
69
|
logger.info("B table refund set: %d order numbers, hash=%s", len(refund_set), new_refund_hash[:8])
|
|
67
70
|
|
|
68
71
|
# 2. Read A table
|
|
69
|
-
a_rows = self._read_a_table()
|
|
72
|
+
a_rows = self._read_a_table(a_target.sheet_id)
|
|
70
73
|
result.rows_read = len(a_rows)
|
|
71
74
|
a_scan_hash = self._build_a_scan_hash(a_rows)
|
|
72
75
|
|
|
@@ -89,12 +92,12 @@ class RefundMatchService:
|
|
|
89
92
|
if updates:
|
|
90
93
|
self._conn.batch_update(
|
|
91
94
|
self._settings.tencent_a_file_id,
|
|
92
|
-
|
|
95
|
+
a_target.sheet_id,
|
|
93
96
|
updates,
|
|
94
97
|
batch_size=self._settings.write_batch_size,
|
|
95
98
|
)
|
|
96
99
|
if self._settings.enable_style_update and style_ops:
|
|
97
|
-
self._apply_styles(style_ops)
|
|
100
|
+
self._apply_styles(a_target.sheet_id, style_ops)
|
|
98
101
|
|
|
99
102
|
state.b_table_refund_hash = new_refund_hash
|
|
100
103
|
state.b_table_refund_set = sorted(refund_set)
|
|
@@ -117,10 +120,10 @@ class RefundMatchService:
|
|
|
117
120
|
|
|
118
121
|
# ── Internal ───────────────────────────────────────────────────────────
|
|
119
122
|
|
|
120
|
-
def _build_refund_set(self) -> set[str]:
|
|
123
|
+
def _build_refund_set(self, sheet_id: str) -> set[str]:
|
|
121
124
|
rows = self._conn.read_rows(
|
|
122
125
|
self._settings.tencent_b_file_id,
|
|
123
|
-
|
|
126
|
+
sheet_id,
|
|
124
127
|
)
|
|
125
128
|
# Skip header
|
|
126
129
|
data_rows = rows[1:] if rows else []
|
|
@@ -133,13 +136,43 @@ class RefundMatchService:
|
|
|
133
136
|
refund_set.add(order_no)
|
|
134
137
|
return refund_set
|
|
135
138
|
|
|
136
|
-
def _read_a_table(self) -> list[list[Any]]:
|
|
139
|
+
def _read_a_table(self, sheet_id: str) -> list[list[Any]]:
|
|
137
140
|
rows = self._conn.read_rows(
|
|
138
141
|
self._settings.tencent_a_file_id,
|
|
139
|
-
|
|
142
|
+
sheet_id,
|
|
140
143
|
)
|
|
141
144
|
return rows[1:] if rows else []
|
|
142
145
|
|
|
146
|
+
def _resolve_a_target(self) -> ResolvedSheetTarget:
|
|
147
|
+
target = resolve_latest_month_sheet(
|
|
148
|
+
self._conn, # type: ignore[arg-type]
|
|
149
|
+
file_id=self._settings.tencent_a_file_id,
|
|
150
|
+
fallback_sheet_id=self._settings.tencent_a_sheet_id,
|
|
151
|
+
title_keyword=self._settings.tencent_a_sheet_name_keyword,
|
|
152
|
+
)
|
|
153
|
+
if target.source != "fixed":
|
|
154
|
+
logger.info(
|
|
155
|
+
"退款匹配使用 A 表最新月份工作表: %s (%s)",
|
|
156
|
+
target.title or target.sheet_id,
|
|
157
|
+
target.sheet_id,
|
|
158
|
+
)
|
|
159
|
+
return target
|
|
160
|
+
|
|
161
|
+
def _resolve_b_target(self) -> ResolvedSheetTarget:
|
|
162
|
+
target = resolve_latest_month_sheet(
|
|
163
|
+
self._conn, # type: ignore[arg-type]
|
|
164
|
+
file_id=self._settings.tencent_b_file_id,
|
|
165
|
+
fallback_sheet_id=self._settings.tencent_b_sheet_id,
|
|
166
|
+
title_keyword=self._settings.tencent_b_sheet_name_keyword,
|
|
167
|
+
)
|
|
168
|
+
if target.source != "fixed":
|
|
169
|
+
logger.info(
|
|
170
|
+
"退款匹配使用 B 表最新月份工作表: %s (%s)",
|
|
171
|
+
target.title or target.sheet_id,
|
|
172
|
+
target.sheet_id,
|
|
173
|
+
)
|
|
174
|
+
return target
|
|
175
|
+
|
|
143
176
|
def _build_a_scan_hash(self, a_rows: list[list[Any]]) -> str:
|
|
144
177
|
m = self._map
|
|
145
178
|
row_hashes = []
|
|
@@ -219,14 +252,14 @@ class RefundMatchService:
|
|
|
219
252
|
|
|
220
253
|
return updates, style_ops, changed
|
|
221
254
|
|
|
222
|
-
def _apply_styles(self, ops: list[tuple[int, Optional[str]]]) -> None:
|
|
255
|
+
def _apply_styles(self, sheet_id: str, ops: list[tuple[int, Optional[str]]]) -> None:
|
|
223
256
|
failures: list[str] = []
|
|
224
257
|
total = len(ops)
|
|
225
258
|
for index, (row_idx, color) in enumerate(ops):
|
|
226
259
|
try:
|
|
227
260
|
self._conn.update_row_style(
|
|
228
261
|
self._settings.tencent_a_file_id,
|
|
229
|
-
|
|
262
|
+
sheet_id,
|
|
230
263
|
row_idx,
|
|
231
264
|
bg_color=color,
|
|
232
265
|
)
|
package/sync_service.spec
CHANGED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Helpers for resolving monthly Tencent Docs sheet targets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Iterable, Protocol
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_YEAR_MONTH_RE = re.compile(r"(?P<year>20\d{2})\s*[年./_-]?\s*(?P<month>1[0-2]|0?[1-9])\s*月?")
|
|
11
|
+
_MONTH_ONLY_RE = re.compile(r"(?<!\d)(?P<month>1[0-2]|0?[1-9])\s*月")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class SheetInfo:
|
|
16
|
+
"""Minimal spreadsheet tab metadata."""
|
|
17
|
+
|
|
18
|
+
sheet_id: str
|
|
19
|
+
title: str
|
|
20
|
+
index: int = 0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class ResolvedSheetTarget:
|
|
25
|
+
"""Resolved file/sheet target for one task run."""
|
|
26
|
+
|
|
27
|
+
file_id: str
|
|
28
|
+
sheet_id: str
|
|
29
|
+
title: str | None = None
|
|
30
|
+
source: str = "fixed"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SheetListingConnector(Protocol):
|
|
34
|
+
"""Connector protocol for optional sheet-listing support."""
|
|
35
|
+
|
|
36
|
+
def list_sheets(self, file_id: str) -> list[SheetInfo]:
|
|
37
|
+
"""Return spreadsheet tabs for a file."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def resolve_latest_month_sheet(
|
|
41
|
+
connector: SheetListingConnector,
|
|
42
|
+
*,
|
|
43
|
+
file_id: str,
|
|
44
|
+
fallback_sheet_id: str,
|
|
45
|
+
title_keyword: str,
|
|
46
|
+
) -> ResolvedSheetTarget:
|
|
47
|
+
"""Resolve the latest monthly sheet by title keyword.
|
|
48
|
+
|
|
49
|
+
If `title_keyword` is blank, the fixed `fallback_sheet_id` is returned unchanged.
|
|
50
|
+
"""
|
|
51
|
+
keyword = title_keyword.strip()
|
|
52
|
+
if not keyword:
|
|
53
|
+
return ResolvedSheetTarget(file_id=file_id, sheet_id=fallback_sheet_id, source="fixed")
|
|
54
|
+
|
|
55
|
+
list_sheets = getattr(connector, "list_sheets", None)
|
|
56
|
+
if not callable(list_sheets):
|
|
57
|
+
raise RuntimeError("当前 connector 不支持按标题自动选择最新月份工作表")
|
|
58
|
+
|
|
59
|
+
sheets = list_sheets(file_id)
|
|
60
|
+
selected = select_latest_month_sheet(sheets, keyword=keyword)
|
|
61
|
+
return ResolvedSheetTarget(
|
|
62
|
+
file_id=file_id,
|
|
63
|
+
sheet_id=selected.sheet_id,
|
|
64
|
+
title=selected.title,
|
|
65
|
+
source="latest-month",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def select_latest_month_sheet(sheets: Iterable[SheetInfo], *, keyword: str) -> SheetInfo:
|
|
70
|
+
"""Choose the latest monthly sheet from matching titles.
|
|
71
|
+
|
|
72
|
+
Priority:
|
|
73
|
+
1. Prefer titles that include both year and month, such as `2026年3月` / `2026-03` / `2026/03`
|
|
74
|
+
2. Fall back to month-only titles such as `3月毛利率`
|
|
75
|
+
|
|
76
|
+
This keeps same-year monthly tabs automatic, while allowing accurate cross-year
|
|
77
|
+
selection when the sheet title carries the year.
|
|
78
|
+
"""
|
|
79
|
+
needle = keyword.strip().lower()
|
|
80
|
+
candidates = [sheet for sheet in sheets if needle in sheet.title.lower()]
|
|
81
|
+
if not candidates:
|
|
82
|
+
raise ValueError(f"未找到包含关键字 '{keyword}' 的工作表")
|
|
83
|
+
|
|
84
|
+
ranked: list[tuple[int, int, int, SheetInfo]] = []
|
|
85
|
+
unparsed: list[SheetInfo] = []
|
|
86
|
+
for sheet in candidates:
|
|
87
|
+
period = extract_year_month(sheet.title)
|
|
88
|
+
if period is None:
|
|
89
|
+
unparsed.append(sheet)
|
|
90
|
+
continue
|
|
91
|
+
year, month = period
|
|
92
|
+
ranked.append((year, month, sheet.index, sheet))
|
|
93
|
+
|
|
94
|
+
if ranked:
|
|
95
|
+
ranked.sort(key=lambda item: (item[0], item[1], item[2]))
|
|
96
|
+
return ranked[-1][3]
|
|
97
|
+
|
|
98
|
+
if len(candidates) == 1:
|
|
99
|
+
return candidates[0]
|
|
100
|
+
|
|
101
|
+
titles = ", ".join(sheet.title for sheet in candidates[:5])
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"找到多个包含关键字 '{keyword}' 的工作表,但无法从标题解析月份: {titles}"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def extract_year_month(title: str) -> tuple[int, int] | None:
|
|
108
|
+
"""Extract `(year, month)` from a sheet title.
|
|
109
|
+
|
|
110
|
+
Supports examples like:
|
|
111
|
+
- 2026年3月毛利率
|
|
112
|
+
- 2026-03 毛利率
|
|
113
|
+
- 3月毛利率
|
|
114
|
+
"""
|
|
115
|
+
year_month = _YEAR_MONTH_RE.search(title)
|
|
116
|
+
if year_month:
|
|
117
|
+
year = int(year_month.group("year"))
|
|
118
|
+
month = int(year_month.group("month"))
|
|
119
|
+
return year, month
|
|
120
|
+
|
|
121
|
+
month_only = _MONTH_ONLY_RE.search(title)
|
|
122
|
+
if month_only:
|
|
123
|
+
return 0, int(month_only.group("month"))
|
|
124
|
+
|
|
125
|
+
return None
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
@echo off
|
|
2
|
+
setlocal EnableExtensions DisableDelayedExpansion
|
|
2
3
|
chcp 65001 >nul 2>&1
|
|
3
4
|
title 多表格同步服务
|
|
4
5
|
cd /d "%~dp0"
|
|
@@ -12,12 +13,14 @@ echo.
|
|
|
12
13
|
:: ── 优先级1:已有打包好的 exe ────────────────────────
|
|
13
14
|
if exist "sync_service.exe" (
|
|
14
15
|
set "CMD=sync_service.exe"
|
|
16
|
+
set "CMD_ARGS="
|
|
15
17
|
goto :menu
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
:: ── 优先级2:已有虚拟环境 ───────────────────────────
|
|
19
21
|
if exist ".venv\Scripts\python.exe" (
|
|
20
|
-
set "CMD=.venv\Scripts\python
|
|
22
|
+
set "CMD=.venv\Scripts\python.exe"
|
|
23
|
+
set "CMD_ARGS=main.py"
|
|
21
24
|
goto :menu
|
|
22
25
|
)
|
|
23
26
|
|
|
@@ -89,7 +92,8 @@ echo [*] 使用嵌入式 Python 安装依赖...
|
|
|
89
92
|
echo.
|
|
90
93
|
python_embed\python.exe -m pip install -q -r requirements.txt --target=".deps" 2>nul
|
|
91
94
|
set "PYTHONPATH=%~dp0.deps;%~dp0"
|
|
92
|
-
set "CMD=python_embed\python.exe
|
|
95
|
+
set "CMD=python_embed\python.exe"
|
|
96
|
+
set "CMD_ARGS=main.py"
|
|
93
97
|
goto :menu
|
|
94
98
|
|
|
95
99
|
:venv_setup
|
|
@@ -111,15 +115,30 @@ if errorlevel 1 (
|
|
|
111
115
|
|
|
112
116
|
echo [3/3] 环境初始化完成!
|
|
113
117
|
echo.
|
|
114
|
-
set "CMD=.venv\Scripts\python
|
|
118
|
+
set "CMD=.venv\Scripts\python.exe"
|
|
119
|
+
set "CMD_ARGS=main.py"
|
|
115
120
|
|
|
116
121
|
:: ── 启动 Rich 控制台 / 执行指定命令 ────────────────
|
|
122
|
+
:menu
|
|
123
|
+
if not defined CMD (
|
|
124
|
+
echo [!] 启动命令未初始化成功
|
|
125
|
+
echo.
|
|
126
|
+
pause
|
|
127
|
+
exit /b 1
|
|
128
|
+
)
|
|
129
|
+
|
|
117
130
|
if "%~1"=="" (
|
|
118
|
-
%CMD%
|
|
131
|
+
call "%CMD%" %CMD_ARGS%
|
|
119
132
|
echo.
|
|
120
133
|
pause
|
|
121
134
|
exit /b 0
|
|
122
135
|
)
|
|
123
136
|
|
|
124
|
-
%CMD% %*
|
|
125
|
-
|
|
137
|
+
call "%CMD%" %CMD_ARGS% %*
|
|
138
|
+
set "EXIT_CODE=%errorlevel%"
|
|
139
|
+
if not "%EXIT_CODE%"=="0" (
|
|
140
|
+
echo.
|
|
141
|
+
echo [!] 程序退出,返回码: %EXIT_CODE%
|
|
142
|
+
pause
|
|
143
|
+
)
|
|
144
|
+
exit /b %EXIT_CODE%
|
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
TB Order Sync 快速开始
|
|
2
2
|
|
|
3
|
-
1.
|
|
3
|
+
1. 一条命令安装 CLI(macOS / 已安装 Node.js)
|
|
4
|
+
- npm install -g tb-order-sync
|
|
5
|
+
- 安装完成后直接运行 `tb`
|
|
6
|
+
- 运行环境会放到 `~/Library/Application Support/tb-order-sync/`
|
|
7
|
+
|
|
8
|
+
2. 双击启动
|
|
4
9
|
- Windows: 启动.bat
|
|
5
10
|
- macOS: 启动.command
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
3. 首次运行
|
|
8
13
|
- 如果本机还没配置,程序会自动进入 setup 配置向导
|
|
9
14
|
- 配好腾讯文档信息后,建议先执行一次 `tb check`
|
|
10
15
|
|
|
11
|
-
|
|
16
|
+
4. 常用命令
|
|
12
17
|
- tb all 执行毛利计算 + 退款匹配
|
|
13
18
|
- tb all --dry-run 仅演练,不写回
|
|
14
19
|
- tb daemon start 后台持续运行
|
|
15
20
|
- tb daemon status 查看后台状态
|
|
16
21
|
- tb daemon logs 查看后台日志
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
5. 登录自启
|
|
19
24
|
- tb daemon autostart-enable
|
|
20
25
|
- tb daemon autostart-status
|
|
21
26
|
- tb daemon autostart-disable
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
- state/
|
|
25
|
-
-
|
|
26
|
-
-
|
|
28
|
+
6. 日志与状态文件
|
|
29
|
+
- npm 安装方式:~/Library/Application Support/tb-order-sync/state/
|
|
30
|
+
- 常见文件:
|
|
31
|
+
- scheduler.console.log
|
|
32
|
+
- last_run.json
|
|
33
|
+
- sync_state.json
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
7. 常见问题
|
|
29
36
|
- 缺配置:运行 tb setup
|
|
30
37
|
- 想做启动自检:运行 tb check
|
|
31
38
|
- 接口限流:稍等后重试,不要连续高频重复执行
|