feat: 添加配置读取工具,支持从环境变量和 .env 文件加载 WEREAD_API_KEY

fix: 修复启动脚本中的 Python 解释器路径识别
style: 更新样式以支持按钮最小宽度和灵活布局
docs: 添加项目最终文档,整合各类信息与说明
This commit is contained in:
admin committed 2026-07-17 22:11:27 +08:00
1 parent 3d5a402117
commit 92a48d14cd
9 files changed
+461 -20

No files matched your search

+4
View File
@@ -29,3 +29,7 @@ Thumbs.db
# Env
.env
.server.pid
# Tool artifacts
.qoder/
.workbuddy/
+14 -8
View File
@@ -20,6 +20,7 @@ import json
import os
import ssl
import subprocess
import sys
import time
import urllib.request
from datetime import datetime
@@ -27,6 +28,8 @@ from functools import wraps
import pymysql
from config_utils import get_env_value
try:
import certifi
_ssl_context = ssl.create_default_context(cafile=certifi.where())
@@ -56,7 +59,6 @@ MYSQL_CONFIG = {
# 微信读书 API 配置
# ======================================================================
WEREAD_API_URL = "https://i.weread.qq.com/api/agent/gateway"
WEREAD_API_KEY = os.environ.get("WEREAD_API_KEY", "")
WEREAD_SKILL_VERSION = "1.0.4"
@@ -154,13 +156,17 @@ def weread_api_call(api_name, **kwargs):
Raises:
Exception: API 调用失败时抛出异常
"""
api_key = get_env_value("WEREAD_API_KEY", "")
if not api_key:
raise RuntimeError("WEREAD_API_KEY 未设置,请检查当前终端环境、项目 .env 或 ~/.zshrc")
body = {"api_name": api_name, "skill_version": WEREAD_SKILL_VERSION}
body.update(kwargs)
req = urllib.request.Request(
WEREAD_API_URL,
data=json.dumps(body).encode("utf-8"),
headers={
"Authorization": f"Bearer {WEREAD_API_KEY}",
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
@@ -1199,14 +1205,14 @@ def api_search(conn):
def sync_data():
"""运行 fetch_data.py + migrate_to_mysql.py,全量同步数据"""
project_dir = os.path.dirname(os.path.abspath(__file__))
python_bin = os.path.join(
os.path.expanduser("~"),
".workbuddy/binaries/python/envs/default/bin/python",
)
python_bin = sys.executable or "python3"
env = os.environ.copy()
env["WEREAD_API_KEY"] = env.get("WEREAD_API_KEY", "")
env["WEREAD_API_KEY"] = get_env_value("WEREAD_API_KEY", "")
if not env["WEREAD_API_KEY"]:
return jsonify({"ok": False, "error": "WEREAD_API_KEY 未设置"}), 500
return jsonify({
"ok": False,
"error": "WEREAD_API_KEY 未设置,请检查当前终端环境、项目 .env 或 ~/.zshrc",
}), 500
results = {"steps": []}
try:
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
"""
项目配置读取工具。
读取顺序:
1. 当前进程环境变量
2. 项目根目录下的 .env / .env.local
3. 用户常见 shell 配置文件中的 export 语句
"""
from pathlib import Path
import os
PROJECT_DIR = Path(__file__).resolve().parent
ENV_FILE_NAMES = (".env", ".env.local")
SHELL_RC_FILES = (
Path("~/.zshrc").expanduser(),
Path("~/.zprofile").expanduser(),
Path("~/.zlogin").expanduser(),
Path("~/.bash_profile").expanduser(),
Path("~/.bashrc").expanduser(),
Path("~/.profile").expanduser(),
)
def _parse_assignment(line):
"""解析 KEY=VALUE 或 export KEY=VALUE 形式的配置行。"""
stripped = line.strip()
if not stripped or stripped.startswith("#"):
return None
if stripped.startswith("export "):
stripped = stripped[7:].strip()
if "=" not in stripped:
return None
key, value = stripped.split("=", 1)
key = key.strip()
value = value.strip()
if not key or any(ch.isspace() for ch in key):
return None
if value[:1] in {"'", '"'} and value[-1:] == value[:1]:
value = value[1:-1]
else:
value = value.split(" #", 1)[0].strip()
return key, value
def _read_var_from_file(file_path, var_name):
"""从文本配置文件里读取指定变量,后出现的值优先。"""
try:
lines = file_path.read_text(encoding="utf-8").splitlines()
except OSError:
return ""
result = ""
for line in lines:
parsed = _parse_assignment(line)
if not parsed:
continue
key, value = parsed
if key == var_name and value:
result = value
return result
def _project_env_files(project_dir=None):
base_dir = Path(project_dir) if project_dir else PROJECT_DIR
return [base_dir / name for name in ENV_FILE_NAMES]
def load_project_env(project_dir=None):
"""把项目 .env 文件里的变量加载到当前进程环境中。"""
for file_path in _project_env_files(project_dir):
if not file_path.exists():
continue
try:
lines = file_path.read_text(encoding="utf-8").splitlines()
except OSError:
continue
for line in lines:
parsed = _parse_assignment(line)
if not parsed:
continue
key, value = parsed
if key and value and key not in os.environ:
os.environ[key] = value
def get_env_value(var_name, default="", project_dir=None):
"""按项目约定顺序读取配置值,并回填到当前进程环境。"""
value = os.environ.get(var_name, "")
if value:
return value
load_project_env(project_dir)
value = os.environ.get(var_name, "")
if value:
return value
for file_path in _project_env_files(project_dir):
value = _read_var_from_file(file_path, var_name)
if value:
os.environ[var_name] = value
return value
for file_path in SHELL_RC_FILES:
value = _read_var_from_file(file_path, var_name)
if value:
os.environ[var_name] = value
return value
return default
+286
View File
@@ -0,0 +1,286 @@
# 微信读书数据看板最终文档
本文档用于替代 `.qoder/` 与 `.workbuddy/` 中分散的工具生成文档,作为当前仓库内的单一最终说明。内容以仓库现状为准,吸收了工具文档中的架构说明、接口说明、数据库分组、部署信息和迭代记录,同时剔除了已经过期或仅对工具自身有意义的中间信息。
## 1. 文档与目录约定
- 本文件是 `.qoder/repowiki` 与 `.workbuddy/memory` 的合并结果,也是这两个目录的人工可维护替代品。
- `.qoder/`、`.workbuddy/` 属于外部工具的中间产物,不参与 Git 管理。
- 深度技术细节仍保留在仓库原有文档中:
- [TECHNICAL_DOC.md](/Users/kinghwj/Codes/WeReadBoard/docs/TECHNICAL_DOC.md)
- [METADATA_DOC.md](/Users/kinghwj/Codes/WeReadBoard/docs/METADATA_DOC.md)
## 2. 项目定位
这是一个基于微信读书 Agent Gateway API 的个人阅读数据看板。系统将微信读书中的书架、阅读统计、笔记、推荐和搜索能力整合到一个本地运行的网站中,提供可视化图表、分页书架、笔记概览和按需详情查询。
当前实现采用:
- 后端:Flask + PyMySQL
- 前端:原生 HTML / CSS / JavaScript + ECharts
- 缓存与采集:SQLite
- 在线查询数据源:MySQL
- 外部能力:微信读书 Agent Gateway API
## 3. 当前仓库结构
核心文件与职责如下:
- [app.py](/Users/kinghwj/Codes/WeReadBoard/app.py):Flask 后端服务,提供页面和 `/api/*` 接口。
- [fetch_data.py](/Users/kinghwj/Codes/WeReadBoard/fetch_data.py):全量采集脚本,将微信读书数据写入 SQLite。
- [migrate_to_mysql.py](/Users/kinghwj/Codes/WeReadBoard/migrate_to_mysql.py):将 SQLite 数据迁移到 MySQL。
- [schema_mysql.sql](/Users/kinghwj/Codes/WeReadBoard/schema_mysql.sql):MySQL 31 张表建表语句。
- [config_utils.py](/Users/kinghwj/Codes/WeReadBoard/config_utils.py):运行时配置读取工具。
- [start_server.sh](/Users/kinghwj/Codes/WeReadBoard/start_server.sh):本地服务生命周期脚本。
- [templates/index.html](/Users/kinghwj/Codes/WeReadBoard/templates/index.html):页面骨架。
- [static/js/app.js](/Users/kinghwj/Codes/WeReadBoard/static/js/app.js):前端交互逻辑。
- [static/css/style.css](/Users/kinghwj/Codes/WeReadBoard/static/css/style.css):主题、布局和组件样式。
## 4. 系统架构
系统采用“采集 -> 迁移 -> 展示 -> 按需回填”的模式:
1. `fetch_data.py` 从微信读书 API 拉取全量数据,落入本地 `weread.db`。
2. `migrate_to_mysql.py` 将 SQLite 中的数据按表迁移到 MySQL `weread_dashboard`。
3. `app.py` 从 MySQL 读取基础数据,向前端提供 REST API。
4. 部分详情型接口会在用户点击时再去调用微信读书 API,并把结果缓存回 MySQL。
5. 前端点击“刷新”时会调用 `/api/sync`,串行执行全量采集与迁移。
## 5. 运行时配置
### 5.1 微信读书 API Key
当前代码通过 [config_utils.py](/Users/kinghwj/Codes/WeReadBoard/config_utils.py) 读取 `WEREAD_API_KEY`,优先级如下:
1. 当前进程环境变量
2. 项目根目录 `.env`
3. 项目根目录 `.env.local`
4. 用户 shell 配置文件,如 `~/.zshrc`、`~/.zprofile`、`~/.bash_profile`
这层逻辑的目的,是避免“终端里已经 export,但服务进程没有继承”导致的误报。
### 5.2 MySQL 配置
当前 MySQL 连接配置仍写在代码中,`app.py` 与 `migrate_to_mysql.py` 各维护一份:
- host: `127.0.0.1`
- port: `3306`
- user: `root`
- password: `258963`
- database: `weread_dashboard`
这部分目前能工作,但仍属于后续值得继续抽离的技术债。
### 5.3 端口与启动
- 默认端口:`8080`
- 可选环境变量:`WEREAD_PORT`
- 启动脚本使用当前机器上可用的 `python3` 或 `python`
常用命令:
```bash
pip install -r requirements.txt
python3 app.py
./start_server.sh daemon
./start_server.sh stop
./start_server.sh status
```
## 6. 主要功能
### 6.1 概览与图表
- 总阅读时长、阅读天数、书架数量、笔记总数
- 年度阅读趋势
- 24 小时时段分布
- 偏好分类
- 偏好作者
- 阅读时长排行
### 6.2 书架
- 关键词搜索书名 / 作者
- 分类筛选与父子分类树
- 仅看已读完
- 分页浏览
- 一键刷新同步微信读书数据
### 6.3 笔记与想法
- 笔记本概览
- 点击书籍查看按章节整理的划线、想法和点评
- 想法详情页按类型筛选
### 6.4 发现能力
- 为你推荐
- 搜索书籍
- 推荐 / 搜索结果写回本地缓存
## 7. API 总览
当前后端接口主要分为四类:
### 7.1 系统接口
- `GET /api/health`:数据库健康检查
- `POST /api/sync`:执行全量采集与迁移
### 7.2 基础统计接口
- `GET /api/overview`
- `GET /api/yearly-trend`
- `GET /api/top-books`
- `GET /api/categories`
- `GET /api/authors`
- `GET /api/time-distribution`
- `GET /api/read-stats`
- `GET /api/archives`
### 7.3 书架与笔记接口
- `GET /api/shelf`
- `GET /api/notebooks`
### 7.4 按需详情 / 发现接口
- `GET /api/book/detail`
- `GET /api/book/notes`
- `GET /api/recommend`
- `GET /api/search`
接口返回格式、参数细节和字段说明,仍以 [TECHNICAL_DOC.md](/Users/kinghwj/Codes/WeReadBoard/docs/TECHNICAL_DOC.md) 为准。
## 8. 数据模型分组
当前 MySQL 使用 31 张表,按职责分为以下几组:
### 8.1 书架
- `books`
- `shelf_albums`
- `shelf_meta`
- `archives`
### 8.2 阅读统计
- `reading_stats`
- `read_times`
- `read_longest`
- `read_stat_items`
- `prefer_categories`
- `prefer_authors`
- `prefer_publishers`
- `prefer_cps`
- `prefer_books`
- `prefer_time`
- `read_rank`
- `medals`
- `year_reports`
### 8.3 笔记与书籍内容
- `notebooks`
- `book_highlights`
- `book_chapters`
- `book_reviews`
- `book_best_bookmarks`
- `book_underlines`
- `public_reviews`
### 8.4 书籍详情与进度
- `book_details`
- `book_progress`
### 8.5 推荐与搜索
- `recommended_books`
- `similar_books`
- `search_sessions`
- `search_results`
### 8.6 系统元信息
- `fetch_meta`
设计特征:
- MySQL 统一使用 `utf8mb4` + `InnoDB`
- 大多数时间字段采用“整型时间戳 + `_dt` 可读时间列”的双列模式
- 每张表统一维护 `created_at` 与 `updated_at`
完整字段说明见 [METADATA_DOC.md](/Users/kinghwj/Codes/WeReadBoard/docs/METADATA_DOC.md)。
## 9. 前端实现要点
当前前端是单页标签切换式界面,主要包括:
- 主题切换:支持明暗主题与本地持久化
- 图表懒加载:首次进入图表页时才初始化 ECharts
- 高清封面策略:低清 URL 自动升级为高清版本
- 书架筛选:关键词、分类、已读完、分页
- 想法详情页:从弹窗升级为独立页面
- 刷新同步:按钮触发 `/api/sync` 后再刷新书架数据
## 10. 部署与运维要点
- MySQL 需要先执行 [schema_mysql.sql](/Users/kinghwj/Codes/WeReadBoard/schema_mysql.sql) 建库建表。
- 初次准备数据时,执行:
```bash
python3 fetch_data.py
python3 migrate_to_mysql.py
```
- 日常使用可直接启动 Web 服务,前端点“刷新”会自动触发同步。
- 若样式或静态资源修改后页面无变化,优先检查:
- 服务是否使用了旧进程
- 浏览器是否使用了缓存
## 11. 合并后的迭代记录
以下内容来自 `.workbuddy/memory/2026-07-17.md`,但已按主题重整并去掉工具内部流程描述。
### 11.1 数据层演进
- 项目由 SQLite 展示切换为 MySQL 展示。
- 建表从 18 张表扩展到 31 张表,对齐官方接口字段。
- 迁移脚本重写,支持全量导入与时间字段转换。
### 11.2 后端演进
- 新增 `db_required` 装饰器统一管理数据库连接。
- 新增 `/api/health` 用于前端健康检查。
- 新增 `/api/sync`,打通“采集 -> 迁移 -> 展示”的刷新流程。
- 新增按需详情接口,失败时可回退本地缓存。
### 11.3 前端演进
- 新增明暗主题切换和图表配色适配。
- 阅读排行从卡片改为列表。
- 想法详情从弹窗改为独立页面。
- 新增顶部搜索框、书架分类树、推荐与搜索页。
- 书架增加刷新按钮,并在后续多次修复其加载态抖动问题。
### 11.4 启动与配置演进
- `start_server.sh` 从硬编码路径改为动态识别项目目录与解释器。
- `WEREAD_API_KEY` 从单纯依赖当前 shell 环境,演进为支持 `.env` 和 shell 配置回退读取。
## 12. 当前已知技术债
- MySQL 连接配置仍硬编码在 `app.py` 和 `migrate_to_mysql.py`,存在重复维护。
- `README.md` 仍保留较早期的 SQLite 描述,后续可与当前架构再对齐。
- 当前仍以本地脚本为主,未引入更正式的进程管理和自动化部署方案。
## 13. 结论
现在仓库内推荐遵循的文档与资料体系如下:
- 最终总说明:本文件 [PROJECT_FINAL_DOC.md](/Users/kinghwj/Codes/WeReadBoard/docs/PROJECT_FINAL_DOC.md)
- 技术展开说明:[TECHNICAL_DOC.md](/Users/kinghwj/Codes/WeReadBoard/docs/TECHNICAL_DOC.md)
- 表结构与字段说明:[METADATA_DOC.md](/Users/kinghwj/Codes/WeReadBoard/docs/METADATA_DOC.md)
`.qoder/` 与 `.workbuddy/` 继续保留给工具使用即可,但不再视为仓库的正式文档来源。
+9 -4
View File
@@ -26,9 +26,10 @@ import time
import urllib.request
from datetime import datetime, timezone, timedelta
from config_utils import get_env_value
# ===== 配置 =====
API_URL = "https://i.weread.qq.com/api/agent/gateway"
API_KEY = os.environ.get("WEREAD_API_KEY", "")
SKILL_VERSION = "1.0.4"
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "weread.db")
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -54,13 +55,17 @@ def now_str():
def api_call(api_name, **kwargs):
"""调用微信读书 API"""
api_key = get_env_value("WEREAD_API_KEY", "")
if not api_key:
raise RuntimeError("WEREAD_API_KEY 未设置,请检查当前终端环境、项目 .env 或 ~/.zshrc")
body = {"api_name": api_name, "skill_version": SKILL_VERSION}
body.update(kwargs)
req = urllib.request.Request(
API_URL,
data=json.dumps(body).encode("utf-8"),
headers={
"Authorization": f"Bearer {API_KEY}",
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
@@ -1020,9 +1025,9 @@ def fetch_notebooks(conn):
# ===== 主函数 =====
def main():
if not API_KEY:
if not get_env_value("WEREAD_API_KEY", ""):
print("错误: 请先设置 WEREAD_API_KEY 环境变量")
print(" export WEREAD_API_KEY=wrk-xxxxxxxx")
print(" 可配置在当前终端、项目 .env,或 ~/.zshrc")
return
print("=" * 60)
+1
View File
@@ -1 +1,2 @@
flask>=3.0
PyMySQL>=1.1
+11 -4
View File
@@ -7,8 +7,8 @@
# ./start_server.sh restart 重启服务
# ./start_server.sh status 查看状态
PROJECT_DIR="/Users/hwj/WorkBuddy/2026-07-17-09-48-20"
PYTHON="/Users/hwj/.workbuddy/binaries/python/envs/default/bin/python"
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
PYTHON="${PYTHON:-$(command -v python3 || command -v python)}"
PID_FILE="$PROJECT_DIR/.server.pid"
LOG_FILE="/tmp/weread_server.log"
PORT=8080
@@ -61,6 +61,10 @@ start_foreground() {
echo "服务已在运行 (PID: $(get_pid))"
exit 1
fi
if [ -z "$PYTHON" ]; then
echo "未找到可用的 Python 解释器"
exit 1
fi
echo "启动服务(前台模式)..."
exec "$PYTHON" app.py
}
@@ -70,9 +74,12 @@ start_daemon() {
echo "服务已在运行 (PID: $(get_pid))"
exit 1
fi
if [ -z "$PYTHON" ]; then
echo "未找到可用的 Python 解释器"
exit 1
fi
echo "启动服务(后台守护模式)..."
WEREAD_API_KEY="${WEREAD_API_KEY:-wrk-ZL_oy0woT5yKFaO1jn_15AAA}" \
nohup "$PYTHON" app.py > "$LOG_FILE" 2>&1 &
nohup "$PYTHON" app.py > "$LOG_FILE" 2>&1 &
local pid=$!
echo "$pid" > "$PID_FILE"
sleep 2
+16 -2
View File
@@ -748,6 +748,7 @@ body {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.shelf-search {
@@ -769,6 +770,7 @@ body {
.shelf-refresh-btn {
padding: 0 16px;
height: 36px;
min-width: 112px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 13px;
@@ -781,7 +783,7 @@ body {
align-items: center;
justify-content: center;
gap: 4px;
flex-shrink: 0;
flex: 0 0 112px;
white-space: nowrap;
}
@@ -789,6 +791,15 @@ body {
display: inline-block;
font-size: 14px;
line-height: 1;
flex: 0 0 auto;
}
.shelf-refresh-btn .btn-text {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 4.5em;
line-height: 1;
}
.shelf-refresh-btn:hover:not(:disabled) {
@@ -801,7 +812,7 @@ body {
cursor: wait;
}
.shelf-refresh-btn.loading {
.shelf-refresh-btn.is-loading {
border-color: var(--accent);
color: var(--accent);
}
@@ -2022,6 +2033,9 @@ body {
.shelf-search {
width: 140px;
}
.shelf-filters {
gap: 10px;
}
.shelf-categories {
gap: 6px;
}
+2 -2
View File
@@ -324,7 +324,7 @@ async function refreshShelf() {
const btn = document.getElementById("shelfRefreshBtn");
const textEl = btn.querySelector(".btn-text");
btn.disabled = true;
btn.classList.add("loading");
btn.classList.add("is-loading");
textEl.textContent = "同步中...";
try {
@@ -344,7 +344,7 @@ async function refreshShelf() {
alert("刷新失败: " + (e.message || "未知错误"));
} finally {
btn.disabled = false;
btn.classList.remove("loading");
btn.classList.remove("is-loading");
textEl.textContent = "刷新";
}
}