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/.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,73 @@
|
|
|
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
|
+
|
|
58
|
+
## [0.4.2] - 2026-03-19
|
|
59
|
+
|
|
60
|
+
### Added
|
|
61
|
+
|
|
62
|
+
- Added npm global-install bootstrap via `postinstall`, so `npm install -g tb-order-sync` can prepare the CLI runtime in one step on macOS.
|
|
63
|
+
- Added dedicated runtime path handling for npm installs, using a writable user app-home instead of the npm package directory.
|
|
64
|
+
- Added tests for `TB_HOME` path resolution and relative `state_dir` normalization.
|
|
65
|
+
- Added `pytest.ini` to keep local release artifacts from polluting test discovery.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- Changed the Node launcher to store `.env`, `state`, and `.venv` under the app home during npm-based CLI usage.
|
|
70
|
+
- Changed documentation to describe the one-command npm install flow and the macOS runtime directory.
|
|
71
|
+
|
|
5
72
|
## [0.4.1] - 2026-03-19
|
|
6
73
|
|
|
7
74
|
### Added
|
package/README.md
CHANGED
|
@@ -54,6 +54,15 @@ tb daemon start
|
|
|
54
54
|
tb daemon status
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
在 macOS 上,`npm install -g tb-order-sync` 会同时完成两件事:
|
|
58
|
+
- 安装全局 `tb` 命令
|
|
59
|
+
- 自动把 CLI 运行环境部署到 `~/Library/Application Support/tb-order-sync/`
|
|
60
|
+
|
|
61
|
+
运行时目录说明:
|
|
62
|
+
- 配置文件:`~/Library/Application Support/tb-order-sync/.env`
|
|
63
|
+
- 状态目录:`~/Library/Application Support/tb-order-sync/state/`
|
|
64
|
+
- Python 运行环境:`~/Library/Application Support/tb-order-sync/.venv/`
|
|
65
|
+
|
|
57
66
|
首次运行说明:
|
|
58
67
|
- 如果本机还没有完整配置,直接执行 `tb` 会自动进入 `setup`
|
|
59
68
|
- `tb check` 会执行启动自检,确认状态目录、A 表、B 表是否可用
|
|
@@ -196,7 +205,9 @@ tb-order-sync/
|
|
|
196
205
|
├── 启动.bat # Windows 一键启动
|
|
197
206
|
├── 启动.command # macOS 一键启动
|
|
198
207
|
├── bin/
|
|
199
|
-
│
|
|
208
|
+
│ ├── tb.js # npm / node 统一入口
|
|
209
|
+
│ ├── runtime.js # npm 运行时 bootstrap
|
|
210
|
+
│ └── postinstall.js # npm 全局安装自动部署
|
|
200
211
|
│
|
|
201
212
|
├── config/
|
|
202
213
|
│ ├── settings.py # Pydantic Settings 全局配置
|
|
@@ -316,8 +327,10 @@ TENCENT_OPEN_ID=your_open_id
|
|
|
316
327
|
TENCENT_ACCESS_TOKEN=your_access_token
|
|
317
328
|
TENCENT_A_FILE_ID=a_table_file_id
|
|
318
329
|
TENCENT_A_SHEET_ID=a_table_sheet_id
|
|
330
|
+
TENCENT_A_SHEET_NAME_KEYWORD=毛利率
|
|
319
331
|
TENCENT_B_FILE_ID=b_table_file_id
|
|
320
332
|
TENCENT_B_SHEET_ID=b_table_sheet_id
|
|
333
|
+
TENCENT_B_SHEET_NAME_KEYWORD=客户退款
|
|
321
334
|
|
|
322
335
|
# 运行模式
|
|
323
336
|
GROSS_PROFIT_MODE=incremental # incremental | full
|
|
@@ -339,6 +352,7 @@ B_COL_ORDER_NO=A
|
|
|
339
352
|
|
|
340
353
|
补充说明:
|
|
341
354
|
- `tb setup` 支持直接粘贴腾讯文档完整链接,自动拆出 `File ID / Sheet ID`
|
|
355
|
+
- 如果填写 `TENCENT_A_SHEET_NAME_KEYWORD` / `TENCENT_B_SHEET_NAME_KEYWORD`,系统会在对应文件里自动选取标题中匹配关键字的“最新月份”工作表
|
|
342
356
|
- `tb check` 会做启动自检,不只是看 `.env` 是否存在
|
|
343
357
|
- 当前退款高亮效果是“整行红色文字”,不是背景填充
|
|
344
358
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { ensurePythonRuntime, getAppHome } = require("./runtime");
|
|
4
|
+
|
|
5
|
+
const isGlobalInstall = process.env.npm_config_global === "true";
|
|
6
|
+
const shouldSkip = process.env.TB_SKIP_POSTINSTALL_BOOTSTRAP === "1";
|
|
7
|
+
|
|
8
|
+
if (!isGlobalInstall || shouldSkip) {
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
console.log("tb-order-sync: 正在准备 CLI 运行环境...");
|
|
13
|
+
const result = ensurePythonRuntime({ quiet: false, exitOnError: false });
|
|
14
|
+
|
|
15
|
+
if (!result.ok) {
|
|
16
|
+
console.warn("tb-order-sync: Python 运行环境未自动安装完成。");
|
|
17
|
+
console.warn("tb-order-sync: 安装仍已完成,后续执行 tb 时会再次尝试初始化。");
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log(`tb-order-sync: CLI 已部署到 ${getAppHome()}`);
|
package/bin/runtime.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { spawnSync } = require("child_process");
|
|
5
|
+
|
|
6
|
+
const PACKAGE_ROOT = path.resolve(__dirname, "..");
|
|
7
|
+
const MIN_PYTHON = { major: 3, minor: 10 };
|
|
8
|
+
|
|
9
|
+
function exists(target) {
|
|
10
|
+
try {
|
|
11
|
+
return fs.existsSync(target);
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureDir(target) {
|
|
18
|
+
fs.mkdirSync(target, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getDefaultAppHome() {
|
|
22
|
+
const explicit = (process.env.TB_HOME || "").trim();
|
|
23
|
+
if (explicit) {
|
|
24
|
+
return path.resolve(explicit);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (process.platform === "darwin") {
|
|
28
|
+
return path.join(os.homedir(), "Library", "Application Support", "tb-order-sync");
|
|
29
|
+
}
|
|
30
|
+
if (process.platform === "win32") {
|
|
31
|
+
return path.join(
|
|
32
|
+
process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"),
|
|
33
|
+
"tb-order-sync",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return path.join(os.homedir(), ".tb-order-sync");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getAppHome() {
|
|
40
|
+
return getDefaultAppHome();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runtimeEnv(extra = {}) {
|
|
44
|
+
return {
|
|
45
|
+
...process.env,
|
|
46
|
+
TB_HOME: getAppHome(),
|
|
47
|
+
...extra,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runChecked(command, commandArgs, options = {}) {
|
|
52
|
+
const result = spawnSync(command, commandArgs, {
|
|
53
|
+
cwd: PACKAGE_ROOT,
|
|
54
|
+
stdio: "inherit",
|
|
55
|
+
env: runtimeEnv(),
|
|
56
|
+
...options,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (result.error) {
|
|
60
|
+
return { ok: false, error: result.error };
|
|
61
|
+
}
|
|
62
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
63
|
+
return { ok: false, code: result.status };
|
|
64
|
+
}
|
|
65
|
+
return { ok: true };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function probePythonVersion(command, prefixArgs) {
|
|
69
|
+
const probe = spawnSync(
|
|
70
|
+
command,
|
|
71
|
+
[
|
|
72
|
+
...prefixArgs,
|
|
73
|
+
"-c",
|
|
74
|
+
"import sys; print('.'.join(map(str, sys.version_info[:3])))",
|
|
75
|
+
],
|
|
76
|
+
{
|
|
77
|
+
cwd: PACKAGE_ROOT,
|
|
78
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
79
|
+
encoding: "utf-8",
|
|
80
|
+
env: runtimeEnv(),
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (probe.error || probe.status !== 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const raw = (probe.stdout || "").trim();
|
|
89
|
+
const match = raw.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
90
|
+
if (!match) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const version = {
|
|
95
|
+
major: Number(match[1]),
|
|
96
|
+
minor: Number(match[2]),
|
|
97
|
+
patch: Number(match[3]),
|
|
98
|
+
raw,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const isSupported = (
|
|
102
|
+
version.major > MIN_PYTHON.major ||
|
|
103
|
+
(version.major === MIN_PYTHON.major && version.minor >= MIN_PYTHON.minor)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
...version,
|
|
108
|
+
isSupported,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function findSystemPython() {
|
|
113
|
+
const candidates = process.platform === "win32"
|
|
114
|
+
? [
|
|
115
|
+
{ command: "py", prefixArgs: ["-3"] },
|
|
116
|
+
{ command: "python", prefixArgs: [] },
|
|
117
|
+
]
|
|
118
|
+
: [
|
|
119
|
+
{ command: "python3.14", prefixArgs: [] },
|
|
120
|
+
{ command: "python3.13", prefixArgs: [] },
|
|
121
|
+
{ command: "python3.12", prefixArgs: [] },
|
|
122
|
+
{ command: "python3.11", prefixArgs: [] },
|
|
123
|
+
{ command: "python3.10", prefixArgs: [] },
|
|
124
|
+
{ command: "python3", prefixArgs: [] },
|
|
125
|
+
{ command: "python", prefixArgs: [] },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
for (const candidate of candidates) {
|
|
129
|
+
const version = probePythonVersion(candidate.command, candidate.prefixArgs);
|
|
130
|
+
if (version && version.isSupported) {
|
|
131
|
+
return { ...candidate, version: version.raw };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getVenvDir() {
|
|
138
|
+
return path.join(getAppHome(), ".venv");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getVenvPython() {
|
|
142
|
+
if (process.platform === "win32") {
|
|
143
|
+
return path.join(getVenvDir(), "Scripts", "python.exe");
|
|
144
|
+
}
|
|
145
|
+
return path.join(getVenvDir(), "bin", "python");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getBundledExecutable() {
|
|
149
|
+
const candidates = process.platform === "win32"
|
|
150
|
+
? [
|
|
151
|
+
path.join(PACKAGE_ROOT, "sync_service.exe"),
|
|
152
|
+
path.join(PACKAGE_ROOT, "dist", "sync_service", "sync_service.exe"),
|
|
153
|
+
]
|
|
154
|
+
: [
|
|
155
|
+
path.join(PACKAGE_ROOT, "sync_service"),
|
|
156
|
+
path.join(PACKAGE_ROOT, "dist", "sync_service", "sync_service"),
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
return candidates.find(exists) || null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function ensurePythonRuntime(options = {}) {
|
|
163
|
+
const { quiet = false, exitOnError = true } = options;
|
|
164
|
+
const appHome = getAppHome();
|
|
165
|
+
const venvPython = getVenvPython();
|
|
166
|
+
ensureDir(appHome);
|
|
167
|
+
|
|
168
|
+
const fail = (lines) => {
|
|
169
|
+
const messages = Array.isArray(lines) ? lines : [lines];
|
|
170
|
+
if (!quiet) {
|
|
171
|
+
for (const line of messages) {
|
|
172
|
+
console.error(line);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (exitOnError) {
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
return { ok: false, messages };
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (exists(venvPython)) {
|
|
182
|
+
return { ok: true, command: venvPython, prefixArgs: [] };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const systemPython = findSystemPython();
|
|
186
|
+
if (!systemPython) {
|
|
187
|
+
return fail([
|
|
188
|
+
"tb: 未找到可用的 Python 3.10+,无法自动部署 CLI 运行环境。",
|
|
189
|
+
"tb: 请先安装 Python 3.10+,然后重新执行 npm install -g tb-order-sync。",
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!quiet) {
|
|
194
|
+
console.log(`tb: 使用 Python ${systemPython.version} 初始化运行环境...`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let result = runChecked(
|
|
198
|
+
systemPython.command,
|
|
199
|
+
[...systemPython.prefixArgs, "-m", "venv", getVenvDir()],
|
|
200
|
+
);
|
|
201
|
+
if (!result.ok) {
|
|
202
|
+
return fail("tb: 创建虚拟环境失败。");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const freshVenvPython = getVenvPython();
|
|
206
|
+
if (!quiet) {
|
|
207
|
+
console.log("tb: 正在安装 Python 依赖...");
|
|
208
|
+
}
|
|
209
|
+
result = runChecked(
|
|
210
|
+
freshVenvPython,
|
|
211
|
+
["-m", "pip", "install", "-q", "-r", path.join(PACKAGE_ROOT, "requirements.txt")],
|
|
212
|
+
);
|
|
213
|
+
if (!result.ok) {
|
|
214
|
+
return fail("tb: 安装依赖失败。");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { ok: true, command: freshVenvPython, prefixArgs: [] };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
PACKAGE_ROOT,
|
|
222
|
+
exists,
|
|
223
|
+
getAppHome,
|
|
224
|
+
getBundledExecutable,
|
|
225
|
+
getVenvPython,
|
|
226
|
+
ensurePythonRuntime,
|
|
227
|
+
runtimeEnv,
|
|
228
|
+
};
|
package/bin/tb.js
CHANGED
|
@@ -1,119 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs = require("fs");
|
|
4
3
|
const path = require("path");
|
|
5
|
-
const { spawn
|
|
4
|
+
const { spawn } = require("child_process");
|
|
5
|
+
const {
|
|
6
|
+
PACKAGE_ROOT,
|
|
7
|
+
exists,
|
|
8
|
+
getBundledExecutable,
|
|
9
|
+
ensurePythonRuntime,
|
|
10
|
+
runtimeEnv,
|
|
11
|
+
} = require("./runtime");
|
|
6
12
|
|
|
7
|
-
const root = path.resolve(__dirname, "..");
|
|
8
13
|
const args = process.argv.slice(2);
|
|
9
14
|
|
|
10
|
-
function exists(target) {
|
|
11
|
-
try {
|
|
12
|
-
return fs.existsSync(target);
|
|
13
|
-
} catch {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function runChecked(command, commandArgs, options = {}) {
|
|
19
|
-
const result = spawnSync(command, commandArgs, {
|
|
20
|
-
cwd: root,
|
|
21
|
-
stdio: "inherit",
|
|
22
|
-
...options,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
if (result.error) {
|
|
26
|
-
return { ok: false, error: result.error };
|
|
27
|
-
}
|
|
28
|
-
if (typeof result.status === "number" && result.status !== 0) {
|
|
29
|
-
return { ok: false, code: result.status };
|
|
30
|
-
}
|
|
31
|
-
return { ok: true };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function findSystemPython() {
|
|
35
|
-
const candidates = process.platform === "win32"
|
|
36
|
-
? [
|
|
37
|
-
{ command: "py", prefixArgs: ["-3"] },
|
|
38
|
-
{ command: "python", prefixArgs: [] },
|
|
39
|
-
]
|
|
40
|
-
: [
|
|
41
|
-
{ command: "python3.14", prefixArgs: [] },
|
|
42
|
-
{ command: "python3.13", prefixArgs: [] },
|
|
43
|
-
{ command: "python3.12", prefixArgs: [] },
|
|
44
|
-
{ command: "python3.11", prefixArgs: [] },
|
|
45
|
-
{ command: "python3", prefixArgs: [] },
|
|
46
|
-
{ command: "python", prefixArgs: [] },
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
for (const candidate of candidates) {
|
|
50
|
-
const probe = runChecked(candidate.command, [...candidate.prefixArgs, "--version"], {
|
|
51
|
-
stdio: "ignore",
|
|
52
|
-
});
|
|
53
|
-
if (probe.ok) {
|
|
54
|
-
return candidate;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function getVenvPython() {
|
|
61
|
-
if (process.platform === "win32") {
|
|
62
|
-
return path.join(root, ".venv", "Scripts", "python.exe");
|
|
63
|
-
}
|
|
64
|
-
return path.join(root, ".venv", "bin", "python");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function getBundledExecutable() {
|
|
68
|
-
const candidates = process.platform === "win32"
|
|
69
|
-
? [
|
|
70
|
-
path.join(root, "sync_service.exe"),
|
|
71
|
-
path.join(root, "dist", "sync_service", "sync_service.exe"),
|
|
72
|
-
]
|
|
73
|
-
: [
|
|
74
|
-
path.join(root, "sync_service"),
|
|
75
|
-
path.join(root, "dist", "sync_service", "sync_service"),
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
return candidates.find(exists) || null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function ensurePythonRuntime() {
|
|
82
|
-
const venvPython = getVenvPython();
|
|
83
|
-
if (exists(venvPython)) {
|
|
84
|
-
return { command: venvPython, prefixArgs: [] };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const systemPython = findSystemPython();
|
|
88
|
-
if (!systemPython) {
|
|
89
|
-
console.error("tb: 未找到可用的 Python 3.11+,也没有现成的打包可执行文件。");
|
|
90
|
-
console.error("tb: 请先安装 Python,或使用 build.py 构建可执行文件。");
|
|
91
|
-
process.exit(1);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
console.log("tb: 正在初始化 Python 虚拟环境...");
|
|
95
|
-
let result = runChecked(systemPython.command, [...systemPython.prefixArgs, "-m", "venv", ".venv"]);
|
|
96
|
-
if (!result.ok) {
|
|
97
|
-
console.error("tb: 创建虚拟环境失败。");
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const freshVenvPython = getVenvPython();
|
|
102
|
-
console.log("tb: 正在安装 Python 依赖...");
|
|
103
|
-
result = runChecked(freshVenvPython, ["-m", "pip", "install", "-q", "-r", "requirements.txt"]);
|
|
104
|
-
if (!result.ok) {
|
|
105
|
-
console.error("tb: 安装依赖失败。");
|
|
106
|
-
process.exit(1);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return { command: freshVenvPython, prefixArgs: [] };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
15
|
function launch(command, commandArgs) {
|
|
113
16
|
const child = spawn(command, commandArgs, {
|
|
114
|
-
cwd:
|
|
17
|
+
cwd: PACKAGE_ROOT,
|
|
115
18
|
stdio: "inherit",
|
|
116
|
-
env:
|
|
19
|
+
env: runtimeEnv(),
|
|
117
20
|
});
|
|
118
21
|
|
|
119
22
|
child.on("error", (error) => {
|
|
@@ -126,7 +29,7 @@ function launch(command, commandArgs) {
|
|
|
126
29
|
});
|
|
127
30
|
}
|
|
128
31
|
|
|
129
|
-
const mainEntry = path.join(
|
|
32
|
+
const mainEntry = path.join(PACKAGE_ROOT, "main.py");
|
|
130
33
|
const preferBundled = process.env.TB_PREFER_BUNDLED === "1" || !exists(mainEntry);
|
|
131
34
|
const bundledExecutable = getBundledExecutable();
|
|
132
35
|
|