systemd 服务里设置工作目录:WorkingDirectory

写 Python / Node 服务的 systemd 单元时,程序里各种相对路径都得基于"工作目录"来解读。systemd 提供了 WorkingDirectory 字段,比在 ExecStart 里 cd 一下再启动规范得多。 最简单的例子 [Unit] Description=My App After=network-online.target[Service] Type=simple User=appuser WorkingDirectory=/opt/myapp ExecStart=/usr/bin/python3 main.py Restart=always RestartSec=3[Install] WantedBy=multi-user.targetWorkingDirectory=/opt/myapp 的效果:进程启动时 cwd 就是这个目录 ExecStart 里的相对路径按它算(比如 python3 main.py 就是 /opt/myapp/main.py) Python 里 os.getcwd() 返回这个目录 写日志到 ./logs/app.log 就是 /opt/myapp/logs/app.log为什么不用 cd 见过这种写法: ExecStart=/bin/bash -c 'cd /opt/myapp && python3 main.py'能跑,但坏处一堆:多一层 shell,进程树多了个 bash 语义没那么直观 systemd 的 Restart / 信号处理会作用在 bash 上,不是真进程 环境变量、User、日志跟你想的不一样能用字段就用字段,cd 是最后的兜底。 常配套的字段 User / Group 以指定用户启动,不用 root 跑业务: User=appuser Group=appuser注意 WorkingDirectory 里的目录必须让这个用户能读、能进: sudo chown -R appuser:appuser /opt/myappEnvironmentFile 把环境变量放外面,方便修改: EnvironmentFile=/etc/myapp/envenv 文件内容: DATABASE_URL=postgres://user:pass@localhost/mydb APP_PORT=8000代码里 os.getenv("DATABASE_URL") 就能拿到。 Restart 崩溃后的行为: Restart=always # 无条件重启 RestartSec=3 # 3 秒后重启 StartLimitInterval=60 # 60 秒内 StartLimitBurst=5 # 最多重启 5 次,再多就放弃生产上都需要——不然崩了以后就得手动来。 日志 默认 stdout / stderr 会走 journald: journalctl -u myapp -f journalctl -u myapp --since "10 minutes ago"想直接写文件: StandardOutput=append:/var/log/myapp/out.log StandardError=append:/var/log/myapp/err.logappend: 需要 systemd 240+,老版本用 file:。 一个"生产可用"的模板 [Unit] Description=My FastAPI App After=network-online.target Wants=network-online.target[Service] Type=simple User=appuser Group=appuser WorkingDirectory=/opt/myapp EnvironmentFile=/etc/myapp/env ExecStart=/opt/myapp/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 Restart=always RestartSec=3 StartLimitBurst=5 StandardOutput=journal StandardError=journal[Install] WantedBy=multi-user.target启用: sudo systemctl daemon-reload sudo systemctl enable --now myapp sudo systemctl status myapp一句话总结 WorkingDirectory=/path 就是 cwd,不要在 ExecStart 里套 bash -c 'cd ... && ...'。搭配 User、EnvironmentFile、Restart 一份 systemd 单元就是完整的服务定义。

Windows 开机自启:脚本、EXE、Python 服务的四种做法

