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 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
- └── tb.js # npm / node 统一入口
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, spawnSync } = require("child_process");
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: root,
17
+ cwd: PACKAGE_ROOT,
115
18
  stdio: "inherit",
116
- env: process.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(root, "main.py");
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
@@ -22,6 +22,7 @@ DIST_RUNTIME_FILES = [
22
22
  "启动.bat",
23
23
  "启动.command",
24
24
  "快速开始.txt",
25
+ "公司同事使用说明.md",
25
26
  ]
26
27
 
27
28
 
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 PROJECT_ROOT
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 = PROJECT_ROOT / ".env"
47
- ENV_EXAMPLE_PATH = PROJECT_ROOT / ".env.example"
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"
@@ -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 Field
11
+ from pydantic import field_validator
12
12
  from pydantic_settings import BaseSettings, SettingsConfigDict
13
13
 
14
- def _get_project_root() -> Path:
15
- """Resolve project root, compatible with PyInstaller frozen exe."""
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
- PROJECT_ROOT = _get_project_root()
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(PROJECT_ROOT / ".env")),
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(PROJECT_ROOT / "state")
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.0",
4
- "description": "Tencent Docs order sync service with gross-profit automation, refund marking, startup self-check, autostart daemon, and tb CLI",
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 PROJECT_ROOT, Settings
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(PROJECT_ROOT),
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(PROJECT_ROOT / "main.py"), "schedule"]
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
- f" <key>WorkingDirectory</key>\n <string>{PROJECT_ROOT}</string>\n"
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
@@ -23,6 +23,7 @@ a = Analysis(
23
23
  # Bundle .env.example so first-run setup can use it as template
24
24
  (str(root / '.env.example'), '.'),
25
25
  (str(root / '快速开始.txt'), '.'),
26
+ (str(root / '公司同事使用说明.md'), '.'),
26
27
  ],
27
28
  hiddenimports=[
28
29
  'config',
@@ -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
- 2. 首次运行
12
+ 3. 首次运行
8
13
  - 如果本机还没配置,程序会自动进入 setup 配置向导
9
14
  - 配好腾讯文档信息后,建议先执行一次 `tb check`
10
15
 
11
- 3. 常用命令
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
- 4. 登录自启
23
+ 5. 登录自启
19
24
  - tb daemon autostart-enable
20
25
  - tb daemon autostart-status
21
26
  - tb daemon autostart-disable
22
27
 
23
- 5. 日志与状态文件
24
- - state/scheduler.console.log
25
- - state/last_run.json
26
- - state/sync_state.json
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
- 6. 常见问题
35
+ 7. 常见问题
29
36
  - 缺配置:运行 tb setup
30
37
  - 想做启动自检:运行 tb check
31
38
  - 接口限流:稍等后重试,不要连续高频重复执行