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
|
||||
|
||||
# WorkBuddy workspace memory
|
||||
# Tool artifacts
|
||||
.qoder/
|
||||
.workbuddy/
|
||||
@@ -18,13 +18,23 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
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())
|
||||
except ImportError:
|
||||
_ssl_context = ssl.create_default_context()
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
|
||||
from api_v2 import v2_bp
|
||||
@@ -54,7 +64,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"
|
||||
|
||||
|
||||
@@ -152,18 +161,22 @@ 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",
|
||||
)
|
||||
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"))
|
||||
|
||||
|
||||
@@ -1197,14 +1210,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
@@ -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
|
||||
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 +1,2 @@
|
||||
flask>=3.0
|
||||
PyMySQL>=1.1
|
||||
+11
-4
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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