Windows 上"开机自启"根据程序性质要选不同方式。一股脑塞启动文件夹经常出问题——GUI 程序有个 UAC 弹窗、脚本要读用户目录、长驻服务要跑在 SYSTEM 下——分类拆开来看。 方式一:启动文件夹(普通程序) Win + R 打开: shell:startup这是当前用户的启动目录。把 .exe 或 .lnk 快捷方式扔进去就行。全用户共用: shell:common startup局限:用户登录后才启动 弹 UAC 会需要手动点 关了窗口就没了适合:桌面小工具、常用软件(VSCode、Chrome 等已经自带这个)。 方式二:NSSM 把脚本包成服务(推荐) 长驻的 Python、Node、FastAPI、Uvicorn 等脚本,最省心是用 NSSM。它能把任何 exe/bat/脚本包装成 Windows 服务。 管理员 CMD: nssm install MyService弹出 GUI,填三项: Application → Path: C:\Python311\python.exe Application → Arguments: -m uvicorn main:app --host 0.0.0.0 --port 8000 Application → Startup directory: D:\project设自动启动: sc config MyService start= auto(start= 和 auto 之间必须有空格。) 启动 / 停止 / 删除: net start MyService net stop MyService nssm remove MyService confirmNSSM 的好处:崩溃自动重启(Details 里配 Exit Action) stdout / stderr 可以重定向到日志文件 支持环境变量、依赖、恢复策略 卸载干净方式三:原生 sc create(真 EXE 程序) 已经写成了 Windows Service 兼容的 EXE: sc create MyService binPath= "D:\app\server.exe" start= auto sc start MyService sc delete MyService局限:只能接已经实现了 ServiceMain 的 EXE 普通脚本用这个会秒退,要靠 NSSM方式四:任务计划程序(脚本 / GUI / 用户上下文) 需要在用户登录后启动、或者要 GUI、或者 SYSTEM 下跑不了的: schtasks /create /tn "MyTask" ^ /tr "C:\Users\me\bin\worker.py" ^ /sc onlogon场景:只登录时启动:/sc onlogon 每次开机(不管登录):/sc onstart(需要管理员) 每天 03:00:/sc daily /st 03:00管理: schtasks /query /tn "MyTask" schtasks /run /tn "MyTask" schtasks /delete /tn "MyTask" /f选哪一种场景 推荐只想开机跑一次的桌面程序 启动文件夹Python/Node 长驻脚本、API、爬虫 NSSM(默认选它)已经是 Windows Service 兼容 EXE sc createGUI 程序、要用户桌面 session 任务计划 onlogon系统级、无用户登录也要跑 任务计划 onstart 或 NSSM一句话总结 长驻脚本 = NSSM,开机自动、崩溃自愈、日志齐全。其它类型再按上面表格分。

剪映批量导出自动化:AutoHotkey、draft_content.json 和 FFmpeg 三种方案

三种方案对比方案 适用场景 优点 缺点AutoHotkey 模拟 UI 几十个工程、保留特效 最稳、兼容所有剪映功能 慢、易被 UI 改版搞炸修改 draft_content.json 矩阵号、批量换素材/文案 工业化效率高 剪映 6+ 有加密问题FFmpeg / MoviePy 图片转视频、批量混剪 服务器可跑、不依赖剪映 无法保留剪映特效方案一:AutoHotkey(最稳) 适合"几十个工程挨个导出"的场景,不需要了解剪映内部格式: Loop, Files, D:\jianying_projects\*.draft { Run, "C:\Program Files\JianyingPro\JianyingPro.exe" "%A_LoopFileFullPath%" Sleep, 10000 ; 点击导出按钮(坐标需根据实际分辨率调整) Click, 1800, 1000 Sleep, 3000 ; 点击确认导出 Click, 1700, 950 ; 等待渲染完成 Sleep, 60000 WinClose, 剪映专业版 }坐标需要根据自己的屏幕分辨率和剪映版本调整。可以用 AHK 内置的 MouseGetPos 命令在悬停时获取当前坐标。 方案二:修改 draft_content.json(矩阵号核心) 剪映草稿本质是 JSON,位于草稿目录下的 draft_content.json。批量替换流程: 模板工程 ↓ Python 复制草稿目录 ↓ 修改 draft_content.json(替换素材路径/字幕文本) ↓ 生成 N 个草稿 ↓ AutoHotkey 自动打开逐个导出Python 批量复制并替换字幕示例: import shutil import json import re from pathlib import Pathtemplate = Path("D:/drafts/template") output_base = Path("D:/drafts/output")texts = ["第一集内容", "第二集内容", "第三集内容"]for i, text in enumerate(texts): dst = output_base / f"episode_{i+1}" shutil.copytree(template, dst) draft_file = dst / "draft_content.json" data = json.loads(draft_file.read_text(encoding="utf-8")) # 替换字幕文本(具体字段名以实际工程为准) content_str = json.dumps(data, ensure_ascii=False) content_str = re.sub(r'"占位文本"', f'"{text}"', content_str) draft_file.write_text(content_str, encoding="utf-8")注意:剪映 6+ 加密问题 从剪映专业版 6.x 开始,draft_content.json 部分字段开始加密,很多老方案失效。目前社区常见的做法:锁定在剪映 5.9 不升级 使用开源项目如 pyJianYingDraft 或 capcut-cli方案三:FFmpeg(不经过剪映) 如果只是图片转视频、批量加字幕、批量合并,完全不需要剪映: # 图片+音频合成视频 ffmpeg -loop 1 -i cover.jpg -i bgm.mp3 \ -shortest \ -vf "scale=1920:1080" \ output.mp4# 批量处理(bash) for img in *.jpg; do ffmpeg -loop 1 -i "$img" -i bgm.mp3 -shortest -vf "scale=1920:1080" "${img%.jpg}.mp4" doneFFmpeg 的缺点是没有剪映的智能字幕、卡点、AI 特效,对"剪映味"的视频替代不了。 实际推荐路线(混合方案) Python 改草稿 JSON(批量换内容) + 剪映负责特效渲染 + AutoHotkey 自动导出剪映最有价值的是特效引擎和 AI 功能,时间轴批量生成用代码做,特效用剪映渲染,导出用自动化点击。这是目前短视频矩阵账号的主流技术路线。

