fix(env): 本机启动读取根目录 .env,监听地址兼容 BIND_ADDR
- study_tool/__init__.py 用 python-dotenv 加载根目录 .env,位置早于 store / config 读环境变量;已有环境变量优先,Docker 注入的不受影响 - app.py 监听地址改为 HOST -> BIND_ADDR -> 127.0.0.1 依次回退 - 新增依赖 python-dotenv>=1.0(pyproject.toml / requirements.txt / uv.lock) - README 环境变量表补充 .env 说明与 BIND_ADDR / TZ 两行 - CHANGELOG / docs/OPTIMIZATION.md 记录本次修复,版本号 0.0.6 -> 0.0.7
This commit is contained in:
@@ -37,6 +37,8 @@
|
||||
- 删除账号只清了按账号 id 记的 `run_state` / `run_log`,登录日志(key 与 `login_log.username` 记的是用户名 / 手机号)残留:`account_delete()` 改为按账号 id + 用户名一起清,`DELETE /api/accounts/{id}` 同时清掉内存里两个 key 的日志
|
||||
- 学习链路上三处引用未定义的全局名(`platforms/jspx.py` 的 `extract_study_token` / `StudyClient` / `save_snapshot` 等、`platforms/study.py` 的 `parse_qs`、`quiz.py` 的 `BeautifulSoup` / `urlsplit`)会让课程发现 / 答题直接 `NameError`:`parse_qs`、`BeautifulSoup`、`urlsplit` 改为模块顶部导入,`jspx` ↔ `study` 之间改用函数内延迟导入(模块级会成环)
|
||||
- 服务重启留下的「已中断」状态在重新登录后仍然压在卡片上,徽标显示「已中断」并挂着旧进度和红条:登录成功后清掉该账号上一轮已结束的运行状态(内存 + SQLite),正在跑(`discovering` / `studying`)的不动,内存日志保留
|
||||
- 本机启动(`python app.py` / `uv run app.py`)时根目录 `.env` 完全无效:只有 docker compose 会读它,`AUTH_TOKEN` / `PORT` / `RUN_LOG_KEEP` / `SNAPSHOT_KEEP` 都得靠命令行环境变量;现在 `study_tool/__init__.py` 用 `python-dotenv` 加载 `.env`(已存在的环境变量优先,Docker 由 compose 注入的不受影响),新增依赖 `python-dotenv>=1.0`
|
||||
- `.env` 里写 `BIND_ADDR` 在本机启动时不起作用(代码只读 `HOST`):监听地址改为 `HOST` → `BIND_ADDR` → `127.0.0.1` 依次回退
|
||||
|
||||
### 其他
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ set PORT=8849 && python app.py
|
||||
set HOST=0.0.0.0 && python app.py
|
||||
```
|
||||
|
||||
也可以不 set:把 `.env.example` 复制成根目录 `.env` 再改(`cp .env.example .env`),本机启动同样读它。
|
||||
同名系统环境变量优先于 `.env`;注意 `.env` 里 `AUTH_TOKEN` 非空时,本机访问也要 `http://127.0.0.1:8848/?token=<值>`。
|
||||
|
||||
### 方式二:Docker(推荐用于远程管理)
|
||||
|
||||
```bash
|
||||
@@ -59,17 +62,19 @@ docker compose logs -f # 看启动日志
|
||||
| 进容器 | `docker compose exec study-tool bash` |
|
||||
| 换端口 | `.env` 里改 `PORT`,再 `docker compose up -d` |
|
||||
|
||||
环境变量:
|
||||
环境变量(写在根目录 `.env` 里,本机启动和 Docker 都读;同名的系统环境变量优先):
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `HOST` | `127.0.0.1` | 监听地址;Docker 里是 `0.0.0.0` |
|
||||
| `BIND_ADDR` | 同 `HOST` | `HOST` 的别名(docker compose 用它绑宿主机端口),本机启动也认 |
|
||||
| `PORT` | `8848` | 端口 |
|
||||
| `DB_PATH` | `<项目>/data/study.db` | SQLite 库文件 |
|
||||
| `AUTH_TOKEN` | 空 | 非空 = 页面/接口需要 `?token=` 或 cookie;`/api/health` 永远放行 |
|
||||
| `RUN_LOG_KEEP` | `2000` | 每个账号保留的日志行数 |
|
||||
| `SNAPSHOT_KEEP` | `200` | 课程页快照保留条数 |
|
||||
| `NODE_BIN` | 自动找 | node.exe 路径(算登录表单的 DES) |
|
||||
| `TZ` | `Asia/Shanghai` | 只对 Docker(Linux 容器)有效,Windows 本机忽略 |
|
||||
|
||||
镜像默认装 Chromium(直链视频要真播到结尾)。不需要就用
|
||||
`docker compose build --build-arg WITH_PLAYWRIGHT=0`,镜像能小一大截。
|
||||
@@ -186,7 +191,7 @@ script/reference/ 不参与运行的参考脚本:油猴 dgjy-toolkit.
|
||||
start.bat 一键启动(Windows)
|
||||
Dockerfile Docker 镜像(python:3.12-slim + nodejs + chromium)
|
||||
docker-compose.yml Docker 编排:端口、数据卷、AUTH_TOKEN、健康检查
|
||||
.env.example 远程管理用的环境变量样例(复制成 .env)
|
||||
.env.example 环境变量样例(复制成 .env;本机启动与 Docker 都读)
|
||||
requirements.txt 依赖
|
||||
data/study.db SQLite 库(账号/设置/日志/快照)
|
||||
docs/architecture.md 分层说明 + 平台插件怎么加
|
||||
|
||||
@@ -2,6 +2,27 @@
|
||||
|
||||
已实现、但还需要你实测确认的事项。确认没问题后把对应条目删掉。
|
||||
|
||||
## 本机启动读取根目录 .env
|
||||
|
||||
**状态**:已实现,待实测确认。
|
||||
|
||||
### 做了什么
|
||||
|
||||
- 修问题:以前只有 docker compose 会读 `.env`,本机 `python app.py` / `uv run app.py` / `start.bat`
|
||||
下 `AUTH_TOKEN` / `PORT` / `RUN_LOG_KEEP` / `SNAPSHOT_KEEP` 全部不生效。
|
||||
- `study_tool/__init__.py` 用 `python-dotenv` 加载仓库根 `.env`;放在包初始化处,早于 store / config
|
||||
读环境变量(`store.py` 是 import 时就读 `DB_PATH` / `RUN_LOG_KEEP`)。已存在的环境变量优先,Docker 由 compose 注入的不受影响。
|
||||
- 新依赖 `python-dotenv>=1.0`(`pyproject.toml` / `requirements.txt`,需 `uv sync`)。
|
||||
- `app.py` 监听地址改成 `HOST` → `BIND_ADDR` → `127.0.0.1`,`.env` 里的 `BIND_ADDR` 本机也生效。
|
||||
- README 环境变量表补 `.env` 说明与 `BIND_ADDR` / `TZ` 两行;`.env.example` 说明改为「本机与 Docker 都读」。
|
||||
|
||||
### 怎么测
|
||||
|
||||
1. 根目录有 `.env`(`AUTH_TOKEN` 非空)时直接 `python app.py`,启动日志里的地址应带 `?token=`。
|
||||
2. 不带 token 开 `http://127.0.0.1:8848/` 应显示「需要访问令牌」页;带 `?token=<值>` 正常进控制台。
|
||||
3. `.env` 改 `PORT=8849` 重启 → 实际监听 8849;命令行 `set PORT=8850` 再启动应压过 `.env`。
|
||||
4. `.env` 里 `BIND_ADDR=127.0.0.1` → 只绑本机;改 `0.0.0.0` → 同网段可访问。
|
||||
|
||||
## 插件热加载 + 启用/停用清单
|
||||
|
||||
**状态**:已实现,待实测确认。
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
name: study-tool
|
||||
version: 0.0.6
|
||||
version: 0.0.7
|
||||
description: 东莞市教师培训平台(jspx.dgjy.net / study.dgjy.net)多账号学习控制台
|
||||
|
||||
@@ -15,6 +15,7 @@ dependencies = [
|
||||
"lxml>=5.0",
|
||||
"playwright>=1.40",
|
||||
"ddddocr",
|
||||
"python-dotenv>=1.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,3 +5,4 @@ beautifulsoup4>=4.12
|
||||
lxml>=5.0
|
||||
playwright>=1.40
|
||||
ddddocr
|
||||
python-dotenv>=1.0
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
"""多账号学习控制台(东莞市教师培训平台)。"""
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 仓库根目录的 .env:本机启动(python app.py / uv run app.py)也按它配置。
|
||||
# 必须在任何子模块之前加载 —— store / config 在 import 时就读环境变量;
|
||||
# 已经在环境里的变量优先(Docker 由 compose 注入,不会被 .env 覆盖)。
|
||||
_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
load_dotenv(os.path.join(_ROOT, ".env"))
|
||||
|
||||
__version__ = "0.0.1"
|
||||
|
||||
@@ -166,8 +166,8 @@ def _probe_self(port):
|
||||
|
||||
def main():
|
||||
port = int(os.environ.get("PORT", "8848"))
|
||||
# 默认只监听本机;Docker / 远程管理用 HOST=0.0.0.0
|
||||
host = os.environ.get("HOST", "127.0.0.1")
|
||||
# 默认只监听本机;Docker / 远程管理用 HOST=0.0.0.0(.env 里写 BIND_ADDR 同样生效)
|
||||
host = os.environ.get("HOST") or os.environ.get("BIND_ADDR") or "127.0.0.1"
|
||||
url = "http://127.0.0.1:%d" % port
|
||||
# 设了访问令牌时,打印出来的地址一律带上 ?token=:本机访问同样要校验令牌
|
||||
suffix = ("/?token=%s" % AUTH_TOKEN) if AUTH_TOKEN else ""
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
"""莞易学平台:jspx.dgjy.net(登录入口)与 study.dgjy.net(学习入口)是同一个平台。
|
||||
|
||||
账号在「添加账号」里选择平台(`accounts.platform`),登录 / 刷新课程 / 发现课程都会
|
||||
走这里的 `login` / `list_courses` / `discover`;学习时 `discover` 会把页面代际写进
|
||||
`course["generation"]`(legacy / spa),由 `study` 分发到对应实现。
|
||||
新增其他平台时,照本文件写一个 `Platform` 子类并在 `entries` 声明登录 / 学习入口。
|
||||
"""
|
||||
from study_tool.platforms import jspx, registry, study
|
||||
from study_tool.platforms.base import Platform
|
||||
|
||||
|
||||
class Guanyixue(Platform):
|
||||
id = "guanyixue"
|
||||
label = "莞易学"
|
||||
# 登录入口 / 学习入口;命中任意一个就归到莞易学
|
||||
entries = ("jspx.dgjy.net", "study.dgjy.net", "dgjy.net")
|
||||
|
||||
def session(self, cookies=None):
|
||||
return jspx.new_session(cookies)
|
||||
|
||||
def dump_cookies(self, s):
|
||||
return jspx.dump_cookies(s)
|
||||
|
||||
def login(self, username, password):
|
||||
return jspx.login(username, password)
|
||||
|
||||
def list_courses(self, s):
|
||||
return jspx.scrape_courses(s)
|
||||
|
||||
def discover(self, s, plan_id, project_id, name=""):
|
||||
return jspx.discover(s, plan_id, project_id, name)
|
||||
|
||||
def renew_session(self, s):
|
||||
return jspx.ensure_jspx_session(s)
|
||||
|
||||
def profile(self, s):
|
||||
return jspx.whoami(jspx.fetch_admin(s))
|
||||
|
||||
def study(self, key, s, d, **opts):
|
||||
if d.get("generation") == "spa":
|
||||
return study._study(key, s, d, **opts)
|
||||
return jspx._study(key, s, d, **opts)
|
||||
|
||||
|
||||
PLATFORM = registry.register(Guanyixue())
|
||||
@@ -0,0 +1,45 @@
|
||||
"""莞易学平台:jspx.dgjy.net(登录入口)与 study.dgjy.net(学习入口)是同一个平台。
|
||||
|
||||
账号在「添加账号」里选择平台(`accounts.platform`),登录 / 刷新课程 / 发现课程都会
|
||||
走这里的 `login` / `list_courses` / `discover`;学习时 `discover` 会把页面代际写进
|
||||
`course["generation"]`(legacy / spa),由 `study` 分发到对应实现。
|
||||
新增其他平台时,照本文件写一个 `Platform` 子类并在 `entries` 声明登录 / 学习入口。
|
||||
"""
|
||||
from study_tool.platforms import jspx, registry, study
|
||||
from study_tool.platforms.base import Platform
|
||||
|
||||
|
||||
class Guanyixue(Platform):
|
||||
id = "guanyixue"
|
||||
label = "莞易学"
|
||||
# 登录入口 / 学习入口;命中任意一个就归到莞易学
|
||||
entries = ("jspx.dgjy.net", "study.dgjy.net", "dgjy.net")
|
||||
|
||||
def session(self, cookies=None):
|
||||
return jspx.new_session(cookies)
|
||||
|
||||
def dump_cookies(self, s):
|
||||
return jspx.dump_cookies(s)
|
||||
|
||||
def login(self, username, password):
|
||||
return jspx.login(username, password)
|
||||
|
||||
def list_courses(self, s):
|
||||
return jspx.scrape_courses(s)
|
||||
|
||||
def discover(self, s, plan_id, project_id, name=""):
|
||||
return jspx.discover(s, plan_id, project_id, name)
|
||||
|
||||
def renew_session(self, s):
|
||||
return jspx.ensure_jspx_session(s)
|
||||
|
||||
def profile(self, s):
|
||||
return jspx.whoami(jspx.fetch_admin(s))
|
||||
|
||||
def study(self, key, s, d, **opts):
|
||||
if d.get("generation") == "spa":
|
||||
return study._study(key, s, d, **opts)
|
||||
return jspx._study(key, s, d, **opts)
|
||||
|
||||
|
||||
PLATFORM = registry.register(Guanyixue())
|
||||
|
||||
@@ -1380,6 +1380,15 @@ wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.4"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
|
||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0" }
|
||||
wheels = [
|
||||
{ url = "https://mirrors.aliyun.com/pypi/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.2"
|
||||
@@ -1465,6 +1474,7 @@ dependencies = [
|
||||
{ name = "fastapi" },
|
||||
{ name = "lxml" },
|
||||
{ name = "playwright" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "requests" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
@@ -1476,6 +1486,7 @@ requires-dist = [
|
||||
{ name = "fastapi", specifier = ">=0.110" },
|
||||
{ name = "lxml", specifier = ">=5.0" },
|
||||
{ name = "playwright", specifier = ">=1.40" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0" },
|
||||
{ name = "requests", specifier = ">=2.31" },
|
||||
{ name = "uvicorn", specifier = ">=0.27" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user