mirror of
https://github.com/KingHWJ/WeReadBoard.git
synced 2026-11-03 18:24:58 +08:00
Merge remote-tracking branch 'origin/master' into dev
# Conflicts: # .gitignore # app.py # static/js/app.js
This commit is contained in:
commit
a1f54e765e
9 files changed
+567
-19
No files matched your search
@@ -31,4 +31,6 @@ Thumbs.db
|
|||||||
.server.pid
|
.server.pid
|
||||||
|
|
||||||
# WorkBuddy workspace memory
|
# WorkBuddy workspace memory
|
||||||
|
# Tool artifacts
|
||||||
|
.qoder/
|
||||||
.workbuddy/
|
.workbuddy/
|
||||||
@@ -18,13 +18,23 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import ssl
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import pymysql
|
import pymysql
|
||||||
|
|
||||||
|
from config_utils import get_env_value
|
||||||
|
|
||||||
|
try:
|
||||||
|
import certifi
|
||||||
|
_ssl_context = ssl.create_default_context(cafile=certifi.where())
|
||||||
|
except ImportError:
|
||||||
|
_ssl_context = ssl.create_default_context()
|
||||||
from flask import Flask, jsonify, render_template, request
|
from flask import Flask, jsonify, render_template, request
|
||||||
|
|
||||||
from api_v2 import v2_bp
|
from api_v2 import v2_bp
|
||||||
@@ -54,7 +64,6 @@ MYSQL_CONFIG = {
|
|||||||
# 微信读书 API 配置
|
# 微信读书 API 配置
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
WEREAD_API_URL = "https://i.weread.qq.com/api/agent/gateway"
|
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"
|
WEREAD_SKILL_VERSION = "1.0.4"
|
||||||
|
|
||||||
|
|
||||||
@@ -152,18 +161,22 @@ def weread_api_call(api_name, **kwargs):
|
|||||||
Raises:
|
Raises:
|
||||||
Exception: API 调用失败时抛出异常
|
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 = {"api_name": api_name, "skill_version": WEREAD_SKILL_VERSION}
|
||||||
body.update(kwargs)
|
body.update(kwargs)
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
WEREAD_API_URL,
|
WEREAD_API_URL,
|
||||||
data=json.dumps(body).encode("utf-8"),
|
data=json.dumps(body).encode("utf-8"),
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {WEREAD_API_KEY}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
method="POST",
|
method="POST",
|
||||||
)
|
)
|
||||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
with urllib.request.urlopen(req, timeout=30, context=_ssl_context) as resp:
|
||||||
return json.loads(resp.read().decode("utf-8"))
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
|
||||||
|
|
||||||
@@ -1197,14 +1210,14 @@ def api_search(conn):
|
|||||||
def sync_data():
|
def sync_data():
|
||||||
"""运行 fetch_data.py + migrate_to_mysql.py,全量同步数据"""
|
"""运行 fetch_data.py + migrate_to_mysql.py,全量同步数据"""
|
||||||
project_dir = os.path.dirname(os.path.abspath(__file__))
|
project_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
python_bin = os.path.join(
|
python_bin = sys.executable or "python3"
|
||||||
os.path.expanduser("~"),
|
|
||||||
".workbuddy/binaries/python/envs/default/bin/python",
|
|
||||||
)
|
|
||||||
env = os.environ.copy()
|
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"]:
|
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": []}
|
results = {"steps": []}
|
||||||
try:
|
try:
|
||||||
|
|||||||
+118
@@ -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
|
||||||
@@ -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
@@ -26,9 +26,10 @@ import time
|
|||||||
import urllib.request
|
import urllib.request
|
||||||
from datetime import datetime, timezone, timedelta
|
from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
|
from config_utils import get_env_value
|
||||||
|
|
||||||
# ===== 配置 =====
|
# ===== 配置 =====
|
||||||
API_URL = "https://i.weread.qq.com/api/agent/gateway"
|
API_URL = "https://i.weread.qq.com/api/agent/gateway"
|
||||||
API_KEY = os.environ.get("WEREAD_API_KEY", "")
|
|
||||||
SKILL_VERSION = "1.0.4"
|
SKILL_VERSION = "1.0.4"
|
||||||
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "weread.db")
|
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "weread.db")
|
||||||
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
@@ -54,13 +55,17 @@ def now_str():
|
|||||||
|
|
||||||
def api_call(api_name, **kwargs):
|
def api_call(api_name, **kwargs):
|
||||||
"""调用微信读书 API"""
|
"""调用微信读书 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 = {"api_name": api_name, "skill_version": SKILL_VERSION}
|
||||||
body.update(kwargs)
|
body.update(kwargs)
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
API_URL,
|
API_URL,
|
||||||
data=json.dumps(body).encode("utf-8"),
|
data=json.dumps(body).encode("utf-8"),
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {API_KEY}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
method="POST",
|
method="POST",
|
||||||
@@ -1020,9 +1025,9 @@ def fetch_notebooks(conn):
|
|||||||
# ===== 主函数 =====
|
# ===== 主函数 =====
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if not API_KEY:
|
if not get_env_value("WEREAD_API_KEY", ""):
|
||||||
print("错误: 请先设置 WEREAD_API_KEY 环境变量")
|
print("错误: 请先设置 WEREAD_API_KEY 环境变量")
|
||||||
print(" export WEREAD_API_KEY=wrk-xxxxxxxx")
|
print(" 可配置在当前终端、项目 .env,或 ~/.zshrc")
|
||||||
return
|
return
|
||||||
|
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
flask>=3.0
|
flask>=3.0
|
||||||
|
PyMySQL>=1.1
|
||||||
+11
-4
@@ -7,8 +7,8 @@
|
|||||||
# ./start_server.sh restart 重启服务
|
# ./start_server.sh restart 重启服务
|
||||||
# ./start_server.sh status 查看状态
|
# ./start_server.sh status 查看状态
|
||||||
|
|
||||||
PROJECT_DIR="/Users/hwj/WorkBuddy/2026-07-17-09-48-20"
|
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
PYTHON="/Users/hwj/.workbuddy/binaries/python/envs/default/bin/python"
|
PYTHON="${PYTHON:-$(command -v python3 || command -v python)}"
|
||||||
PID_FILE="$PROJECT_DIR/.server.pid"
|
PID_FILE="$PROJECT_DIR/.server.pid"
|
||||||
LOG_FILE="/tmp/weread_server.log"
|
LOG_FILE="/tmp/weread_server.log"
|
||||||
PORT=8080
|
PORT=8080
|
||||||
@@ -61,6 +61,10 @@ start_foreground() {
|
|||||||
echo "服务已在运行 (PID: $(get_pid))"
|
echo "服务已在运行 (PID: $(get_pid))"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if [ -z "$PYTHON" ]; then
|
||||||
|
echo "未找到可用的 Python 解释器"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "启动服务(前台模式)..."
|
echo "启动服务(前台模式)..."
|
||||||
exec "$PYTHON" app.py
|
exec "$PYTHON" app.py
|
||||||
}
|
}
|
||||||
@@ -70,9 +74,12 @@ start_daemon() {
|
|||||||
echo "服务已在运行 (PID: $(get_pid))"
|
echo "服务已在运行 (PID: $(get_pid))"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if [ -z "$PYTHON" ]; then
|
||||||
|
echo "未找到可用的 Python 解释器"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "启动服务(后台守护模式)..."
|
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=$!
|
local pid=$!
|
||||||
echo "$pid" > "$PID_FILE"
|
echo "$pid" > "$PID_FILE"
|
||||||
sleep 2
|
sleep 2
|
||||||
|
|||||||
+16
-2
@@ -748,6 +748,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelf-search {
|
.shelf-search {
|
||||||
@@ -769,6 +770,7 @@ body {
|
|||||||
.shelf-refresh-btn {
|
.shelf-refresh-btn {
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
|
min-width: 112px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -781,7 +783,7 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
flex-shrink: 0;
|
flex: 0 0 112px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -789,6 +791,15 @@ body {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1;
|
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) {
|
.shelf-refresh-btn:hover:not(:disabled) {
|
||||||
@@ -801,7 +812,7 @@ body {
|
|||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shelf-refresh-btn.loading {
|
.shelf-refresh-btn.is-loading {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
@@ -2022,6 +2033,9 @@ body {
|
|||||||
.shelf-search {
|
.shelf-search {
|
||||||
width: 140px;
|
width: 140px;
|
||||||
}
|
}
|
||||||
|
.shelf-filters {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
.shelf-categories {
|
.shelf-categories {
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# weread - 微信读书数据看板 服务管理命令
|
||||||
|
# 用法:
|
||||||
|
# weread start [port] 启动服务(默认端口 8080)
|
||||||
|
# weread stop [port] 停止服务(默认端口 8080)
|
||||||
|
|
||||||
|
PROJECT_DIR="/Users/hwj/Codes/MyProjects/WeReadBoard"
|
||||||
|
PID_FILE="$PROJECT_DIR/.server.pid"
|
||||||
|
LOG_FILE="/tmp/weread_server.log"
|
||||||
|
DEFAULT_PORT=8080
|
||||||
|
|
||||||
|
get_port() {
|
||||||
|
echo "${1:-$DEFAULT_PORT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
get_pid() {
|
||||||
|
local port="$1"
|
||||||
|
if [ -f "$PID_FILE" ]; then
|
||||||
|
local saved_port
|
||||||
|
saved_port=$(sed -n '2p' "$PID_FILE" 2>/dev/null)
|
||||||
|
local pid
|
||||||
|
pid=$(head -1 "$PID_FILE" 2>/dev/null)
|
||||||
|
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
||||||
|
echo "$pid"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
lsof -i :"$port" -t 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_running() {
|
||||||
|
local port="$1"
|
||||||
|
if [ -n "$(get_pid "$port")" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_start() {
|
||||||
|
local port
|
||||||
|
port=$(get_port "$1")
|
||||||
|
|
||||||
|
if is_running "$port"; then
|
||||||
|
echo "❌ 服务已在运行 (PID: $(get_pid "$port"), 端口: $port)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "启动服务(端口: $port)..."
|
||||||
|
WEREAD_PORT="$port" nohup python3 "$PROJECT_DIR/app.py" > "$LOG_FILE" 2>&1 &
|
||||||
|
local pid=$!
|
||||||
|
printf '%s\n%s\n' "$pid" "$port" > "$PID_FILE"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
echo "✅ 服务已启动"
|
||||||
|
echo " PID: $pid"
|
||||||
|
echo " 端口: $port"
|
||||||
|
echo " 访问: http://127.0.0.1:$port"
|
||||||
|
echo " 日志: $LOG_FILE"
|
||||||
|
else
|
||||||
|
echo "❌ 启动失败,请查看日志: $LOG_FILE"
|
||||||
|
cat "$LOG_FILE" 2>/dev/null
|
||||||
|
rm -f "$PID_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_stop() {
|
||||||
|
local port
|
||||||
|
port=$(get_port "$1")
|
||||||
|
|
||||||
|
if ! is_running "$port"; then
|
||||||
|
echo "服务未运行(端口: $port)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pid
|
||||||
|
pid=$(get_pid "$port")
|
||||||
|
echo "停止服务 (PID: $pid, 端口: $port)..."
|
||||||
|
kill "$pid" 2>/dev/null
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
kill -9 "$pid" 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$PID_FILE"
|
||||||
|
echo "✅ 已停止"
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "用法:"
|
||||||
|
echo " weread start [port] 启动服务(默认端口 8080)"
|
||||||
|
echo " weread stop [port] 停止服务(默认端口 8080)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1}" in
|
||||||
|
start) cmd_start "$2" ;;
|
||||||
|
stop) cmd_stop "$2" ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
Reference in new issue
Block a user