PyInstaller 打包 FastAPI + Uvicorn:启动文件写法与依赖收集

直接打包 uvicorn main:app 的方式容易出现模块找不到的问题,推荐单独写入口文件。 项目结构 project/ ├── main.py # FastAPI app 定义 ├── start.py # PyInstaller 打包入口 ├── templates/ # Jinja2 模板(如有) └── static/ # 静态文件(如有)入口文件 start.py 关键点:直接传 app 对象,不用字符串 import uvicorn from main import appif __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)字符串形式 uvicorn.run("main:app", ...) 在 PyInstaller 环境中无法动态导入,会报: Error loading ASGI app. Could not import module "main"打包命令 基础打包: pyinstaller -F start.pyFastAPI/Uvicorn 有大量动态导入,需要显式收集: pyinstaller -F start.py \ --collect-all fastapi \ --collect-all uvicorn \ --collect-all starlette \ --collect-all pydantic包含模板和静态文件(macOS/Linux 用 :,Windows 用 ;): # Linux/macOS pyinstaller -F start.py \ --collect-all fastapi \ --collect-all uvicorn \ --collect-all starlette \ --collect-all pydantic \ --add-data "templates:templates" \ --add-data "static:static"# Windows pyinstaller -F start.py ^ --collect-all fastapi ^ --collect-all uvicorn ^ --collect-all starlette ^ --collect-all pydantic ^ --add-data "templates;templates" ^ --add-data "static;static"打包后静态文件路径 打包后程序解压到临时目录,__file__ 路径会变。读取打包内的文件需要: import sys import osdef get_resource_path(relative_path): if getattr(sys, 'frozen', False): base = sys._MEIPASS # PyInstaller 临时目录 else: base = os.path.dirname(__file__) return os.path.join(base, relative_path)# 挂载静态文件 from fastapi.staticfiles import StaticFiles app.mount("/static", StaticFiles(directory=get_resource_path("static")), name="static")常见错误 ModuleNotFoundError: No module named 'uvicorn.loops' --collect-all uvicornpydantic validator 找不到 --collect-all pydanticmultiprocessing 相关报错 确保 start.py 有 if __name__ == "__main__": 保护,避免多进程递归启动。 生产环境建议 PyInstaller 打包适合桌面工具分发,生产服务器更推荐:Docker 容器(环境固定、更新方便) systemd + venv(直接部署,无需打包) Supervisor 管理进程打包后可执行文件体积通常在 50-150 MB,启动时间比直接运行慢 3-10 秒。

