tb-order-sync 0.4.0 → 0.4.2
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/CHANGELOG.md +27 -0
- package/README.md +16 -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/setup.py +6 -4
- package/config/settings.py +50 -7
- package/package.json +3 -2
- package/services/daemon_service.py +12 -4
- package/sync_service.spec +1 -0
- package//345/277/253/351/200/237/345/274/200/345/247/213.txt +16 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.2] - 2026-03-19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added npm global-install bootstrap via `postinstall`, so `npm install -g tb-order-sync` can prepare the CLI runtime in one step on macOS.
|
|
10
|
+
- Added dedicated runtime path handling for npm installs, using a writable user app-home instead of the npm package directory.
|
|
11
|
+
- Added tests for `TB_HOME` path resolution and relative `state_dir` normalization.
|
|
12
|
+
- Added `pytest.ini` to keep local release artifacts from polluting test discovery.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Changed the Node launcher to store `.env`, `state`, and `.venv` under the app home during npm-based CLI usage.
|
|
17
|
+
- Changed documentation to describe the one-command npm install flow and the macOS runtime directory.
|
|
18
|
+
|
|
19
|
+
## [0.4.1] - 2026-03-19
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Added standard GitHub Release packaging workflow for macOS and Windows.
|
|
24
|
+
- Added complete distribution package output including startup scripts, `.env.example`, `快速开始.txt`, and `公司同事使用说明.md`.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Changed release packaging to produce platform-specific archives:
|
|
29
|
+
- `tb-order-sync-macos-x64-<version>.zip`
|
|
30
|
+
- `tb-order-sync-windows-x64-<version>.zip`
|
|
31
|
+
|
|
5
32
|
## [0.4.0] - 2026-03-19
|
|
6
33
|
|
|
7
34
|
### 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 表是否可用
|
|
@@ -67,6 +76,10 @@ tb daemon status
|
|
|
67
76
|
| Python 兼容入口 | 调试或源码环境 | `python main.py` |
|
|
68
77
|
| 打包分发 | 免安装交付 | `dist/sync_service/` |
|
|
69
78
|
|
|
79
|
+
GitHub Release 现已提供标准完整分发包:
|
|
80
|
+
- Windows: `tb-order-sync-windows-x64-<version>.zip`
|
|
81
|
+
- macOS: `tb-order-sync-macos-bootstrap-<version>.zip`
|
|
82
|
+
|
|
70
83
|
### 常用命令速查
|
|
71
84
|
|
|
72
85
|
```bash
|
|
@@ -192,7 +205,9 @@ tb-order-sync/
|
|
|
192
205
|
├── 启动.bat # Windows 一键启动
|
|
193
206
|
├── 启动.command # macOS 一键启动
|
|
194
207
|
├── bin/
|
|
195
|
-
│
|
|
208
|
+
│ ├── tb.js # npm / node 统一入口
|
|
209
|
+
│ ├── runtime.js # npm 运行时 bootstrap
|
|
210
|
+
│ └── postinstall.js # npm 全局安装自动部署
|
|
196
211
|
│
|
|
197
212
|
├── config/
|
|
198
213
|
│ ├── settings.py # Pydantic Settings 全局配置
|
|
@@ -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
|
|
package/build.py
CHANGED
package/cli/setup.py
CHANGED
|
@@ -28,7 +28,7 @@ except ImportError:
|
|
|
28
28
|
|
|
29
29
|
from dotenv import dotenv_values
|
|
30
30
|
|
|
31
|
-
from config.settings import
|
|
31
|
+
from config.settings import APP_HOME, PACKAGE_ROOT
|
|
32
32
|
|
|
33
33
|
# ── UI 文案 ────────────────────────────────────────────────────────────────
|
|
34
34
|
BANNER_TITLE = "多表格同步服务 — 配置向导"
|
|
@@ -43,8 +43,8 @@ STEP_SUMMARY = "配置总览"
|
|
|
43
43
|
STEP_WRITE = "写入配置"
|
|
44
44
|
STEP_TEST = "连接测试"
|
|
45
45
|
|
|
46
|
-
ENV_PATH =
|
|
47
|
-
ENV_EXAMPLE_PATH =
|
|
46
|
+
ENV_PATH = APP_HOME / ".env"
|
|
47
|
+
ENV_EXAMPLE_PATH = PACKAGE_ROOT / ".env.example"
|
|
48
48
|
|
|
49
49
|
# Column letter validator
|
|
50
50
|
_COL_RE = re.compile(r"^[A-Z]{1,3}$")
|
|
@@ -551,7 +551,9 @@ class SetupWizard:
|
|
|
551
551
|
self.console.print(f"\n[bold cyan]🔌 {STEP_TEST}[/bold cyan]")
|
|
552
552
|
self.console.print(" 正在执行启动自检:状态目录 + 腾讯文档 A/B 表读取...\n")
|
|
553
553
|
|
|
554
|
-
state_dir = Path(self.values.get("STATE_DIR", "state"))
|
|
554
|
+
state_dir = Path(self.values.get("STATE_DIR", "state")).expanduser()
|
|
555
|
+
if not state_dir.is_absolute():
|
|
556
|
+
state_dir = APP_HOME / state_dir
|
|
555
557
|
try:
|
|
556
558
|
state_dir.mkdir(parents=True, exist_ok=True)
|
|
557
559
|
probe = state_dir / ".write_test"
|
package/config/settings.py
CHANGED
|
@@ -8,18 +8,49 @@ from enum import Enum
|
|
|
8
8
|
from functools import lru_cache
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
|
-
from pydantic import
|
|
11
|
+
from pydantic import field_validator
|
|
12
12
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
13
|
|
|
14
|
-
def
|
|
15
|
-
"""Resolve
|
|
14
|
+
def _get_package_root() -> Path:
|
|
15
|
+
"""Resolve package root, compatible with PyInstaller frozen exe."""
|
|
16
16
|
if getattr(sys, "frozen", False):
|
|
17
|
-
# Running as packaged exe — use exe's directory as root
|
|
18
17
|
return Path(sys.executable).resolve().parent
|
|
19
18
|
return Path(__file__).resolve().parent.parent
|
|
20
19
|
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
def _looks_like_global_node_package(root: Path) -> bool:
|
|
22
|
+
"""Best-effort detection for an npm global package install."""
|
|
23
|
+
parts = {part.lower() for part in root.parts}
|
|
24
|
+
return root.name == "tb-order-sync" and "node_modules" in parts
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _default_app_home() -> Path:
|
|
28
|
+
"""Resolve the writable runtime home for config/state/venv."""
|
|
29
|
+
package_root = _get_package_root()
|
|
30
|
+
explicit = os.environ.get("TB_HOME", "").strip()
|
|
31
|
+
if explicit:
|
|
32
|
+
return Path(explicit).expanduser().resolve()
|
|
33
|
+
|
|
34
|
+
if getattr(sys, "frozen", False):
|
|
35
|
+
return package_root
|
|
36
|
+
|
|
37
|
+
if _looks_like_global_node_package(package_root):
|
|
38
|
+
home = Path.home()
|
|
39
|
+
if os.name == "nt":
|
|
40
|
+
appdata = os.environ.get("APPDATA")
|
|
41
|
+
if appdata:
|
|
42
|
+
return Path(appdata).expanduser().resolve() / "tb-order-sync"
|
|
43
|
+
return (home / "AppData" / "Roaming" / "tb-order-sync").resolve()
|
|
44
|
+
if sys.platform == "darwin":
|
|
45
|
+
return (home / "Library" / "Application Support" / "tb-order-sync").resolve()
|
|
46
|
+
return (home / ".tb-order-sync").resolve()
|
|
47
|
+
|
|
48
|
+
return package_root
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
PACKAGE_ROOT = _get_package_root()
|
|
52
|
+
APP_HOME = _default_app_home()
|
|
53
|
+
PROJECT_ROOT = PACKAGE_ROOT
|
|
23
54
|
|
|
24
55
|
|
|
25
56
|
class AppEnv(str, Enum):
|
|
@@ -37,7 +68,7 @@ class Settings(BaseSettings):
|
|
|
37
68
|
"""All configuration knobs, sourced from .env / environment variables."""
|
|
38
69
|
|
|
39
70
|
model_config = SettingsConfigDict(
|
|
40
|
-
env_file=os.environ.get("DOTENV_PATH", str(
|
|
71
|
+
env_file=os.environ.get("DOTENV_PATH", str(APP_HOME / ".env")),
|
|
41
72
|
env_file_encoding="utf-8",
|
|
42
73
|
case_sensitive=False,
|
|
43
74
|
extra="ignore",
|
|
@@ -46,7 +77,7 @@ class Settings(BaseSettings):
|
|
|
46
77
|
# ── 基础 ──────────────────────────────────────────────
|
|
47
78
|
app_env: AppEnv = AppEnv.DEV
|
|
48
79
|
log_level: str = "INFO"
|
|
49
|
-
state_dir: str = str(
|
|
80
|
+
state_dir: str = str(APP_HOME / "state")
|
|
50
81
|
|
|
51
82
|
# ── 腾讯文档 ──────────────────────────────────────────
|
|
52
83
|
tencent_client_id: str = ""
|
|
@@ -89,6 +120,18 @@ class Settings(BaseSettings):
|
|
|
89
120
|
refund_status_text: str = "已退款"
|
|
90
121
|
data_error_text: str = "数据异常"
|
|
91
122
|
|
|
123
|
+
@field_validator("state_dir", mode="before")
|
|
124
|
+
@classmethod
|
|
125
|
+
def _resolve_state_dir(cls, value: object) -> str:
|
|
126
|
+
"""Resolve relative state_dir values against APP_HOME."""
|
|
127
|
+
if value in (None, ""):
|
|
128
|
+
return str(APP_HOME / "state")
|
|
129
|
+
|
|
130
|
+
path = Path(str(value)).expanduser()
|
|
131
|
+
if not path.is_absolute():
|
|
132
|
+
path = APP_HOME / path
|
|
133
|
+
return str(path.resolve())
|
|
134
|
+
|
|
92
135
|
|
|
93
136
|
@lru_cache(maxsize=1)
|
|
94
137
|
def get_settings() -> Settings:
|
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.2",
|
|
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"
|
package/sync_service.spec
CHANGED
|
@@ -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
|
- 接口限流:稍等后重试,不要连续高频重复执行
|