PyInstaller 打包 FastAPI + uvicorn:能跑起来的最小写法

想把 FastAPI 项目打包成一个 exe 发给非技术用户,直接: pyinstaller -F main.py跑一下崩得七七八八。因为 uvicorn 走的是字符串式导入,PyInstaller 环境下拿不到;再加上一堆子模块(uvicorn.loops.*、starlette.*)静态分析捞不全。 正确起步:写个 start.py 不要直接打 main.py,写个显式导入 app 对象的启动文件: # start.py from main import app # 直接拿对象,不用字符串 import uvicornif __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)对应的 main.py: from fastapi import FastAPI app = FastAPI()@app.get("/") async def root(): return {"hello": "world"}打包 start.py: pyinstaller -F start.pyCould not import module "main" 的原因 如果 start.py 里写: uvicorn.run("main:app", host="0.0.0.0", port=8000)main:app 是字符串,uvicorn 会在运行时 importlib.import_module("main")。PyInstaller 打包后 main 不在 sys.path 里,直接崩: Error loading ASGI app. Could not import module "main"改成直接 import + 传对象最省事: from main import app uvicorn.run(app, ...)或者告诉 PyInstaller 也带上 main: pyinstaller -F start.py --hidden-import=mainuvicorn 子模块丢失 跑起来抛: ModuleNotFoundError: No module named 'uvicorn.loops'uvicorn 有一堆按需加载的 loop / protocol / logger 子模块,PyInstaller 全漏了。一次性带上: pyinstaller -F start.py \ --collect-all fastapi \ --collect-all uvicorn \ --collect-all starlette \ --collect-all pydantic--collect-all X = 把 X 及其所有子模块、数据文件、二进制全打进去。 静态文件 / 模板 用了 StaticFiles 或 Jinja2Templates 得加数据: Windows: pyinstaller -F start.py ^ --add-data "templates;templates" ^ --add-data "static;static"Linux/macOS: pyinstaller -F start.py \ --add-data "templates:templates" \ --add-data "static:static"代码里读路径要兼容 PyInstaller 临时目录: import sys, os from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templatesdef base_path(): return getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))app.mount("/static", StaticFiles(directory=os.path.join(base_path(), "static")), name="static") templates = Jinja2Templates(directory=os.path.join(base_path(), "templates"))后台运行、无 CMD 窗口 想双击不弹黑框: pyinstaller -F -w start.py --collect-all uvicorn ...-w 关控制台。注意:关了控制台,print / uvicorn 的日志就看不到了,改成写日志文件: uvicorn.run( app, host="0.0.0.0", port=8000, log_config={ "version": 1, "handlers": { "file": {"class": "logging.FileHandler", "filename": "app.log"}, }, "root": {"handlers": ["file"], "level": "INFO"}, }, )完整命令模板 一次搞定: pyinstaller -F -w start.py ^ --name myapi ^ --icon app.ico ^ --add-data "templates;templates" ^ --add-data "static;static" ^ --collect-all fastapi ^ --collect-all uvicorn ^ --collect-all starlette ^ --collect-all pydantic产物:dist/myapi.exe,双击就是一个后台服务。 什么时候别打包生产环境线上服务:Docker / systemd 更好 要热更新:exe 每次改代码都要重打 依赖 GPU / CUDA:打包体动辄几百 MB 多用户 / 高并发:exe 里塞 uvicorn 效率一般PyInstaller 更适合:内网小工具、给客户发一个"点两下就跑"的本地 API、演示 demo。 一句话总结 写 start.py + 直接 import app + --collect-all 四个关键字。字符串导入是坑,把 uvicorn 家族收全就通了。