diff --git a/.dockerignore b/.dockerignore index 1edccf1..1bf7efb 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,6 +6,7 @@ __pycache__/ .Python env/ venv/ +.venv/ ENV/ *.egg-info/ dist/ @@ -69,8 +70,8 @@ test/ frontend/dist/ frontend/build/ -# 后端静态文件(会从前端构建阶段复制) -backend/static/ +# 后端静态文件(将从宿主机复制) +# backend/static/ # 提示词工坊实例标识(每个容器需要独立生成) backend/.instance_id diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 3157d58..851faec 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -7,7 +7,7 @@ on: workflow_dispatch: # 允许手动触发 env: - DOCKER_IMAGE: mumujie/mumuainovel + DOCKER_IMAGE: mumulingsi-project/mumulingsi FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: diff --git a/.gitignore b/.gitignore index f5830fe..c0d305c 100644 --- a/.gitignore +++ b/.gitignore @@ -81,6 +81,10 @@ backend/data/users.json backend/data/admins.json backend/storage/ +# Project analysis / intermediate docs (do not commit) +.claude/ +.work/ + # Temporary files *.bak *.swp @@ -108,7 +112,7 @@ dmypy.json BUILD_GUIDE.md launcher.py launcher.spec -mumuainovel.md +xinmi.md logo.ico .embed_cache dist_embed/ diff --git a/Dockerfile b/Dockerfile index 96c4bd7..c858b40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,37 +4,7 @@ # 构建参数 ARG USE_CN_MIRROR=false -# 阶段1: 构建前端 -FROM node:22-alpine AS frontend-builder - -ARG USE_CN_MIRROR - -WORKDIR /frontend - -# 复制前端依赖文件 -COPY frontend/package*.json ./ - -# 根据参数决定是否使用国内npm镜像 -RUN if [ "$USE_CN_MIRROR" = "true" ]; then \ - npm config set registry https://registry.npmmirror.com; \ - fi - -# 删除 package-lock.json 以避免因镜像源不一致导致的 404 错误 -RUN rm -f package-lock.json - -# 安装依赖 -RUN npm install - -# 复制前端源代码 -COPY frontend/ ./ - -# 临时修改vite配置,使其输出到dist目录(而不是../backend/static) -RUN sed -i "s|outDir: '../backend/static'|outDir: 'dist'|g" vite.config.ts - -# 构建前端 -RUN npm run build - -# 阶段2: 构建最终镜像 +# 阶段1: 最终镜像 FROM python:3.11-slim ARG USE_CN_MIRROR @@ -46,8 +16,8 @@ WORKDIR /app # 根据参数决定是否使用国内镜像源 RUN if [ "$USE_CN_MIRROR" = "true" ]; then \ - sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources && \ - sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list.d/debian.sources; \ + sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \ + sed -i 's/security.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources; \ fi # 安装系统依赖(添加数据库工具) @@ -61,8 +31,6 @@ RUN apt-get update && apt-get install -y \ COPY backend/requirements.txt ./ # 安装 Python 依赖 -# 先安装 torch CPU版本(~200MB vs 完整版~2GB,节省90%下载时间) -# 对于embedding场景,CPU版本完全够用 RUN if [ "$USE_CN_MIRROR" = "true" ]; then \ pip install --no-cache-dir torch==2.8.0 --index-url https://mirrors.aliyun.com/pypi/simple/ --extra-index-url https://download.pytorch.org/whl/cpu && \ pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/; \ @@ -78,7 +46,6 @@ RUN mkdir -p /app/embedding ENV SENTENCE_TRANSFORMERS_HOME=/app/embedding # 下载 embedding 模型(从 HuggingFace) -# 使用 Python 脚本预下载模型,这样运行时不需要网络 RUN python -c "\ from sentence_transformers import SentenceTransformer; \ import os; \ @@ -88,11 +55,12 @@ model = SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniL print('Model downloaded successfully!'); \ " -# 复制后端代码(不包含embedding,因为已经下载了) +# 复制后端代码 COPY backend/ ./ -# 从前端构建阶段复制构建好的静态文件 -COPY --from=frontend-builder /frontend/dist ./static +# 复制宿主机预构建的静态文件 +# 这样可以避免 Docker 内部构建前端时的各种环境问题 +COPY backend/static/ ./static # 复制 Alembic 迁移配置和脚本(PostgreSQL) COPY backend/alembic-postgres.ini ./alembic.ini @@ -100,8 +68,8 @@ COPY backend/alembic/postgres ./alembic COPY backend/scripts/entrypoint.sh /app/entrypoint.sh COPY backend/scripts/migrate.py ./scripts/migrate.py -# 赋予执行权限 -RUN chmod +x /app/entrypoint.sh +# 修复 Windows CRLF 换行导致的启动失败,并赋予执行权限 +RUN sed -i 's/\r$//' /app/entrypoint.sh && chmod +x /app/entrypoint.sh # 创建必要的目录 RUN mkdir -p /app/data /app/logs @@ -114,7 +82,7 @@ ENV PYTHONUNBUFFERED=1 ENV APP_HOST=0.0.0.0 ENV APP_PORT=8000 -# 设置运行时为离线模式(模型已在构建时下载) +# 设置运行时为离线模式 ENV TRANSFORMERS_OFFLINE=1 ENV HF_DATASETS_OFFLINE=1 ENV HF_HUB_OFFLINE=1 @@ -123,5 +91,5 @@ ENV HF_HUB_OFFLINE=1 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 -# 使用 entrypoint 脚本启动(自动执行迁移) -ENTRYPOINT ["/app/entrypoint.sh"] \ No newline at end of file +# 使用 entrypoint 脚本启动 +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/README.md b/README.md index 8f59bff..0495741 100644 --- a/README.md +++ b/README.md @@ -1,643 +1,317 @@ -# 墨木灵思 📚✨ - -
+# 墨木灵思 ![Version](https://img.shields.io/badge/version-1.4.8-blue.svg) ![Python](https://img.shields.io/badge/python-3.11-blue.svg) -![FastAPI](https://img.shields.io/badge/FastAPI-0.109.0-green.svg) +![FastAPI](https://img.shields.io/badge/FastAPI-0.121.0-green.svg) ![React](https://img.shields.io/badge/react-18.3.1-blue.svg) -![License](https://img.shields.io/badge/license-GPL%20v3-blue.svg) -**基于 AI 的智能小说创作助手** +## 1. 项目简介 -[特性](#-特性) • [快速开始](#-快速开始) • [配置说明](#%EF%B8%8F-配置说明) • [项目结构](#-项目结构) +### 项目概述 -
+**墨木灵思**是一款基于大语言模型的智能小说创作平台,帮助作者从大纲、角色到章节一气呵成地完成创作,让 AI 成为可靠的写作搭档。 + +### 项目起源 + +长篇网文与原创小说创作往往面临设定繁杂、人物关系难梳理、章节衔接不连贯等问题。墨木灵思将 AI 能力融入创作全流程,把「灵感 → 大纲 → 角色 → 章节」串联为可管理的结构化工作流,降低创作门槛并提升产出效率。 + +### 项目定位 + +| 维度 | 说明 | +|------|------| +| **目标用户** | 网文作者、业余写作者、内容创作者、文学爱好者 | +| **适用场景** | 长篇连载、短篇创作、世界观搭建、同人续写、拆书仿写 | +| **部署形态** | 支持 Docker 一键部署,也可本地开发运行 | + +### 核心价值 + +- 用 AI 辅助完成大纲、角色、世界观等前期设定,缩短冷启动时间 +- 多模型灵活切换,适配不同文风与成本需求 +- 角色关系、伏笔、职业体系等结构化管理能力,保持长篇一致性 +- 多用户数据隔离,适合个人或小团队私有化部署 --- -
+## 2. 整体架构与技术栈 -## 💬 加入交流群 +### 系统架构 -欢迎扫码加入 QQ 交流群,一起交流 AI 小说创作心得、反馈问题、获取最新动态! +采用**前后端分离**架构:React 单页应用负责交互,FastAPI 提供 REST API,PostgreSQL 持久化业务数据;生产环境通过 Docker Compose 编排应用与数据库,前端构建产物由后端静态托管,统一对外暴露 HTTP 服务。 -QQ交流群二维码 +```mermaid +flowchart LR + subgraph Client + UI[React SPA] + end + subgraph Server + API[FastAPI] + AI[AI 服务层] + DB[(PostgreSQL)] + Vec[ChromaDB / Embedding] + end + UI -->|HTTP / API| API + API --> AI + API --> DB + API --> Vec + AI -->|OpenAI 兼容 API| Ext[外部 LLM] +``` -
+### 技术栈 + +| 分类 | 技术 | 版本 / 说明 | +|------|------|-------------| +| **后端** | Python | 3.11 | +| | FastAPI | 0.121.0 | +| | SQLAlchemy + Alembic | 2.0.25 / 1.14.0 | +| | Uvicorn | 0.38.0 | +| **前端** | React | 18.3.1 | +| | TypeScript + Vite | — | +| | Ant Design | 5.x | +| | Zustand | 5.x | +| **数据库** | PostgreSQL | 18(推荐) | +| | SQLite | 可选(开发 / 轻量场景) | +| **AI 与向量** | OpenAI / Anthropic SDK | 多模型接入 | +| | ChromaDB + Sentence Transformers | 长期记忆与语义检索 | +| **部署** | Docker + Docker Compose | 一键编排 | +| | GitHub Actions | 镜像构建与发布 | + +### 技术选型说明 + +- **FastAPI**:异步性能好,自带 OpenAPI 文档,适合 AI 类长耗时接口。 +- **PostgreSQL**:支持多用户、复杂关系与并发,满足生产级数据隔离。 +- **React + Ant Design**:组件丰富,适合复杂表单与可视化(关系图、时间线等)。 +- **Docker Compose**:降低部署成本,数据库与应用一键拉起。 --- -
+## 3. 项目目录概览 -## 💖 支持项目 +``` +MuMuAINovel/ +├── backend/ # 后端服务 +│ ├── app/ +│ │ ├── api/ # REST API 路由 +│ │ ├── models/ # 数据模型 +│ │ ├── services/ # 业务逻辑(含 AI 调用) +│ │ ├── middleware/ # 中间件 +│ │ ├── mcp/ # MCP 插件集成 +│ │ └── main.py # 应用入口 +│ ├── alembic/ # 数据库迁移 +│ ├── scripts/ # 初始化与运维脚本 +│ ├── static/ # 前端构建产物(生产) +│ └── requirements.txt +├── frontend/ # 前端应用 +│ ├── src/ +│ │ ├── pages/ # 页面 +│ │ ├── components/ # 通用组件 +│ │ ├── services/ # API 封装 +│ │ ├── store/ # 状态管理 +│ │ └── theme/ # 主题配置 +│ └── package.json +├── images/ # 文档与截图资源 +├── storage/ # 用户生成资源(如封面) +├── logs/ # 运行日志 +├── docker-compose.yml # 容器编排 +├── Dockerfile # 镜像构建 +└── README.md +``` -如果这个项目对你有帮助,欢迎通过以下方式支持开发: - -**[☕ 请我喝杯咖啡](https://mumuverse.space:1588/)** - -**[🌐 MuMuのAPI站点](https://api.mumuverse.space/register?aff=4NN8)** - -> 在 MuMu の API 站点充值满 50 元及以上,也可以获得下方赞助专属权益。 - -### 🎁 赞助专属权益 - -| 权益 | 说明 | +| 目录 | 作用 | |------|------| -| 📋 **优先需求响应** | 您的功能需求和问题反馈将获得优先处理 | -| 🚀 **Windows一键启动** | 获取免安装 EXE 程序,双击即可使用 | -| 💬 **专属技术支持** | 加入赞助者内部群,获得远程协助和配置指导 | - -### ☕ 赞助 / API 站点充值档位 - -| 金额 | 描述 | -|------|------| -| ¥5 | 🌶️ 一包辣条 | -| ¥10 | 🍱 一顿拼好饭 | -| ¥20 | 🧋 一杯咖啡 | -| ¥50 | 🍖 一次烧烤 | -| ¥99 | 🍲 一顿海底捞 | - -您的支持是我持续开发的动力!🙏 - -
+| `backend/app/api/` | 项目、章节、角色、提示词等业务接口 | +| `backend/app/services/` | AI 生成、润色、向量记忆等核心逻辑 | +| `frontend/src/pages/` | 书架、项目详情、设置、提示词模板等页面 | +| `backend/alembic/` | PostgreSQL / SQLite schema 迁移 | +| `backend/scripts/` | 数据库初始化、入口脚本 | --- -## ✨ 特性 +## 4. 核心业务功能 -- 🤖 **多 AI 模型** - 支持 OpenAI、Gemini、Claude 等主流模型 -- 📝 **智能向导** - AI 自动生成大纲、角色和世界观 -- 👥 **角色管理** - 人物关系、组织架构可视化管理 -- 📖 **章节编辑** - 支持创建、编辑、重新生成和润色 -- 🌐 **世界观设定** - 构建完整的故事背景 -- 🔐 **多种登录** - LinuxDO OAuth 或本地账户登录 -- 💾 **PostgreSQL** - 生产级数据库,多用户数据隔离 -- 🐳 **Docker 部署** - 一键启动,开箱即用 +- **智能创作向导**:根据题材与设定,AI 自动生成大纲、角色档案与世界观框架。 +- **多 AI 模型支持**:接入 OpenAI、Claude、Gemini 等,支持自定义 Base URL(兼容中转 API)。 +- **项目管理与书架**:多项目并行,支持导入导出,便于备份与迁移。 +- **角色与组织管理**:人物卡片、关系图谱、组织架构可视化编辑。 +- **职业等级体系**:可自定义修仙境界、魔法等级等成长体系。 +- **章节创作与润色**:章节生成、续写、重写、字数控制及 diff 对比。 +- **世界观与大纲**:结构化维护故事背景与情节脉络。 +- **伏笔管理**:追踪未回收伏笔,可视化时间线提醒。 +- **灵感模式**:快速生成创作点子与情节灵感。 +- **提示词工坊**:浏览、导入社区 Prompt 模板,可视化编辑自有模板。 +- **拆书功能**:分析既有作品结构,辅助仿写与续写。 +- **封面生成**:基于项目信息 AI 生成封面图。 +- **长期记忆**:基于 Embedding 的语义记忆,保持跨章节一致性。 +- **用户与认证**:本地账户、邮箱验证、LinuxDO OAuth;多用户数据隔离。 +- **系统设置**:SMTP、AI 密钥、主题等可在线配置。 -## 📸 项目预览 +--- -
+## 5. 实际应用场景示例 -多图预警 +### 适用行业与领域 -
+- 网络文学与自媒体连载 +- 游戏、动漫、影视衍生文案 +- 教育培训中的创意写作练习 +- 个人 IP 与世界观孵化 -### 登录界面 -![登录界面](images/1.png) +### 典型场景 -![登录界面](images/1-1.png) +**场景一:新人作者快速开书** +输入题材与基调后,由向导生成大纲与主要角色,再逐章扩写;适合从零起步、需要结构指引的作者。 -### 主界面 -![主界面](images/2.png) +**场景二:长篇连载一致性维护** +在数百章规模下,通过角色关系图、伏笔管理与向量记忆,减少人设崩坏与情节穿帮。 -![主界面(暗色)](images/2-1.png) +**场景三:拆书仿写与风格学习** +导入参考作品或章节,分析结构后结合自身设定续写,用于练笔或同人向创作。 -### 项目管理 -![项目管理](images/3.png) +--- -![项目管理](images/3-1.png) +## 6. 帮助解决的核心问题 -### 赞助我 💖 -![赞助我](images/4.png) +- **创作冷启动难**:自动生成大纲、角色与世界观,减少空白页焦虑。 +- **设定易混乱**:关系图、职业体系、伏笔追踪让长篇设定可检索、可维护。 +- **AI 调用分散**:统一配置多模型与 Prompt,降低切换成本。 +- **协作与部署复杂**:Docker 私有化部署,数据留在自有环境。 +- **章节质量不稳定**:润色、重写、分析建议一键应用,提升成稿效率。 +- **提示词难以沉淀**:模板工坊与可视化编辑,复用优质 Prompt。 -![赞助我](images/4-1.png) +--- -
+## 7. 快速开始 -
+### 环境要求 -## 📋 TODO List - -### ✅ 已完成功能 - -- [x] **灵感模式** - 创作灵感和点子生成 -- [x] **自定义写作风格** - 支持自定义 AI 写作风格 -- [x] **数据导入导出** - 项目数据的导入导出 -- [x] **Prompt 调整界面** - 可视化编辑 Prompt 模板 -- [x] **章节字数限制** - 用户可设置生成字数 -- [x] **思维链与章节关系图谱** - 可视化章节逻辑关系 -- [x] **根据分析一键重写** - 根据分析建议重新生成 -- [x] **Linux DO 自动创建账号** - OAuth 登录自动生成账号 -- [x] **职业等级体系** - 自定义职业和等级系统,支持修仙境界、魔法等级等多种体系 -- [x] **角色/组织卡片导入导出** - 单独导出角色和组织卡片,支持跨项目数据共享 -- [x] **伏笔管理** - 智能追踪剧情伏笔,提醒未回收线索,可视化伏笔时间线 -- [x] **提示词工坊** - 社区驱动的 Prompt 模板分享平台,一键导入优质提示词 -- [x] **拆书功能** - 目前呼声比较高的功能,一键拆书,给当年的ta一个圆满的结局 - -### 📝 规划中功能 - -...... - -> 💡 欢迎提交 Issue 或 Pull Request! - -## 💻 硬件配置要求 - -### 最低配置(个人使用/开发环境) - -| 组件 | 要求 | +| 方式 | 要求 | |------|------| -| **CPU** | 2 核 | -| **内存** | 2 GB RAM | -| **存储** | 10 GB 可用空间 | -| **网络** | 稳定互联网连接(用于调用 AI API) | +| **Docker 部署(推荐)** | Docker 20.10+、Docker Compose 2.0+;至少一个 LLM API Key | +| **本地开发** | Python 3.11、Node.js 18+、PostgreSQL 18(或 SQLite);稳定网络(调用外部 AI API) | -### 推荐配置(小型团队/生产环境) +**硬件建议(个人使用)**:2 核 CPU、2 GB 内存、10 GB 磁盘;生产或小团队建议 4 核 / 8 GB 内存。项目依赖外部 AI API,无需本地 GPU。 -| 组件 | 要求 | -|------|------| -| **CPU** | 4 核 | -| **内存** | 8 GB RAM | -| **存储** | 20 GB SSD | -| **网络** | 稳定互联网连接 | - -### 高并发配置(80-150 用户) - -| 组件 | 要求 | -|------|------| -| **CPU** | 8 核 | -| **内存** | 16 GB RAM | -| **存储** | 50 GB+ SSD | -| **网络** | 高带宽连接 | - -> **📌 说明** -> - **Embedding 模型**:约 400 MB 磁盘空间,运行时加载到内存 -> - **PostgreSQL**:默认配置使用 256 MB shared_buffers,1 GB effective_cache_size -> - **Docker 部署**:建议预留额外 1-2 GB 内存给容器运行时 -> - 本项目主要依赖外部 AI API(OpenAI/Claude/Gemini),不需要本地 GPU - -## 🚀 快速开始 - -### 前置要求 - -- Docker 和 Docker Compose -- 至少一个 AI 服务的 API Key(OpenAI/Gemini/Claude) - -### Docker Compose 部署(推荐) +### 安装步骤(Docker) ```bash -# 1. 克隆项目 -git clone https://github.com/xiamuceer-j/墨木灵思.git -cd 墨木灵思 +# 1. 获取项目代码 +git clone <你的仓库地址> +cd MuMuAINovel -# 2. 配置环境变量(必需) +# 2. 配置环境变量 cp backend/.env.example .env -# 编辑 .env 文件,填入必要配置(API Key、数据库密码等) +# 编辑 .env:至少配置 AI API Key、数据库密码等 -# 3. 确保文件准备完整 -# ⚠️ 重要:确保以下文件存在 -# - .env(配置文件,必需挂载到容器) -# - backend/scripts/init_postgres.sql(数据库初始化脚本) - -# 4. 启动服务 -docker-compose up -d - -# 5. 访问应用 -# 打开浏览器访问 http://localhost:8000 +# 3. 确认必要文件存在 +# - .env +# - backend/scripts/init_postgres.sql ``` -> **📌 注意事项** -> -> 1. **`.env` 文件挂载**: `docker-compose.yml` 会自动将 `.env` 挂载到容器,确保文件存在 -> 2. **数据库初始化**: `init_postgres.sql` 会在首次启动时自动执行,安装必要的PostgreSQL扩展 -> 3. **自行构建**: 如需从源码构建,请先下载 embedding 模型文件([加群获取](frontend/public/qq.jpg)) - -### 使用 Docker Hub 镜像(推荐新手) +### 运行步骤 ```bash -# 1. 拉取最新镜像(已包含模型文件) -docker pull mumujie/mumuainovel:latest +# 启动全部服务 +docker compose up -d -# 2. 创建 docker-compose.yml(点击下方展开查看完整配置) +# 查看状态与日志 +docker compose ps +docker compose logs -f ``` -
-📄 点击展开 docker-compose.yml 完整配置 +### 访问地址 -```yaml -services: - postgres: - image: postgres:18-alpine - container_name: mumuainovel-postgres - environment: - POSTGRES_DB: ${POSTGRES_DB:-mumuai_novel} - POSTGRES_USER: ${POSTGRES_USER:-mumuai} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-123456} - POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" - TZ: ${TZ:-Asia/Shanghai} - volumes: - - postgres_data:/var/lib/postgresql - - ./backend/scripts/init_postgres.sql:/docker-entrypoint-initdb.d/init.sql:ro - ports: - - "${POSTGRES_PORT:-5432}:5432" - restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mumuai} -d ${POSTGRES_DB:-mumuai_novel}"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - networks: - - ai-story-network - command: - - postgres - - -c - - max_connections=${POSTGRES_MAX_CONNECTIONS:-200} - - -c - - shared_buffers=${POSTGRES_SHARED_BUFFERS:-256MB} - - -c - - effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-1GB} - - -c - - maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB} - - -c - - checkpoint_completion_target=${POSTGRES_CHECKPOINT_COMPLETION_TARGET:-0.9} - - -c - - wal_buffers=${POSTGRES_WAL_BUFFERS:-16MB} - - -c - - default_statistics_target=${POSTGRES_DEFAULT_STATISTICS_TARGET:-100} - - -c - - random_page_cost=${POSTGRES_RANDOM_PAGE_COST:-1.1} - - -c - - effective_io_concurrency=${POSTGRES_EFFECTIVE_IO_CONCURRENCY:-200} - - -c - - work_mem=${POSTGRES_WORK_MEM:-4MB} - - -c - - min_wal_size=${POSTGRES_MIN_WAL_SIZE:-1GB} - - -c - - max_wal_size=${POSTGRES_MAX_WAL_SIZE:-4GB} +| 服务 | 默认地址 | +|------|----------| +| **Web 应用** | http://localhost:8000 | +| **API 文档(Swagger)** | http://localhost:8000/docs | +| **API 文档(ReDoc)** | http://localhost:8000/redoc | +| **PostgreSQL** | localhost:5432(容器内通过服务名 `postgres` 访问) | - mumuainovel: - image: mumujie/mumuainovel:latest - container_name: mumuainovel - depends_on: - postgres: - condition: service_healthy - ports: - - "${APP_PORT:-8000}:8000" - volumes: - - ./logs:/app/logs - - ./.env:/app/.env:ro - - ./storage/generated_covers:/app/backend/storage/generated_covers - environment: - # 应用配置 - - APP_NAME=${APP_NAME:-墨木灵思} - - APP_VERSION=${APP_VERSION:-1.0.0} - - APP_HOST=${APP_HOST:-0.0.0.0} - - APP_PORT=8000 - - DEBUG=${DEBUG:-false} - # 数据库配置 - - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-mumuai}:${POSTGRES_PASSWORD:-123456}@postgres:5432/${POSTGRES_DB:-mumuai_novel} - - DB_HOST=postgres - - DB_PORT=5432 - - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-123456} - # PostgreSQL 连接池配置 - - DATABASE_POOL_SIZE=${DATABASE_POOL_SIZE:-30} - - DATABASE_MAX_OVERFLOW=${DATABASE_MAX_OVERFLOW:-20} - - DATABASE_POOL_TIMEOUT=${DATABASE_POOL_TIMEOUT:-60} - - DATABASE_POOL_RECYCLE=${DATABASE_POOL_RECYCLE:-1800} - - DATABASE_POOL_PRE_PING=${DATABASE_POOL_PRE_PING:-True} - - DATABASE_POOL_USE_LIFO=${DATABASE_POOL_USE_LIFO:-True} - # 代理配置(可选) - - HTTP_PROXY=${HTTP_PROXY:-} - - HTTPS_PROXY=${HTTPS_PROXY:-} - - NO_PROXY=${NO_PROXY:-localhost,127.0.0.1} - # AI 服务配置 - - OPENAI_API_KEY=${OPENAI_API_KEY:-} - - OPENAI_BASE_URL=${OPENAI_BASE_URL:-https://api.openai.com/v1} - - GEMINI_API_KEY=${GEMINI_API_KEY:-} - - GEMINI_BASE_URL=${GEMINI_BASE_URL:-} - - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - - ANTHROPIC_BASE_URL=${ANTHROPIC_BASE_URL:-} - - DEFAULT_AI_PROVIDER=${DEFAULT_AI_PROVIDER:-openai} - - DEFAULT_MODEL=${DEFAULT_MODEL:-gpt-4o-mini} - - DEFAULT_TEMPERATURE=${DEFAULT_TEMPERATURE:-0.7} - - DEFAULT_MAX_TOKENS=${DEFAULT_MAX_TOKENS:-32000} - # LinuxDO OAuth 配置 - - LINUXDO_CLIENT_ID=${LINUXDO_CLIENT_ID:-11111} - - LINUXDO_CLIENT_SECRET=${LINUXDO_CLIENT_SECRET:-11111} - - LINUXDO_REDIRECT_URI=${LINUXDO_REDIRECT_URI:-http://localhost:8000/api/auth/linuxdo/callback} - - FRONTEND_URL=${FRONTEND_URL:-http://localhost:8000} - # 本地账户登录配置 - - LOCAL_AUTH_ENABLED=${LOCAL_AUTH_ENABLED:-true} - - LOCAL_AUTH_USERNAME=${LOCAL_AUTH_USERNAME:-admin} - - LOCAL_AUTH_PASSWORD=${LOCAL_AUTH_PASSWORD:-admin123} - - LOCAL_AUTH_DISPLAY_NAME=${LOCAL_AUTH_DISPLAY_NAME:-本地管理员} - # 会话配置 - - SESSION_EXPIRE_MINUTES=${SESSION_EXPIRE_MINUTES:-120} - - SESSION_REFRESH_THRESHOLD_MINUTES=${SESSION_REFRESH_THRESHOLD_MINUTES:-30} - - SESSION_COOKIE_SECURE=${SESSION_COOKIE_SECURE:-true} - restart: unless-stopped - healthcheck: - test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 30s - networks: - - ai-story-network +> 若修改 `.env` 中 `APP_PORT`(例如 `10003`),访问地址为 `http://localhost:10003`。 -volumes: - postgres_data: - driver: local +### 默认账号 -networks: - ai-story-network: - driver: bridge -``` +在 `.env` 中启用本地登录时,默认配置示例: -
+| 项 | 默认值 | +|----|--------| +| 用户名 | `admin` | +| 密码 | `admin123` | -```bash -# 3. 启动服务 -docker-compose up -d +**请在生产环境中立即修改默认密码。** -# 4. 查看日志 -docker-compose logs -f +### 本地开发(可选) -# 5. 更新到最新版本 -docker-compose pull -docker-compose up -d -``` - -> **💡 提示**: Docker Hub 镜像已包含所有依赖和模型文件,无需额外下载 - -### 本地开发 / 从源码构建 - -#### 前置准备 - -```bash -# ⚠️ 重要:如果从源码构建,需要先下载 embedding 模型文件 -# 模型文件较大(约 400MB),需放置到以下目录: -# backend/embedding/models--sentence-transformers--paraphrase-multilingual-MiniLM-L12-v2/ -# -# 📥 获取方式: -# - 加入项目 QQ 群或 Linux DO 讨论区获取下载链接 -# - 群号:见项目主页 -# - Linux DO:https://linux.do/t/topic/1100112 -``` - -#### 后端 +**后端** ```bash cd backend python -m venv .venv -source .venv/bin/activate # Windows: .venv\Scripts\activate +# Windows: .venv\Scripts\activate +# Linux/macOS: source .venv/bin/activate pip install -r requirements.txt - -# 配置 .env 文件 cp .env.example .env -# 编辑 .env 填入必要配置 - -# 启动 PostgreSQL(可使用 Docker) -docker run -d --name postgres \ - -e POSTGRES_PASSWORD=your_password \ - -e POSTGRES_DB=mumuai_novel \ - -p 5432:5432 \ - postgres:18-alpine - -# 启动后端 -python -m uvicorn app.main:app --host localhost --port 8000 --reload +# 配置 DATABASE_URL 与 AI Key 后启动 +python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload ``` -#### 前端 +**前端** ```bash cd frontend npm install -npm run dev # 开发模式 -npm run build # 生产构建 +npm run dev ``` -## ⚙️ 配置说明 +生产构建:`npm run build`,产物由后端 `backend/static` 托管。 -### 必需配置 - -创建 `.env` 文件: +### 必需配置示例 ```bash -# PostgreSQL 数据库(必需) -DATABASE_URL=postgresql+asyncpg://mumuai:your_password@postgres:5432/mumuai_novel +# 数据库(Docker 场景由 compose 注入,本地需自行填写) POSTGRES_PASSWORD=your_secure_password -# AI 服务 -OPENAI_API_KEY=your_openai_key +# AI 服务(至少配置一项) +OPENAI_API_KEY=your_key OPENAI_BASE_URL=https://api.openai.com/v1 DEFAULT_AI_PROVIDER=openai DEFAULT_MODEL=gpt-4o-mini -# 本地账户登录 +# 本地登录 LOCAL_AUTH_ENABLED=true LOCAL_AUTH_USERNAME=admin LOCAL_AUTH_PASSWORD=your_password ``` -### 可选配置 +### 常见问题 -```bash -# LinuxDO OAuth -LINUXDO_CLIENT_ID=your_client_id -LINUXDO_CLIENT_SECRET=your_client_secret -LINUXDO_REDIRECT_URI=http://localhost:8000/api/auth/callback - -# PostgreSQL 连接池(高并发优化) -DATABASE_POOL_SIZE=30 -DATABASE_MAX_OVERFLOW=20 - -# 会话 Cookie Secure 标记 -# 默认 true,适合 HTTPS 部署;如果使用 HTTP 访问并且浏览器不保存登录 Cookie,可设为 false -SESSION_COOKIE_SECURE=true -``` - -> **🔐 Cookie Secure 说明** -> -> - HTTPS 部署:建议保持 `SESSION_COOKIE_SECURE=true`,浏览器只会通过 HTTPS 发送登录 Cookie。 -> - HTTP 部署:如果登录后浏览器没有保存 Cookie,请在 `.env` 中设置 `SESSION_COOKIE_SECURE=false`,然后重启后端或 Docker 容器。 -> - Docker Compose 示例默认使用 `SESSION_COOKIE_SECURE=${SESSION_COOKIE_SECURE:-true}`,如需关闭必须在 `.env` 中显式配置。 - -### 中转 API 配置 - -支持所有 OpenAI 兼容格式的中转服务: - -```bash -# New API 示例 -OPENAI_API_KEY=sk-xxxxxxxx -OPENAI_BASE_URL=https://api.new-api.com/v1 - -# 其他中转服务 -OPENAI_BASE_URL=https://your-proxy-service.com/v1 -``` - -## 🐳 Docker 部署详情 - -### 服务架构 - -- **postgres**: PostgreSQL 18 数据库 - - 端口: 5432 - - 数据持久化: `postgres_data` volume - - 初始化脚本: `backend/scripts/init_postgres.sql`(自动挂载) - - 优化配置: 支持 80-150 并发用户 - -- **mumuainovel**: 主应用服务 - - 端口: 8000 - - 日志目录: `./logs` - - 配置挂载: `.env` 文件 - - 自动等待数据库就绪 - - 健康检查: 每 30 秒检测一次 - -### 重要文件说明 - -| 文件 | 说明 | 是否必需 | -|------|------|---------| -| `.env` | 环境配置(API Key、数据库密码等) | ✅ 必需 | -| `docker-compose.yml` | 服务编排配置 | ✅ 必需 | -| `backend/scripts/init_postgres.sql` | PostgreSQL 扩展安装脚本 | ✅ 自动挂载 | -| `backend/embedding/models--*/` | Embedding 模型文件 | ⚠️ 自建需要 | - -> **注意**: 使用 Docker Hub 镜像时,模型文件已包含在镜像中,无需额外下载 - -### 常用命令 - -```bash -# 启动服务 -docker-compose up -d - -# 查看状态 -docker-compose ps - -# 查看日志 -docker-compose logs -f - -# 停止服务 -docker-compose down - -# 重启服务 -docker-compose restart - -# 查看资源使用 -docker stats -``` - -### 数据持久化 - -- `./postgres_data` - PostgreSQL 数据库文件 -- `./logs` - 应用日志文件 - -### 端口配置 - -修改 `docker-compose.yml` 中的端口映射: - -```yaml -ports: - - "8800:8000" # 宿主机:容器 -``` - -## 📁 项目结构 - -``` -墨木灵思/ -├── backend/ # 后端服务 -│ ├── app/ -│ │ ├── api/ # API 路由 -│ │ ├── models/ # 数据模型 -│ │ ├── services/ # 业务逻辑 -│ │ ├── middleware/ # 中间件 -│ │ ├── database.py # 数据库连接 -│ │ └── main.py # 应用入口 -│ ├── scripts/ # 工具脚本 -│ └── requirements.txt # Python 依赖 -├── frontend/ # 前端应用 -│ ├── src/ -│ │ ├── pages/ # 页面组件 -│ │ ├── components/ # 通用组件 -│ │ ├── services/ # API 服务 -│ │ └── store/ # 状态管理 -│ └── package.json -├── docker-compose.yml # Docker Compose 配置 -├── Dockerfile # Docker 镜像构建 -└── README.md -``` - -## 🛠️ 技术栈 - -**后端**: FastAPI • PostgreSQL • SQLAlchemy • OpenAI/Claude/Gemini SDK - -**前端**: React 18 • TypeScript • Ant Design • Zustand • Vite - -## 📖 使用指南 - -1. **登录系统** - 使用本地账户或 LinuxDO 账户 -2. **创建项目** - 选择"使用向导创建" -3. **AI 生成** - 输入基本信息,AI 自动生成大纲和角色 -4. **编辑完善** - 管理角色关系,生成和编辑章节 - -### API 文档 - -- Swagger UI: `http://localhost:8000/docs` -- ReDoc: `http://localhost:8000/redoc` - -## 🤝 贡献 - -欢迎提交 Issue 和 Pull Request! - -1. Fork 本项目 -2. 创建特性分支 (`git checkout -b feature/AmazingFeature`) -3. 提交更改 (`git commit -m 'Add some AmazingFeature'`) -4. 推送到分支 (`git push origin feature/AmazingFeature`) -5. 提交 Pull Request - -### 贡献者 - -感谢所有为本项目做出贡献的开发者! - - - - - -## 📝 许可证 - -本项目采用 [GNU General Public License v3.0](LICENSE) - -**GPL v3 意味着:** -- ✅ 可自由使用、修改和分发 -- ✅ 可用于商业目的 -- 📝 必须开源修改版本 -- 📝 必须保留原作者版权 -- 📝 衍生作品必须使用 GPL v3 协议 - -## 🙏 致谢 - -- [FastAPI](https://fastapi.tiangolo.com/) - Python Web 框架 -- [React](https://react.dev/) - 前端框架 -- [Ant Design](https://ant.design/) - UI 组件库 -- [PostgreSQL](https://www.postgresql.org/) - 数据库 - -## 📧 联系方式 - -- 提交 [Issue](https://github.com/xiamuceer-j/墨木灵思/issues) -- Linux DO [讨论](https://linux.do/t/topic/1106333) -- 加入QQ群 [QQ群](frontend/public/qq.jpg) -- 加入WX群 [WX群](frontend/public/WX.png) +| 问题 | 解决方案 | +|------|----------| +| 启动后无法访问 | 检查 `APP_PORT` 映射与防火墙;`docker compose ps` 确认容器健康 | +| 登录后 Cookie 未保存 | HTTP 部署时将 `SESSION_COOKIE_SECURE=false` 写入 `.env` 并重启 | +| AI 调用失败 | 核对 API Key、Base URL 及网络代理(`HTTP_PROXY` / `HTTPS_PROXY`) | +| 数据库连接失败 | 确认 PostgreSQL 容器已 healthy;检查 `DATABASE_URL` 用户名密码 | +| 自建镜像缺少 Embedding | 使用官方镜像或按文档将模型放入 `backend/embedding/` 目录 | --- -
+## 8. 运行示例截图 -**如果这个项目对你有帮助,请给个 ⭐️ Star!** +![image-20260518142402652](images/image-20260518142402652.png) -Made with ❤️ +![image-20260518142411086](images/image-20260518142411086.png) -
+![image-20260518142620791](images/image-20260518142620791.png) -## Star History +![image-20260518142641345](images/image-20260518142641345.png) - - - - - Star History Chart - - +![image-20260518143108652](images/image-20260518143108652.png) -## History +![image-20260518143113951](images/image-20260518143113951.png) -![Alt](https://repobeats.axiom.co/api/embed/ee7141a5f269c64759302e067abe23b46796bafe.svg "Repobeats analytics image") +![image-20260518143117651](images/image-20260518143117651.png) + +![image-20260518143121175](images/image-20260518143121175.png) + +![image-20260518143125197](images/image-20260518143125197.png) + +![image-20260518143134898](images/image-20260518143134898.png) diff --git a/backend/.env.example b/backend/.env.example index 56d00f0..cb45f07 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -19,13 +19,13 @@ TZ=Asia/Shanghai # ========================================== # PostgreSQL 连接信息 -POSTGRES_DB=mumuai_novel -POSTGRES_USER=mumuai +POSTGRES_DB=mumulingsi_novel +POSTGRES_USER=mumulingsi POSTGRES_PASSWORD=123456 POSTGRES_PORT=5432 # 数据库连接 URL(Docker 部署时自动生成) -# DATABASE_URL=postgresql+asyncpg://mumuai:123456@localhost:5432/mumuai_novel +# DATABASE_URL=postgresql+asyncpg://mumulingsi:123456@localhost:5432/mumulingsi_novel # ========================================== # SQLite 数据库配置 @@ -45,7 +45,7 @@ LOG_BACKUP_COUNT=30 # ========================================== # CORS 配置 # ========================================== -CORS_ORIGINS=["http://localhost:8000","http://127.0.0.1:8000"] +CORS_ORIGINS=["http://localhost:3000","http://127.0.0.1:8000"] # ========================================== # 代理配置(可选) @@ -116,11 +116,11 @@ EMAIL_VERIFICATION_RESEND_INTERVAL_SECONDS=60 # 提示词工坊配置 # ========================================== # 运行模式:client(本地部署)或 server(云端服务器) -# 只有 mumuverse.space:1566 需要设置为 server +# 云端服务配置示例 WORKSHOP_MODE=client # 云端服务地址(client 模式使用) -WORKSHOP_CLOUD_URL=https://mumuverse.space:1566 +WORKSHOP_CLOUD_URL= # 云端 API 请求超时时间(秒) WORKSHOP_API_TIMEOUT=30 diff --git a/backend/alembic-postgres.ini b/backend/alembic-postgres.ini index fa9a3df..688e0f8 100644 --- a/backend/alembic-postgres.ini +++ b/backend/alembic-postgres.ini @@ -1,5 +1,5 @@ # Alembic Database Migration Profile - PostgreSQL -# Database version management for the 墨木灵思 project +# Database version management for the mumulingsi project [alembic] # Migration Script storage directory (PostgreSQL) @@ -10,7 +10,7 @@ file_template = %%(year)d%%(month).2d%%(day).2d_%%(hour).2d%%(minute).2d_%%(rev) # Database connection string # Note: The actual connection string is read from the environment variable in env.py -# sqlalchemy.url = postgresql+asyncpg://mumuai:password@localhost:5432/mumuai_novel +# sqlalchemy.url = postgresql+asyncpg://mumulingsi:password@localhost:5432/mumulingsi_novel # Log Configuration [loggers] diff --git a/backend/alembic-sqlite.ini b/backend/alembic-sqlite.ini index a394c9b..16f6263 100644 --- a/backend/alembic-sqlite.ini +++ b/backend/alembic-sqlite.ini @@ -1,5 +1,5 @@ # Alembic Database Migration Profile - SQLite -# Database version management for the 墨木灵思 project +# Database version management for the mumulingsi project [alembic] # Migration Script storage directory (SQLite) diff --git a/backend/alembic/README b/backend/alembic/README index 46ff504..2bc1639 100644 --- a/backend/alembic/README +++ b/backend/alembic/README @@ -59,7 +59,7 @@ alembic -c alembic-postgres.ini current #### 配置环境变量 ```bash # .env 文件 -DATABASE_URL=sqlite+aiosqlite:///./data/mumuai.db +DATABASE_URL=sqlite+aiosqlite:///./data/mumulingsi.db ``` #### 生成迁移脚本 @@ -115,7 +115,7 @@ alembic -c alembic-sqlite.ini current ```bash # 切换到 SQLite -DATABASE_URL=sqlite+aiosqlite:///./data/mumuai.db +DATABASE_URL=sqlite+aiosqlite:///./data/mumulingsi.db alembic -c alembic-sqlite.ini upgrade head # 切换到 PostgreSQL diff --git a/backend/app/api/changelog.py b/backend/app/api/changelog.py index 5426f52..7ff62e0 100644 --- a/backend/app/api/changelog.py +++ b/backend/app/api/changelog.py @@ -21,8 +21,8 @@ def require_login(request: Request): # GitHub API配置 GITHUB_API_BASE = "https://api.github.com" -REPO_OWNER = "xiamuceer-j" -REPO_NAME = "墨木灵思" +REPO_OWNER = "mumulingsi-project" +REPO_NAME = "mumulingsi" # 缓存配置 _cache = { @@ -88,7 +88,7 @@ async def fetch_github_commits(page: int = 1, per_page: int = 30) -> List[dict]: headers = { "Accept": "application/vnd.github.v3+json", - "User-Agent": "墨木灵思-App" + "User-Agent": "mumulingsi-App" } try: diff --git a/backend/app/config.py b/backend/app/config.py index f2163ae..3cbacb9 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -16,7 +16,7 @@ config_logger = logging.getLogger(__name__) # 数据库配置:PostgreSQL # 从环境变量获取数据库URL -DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://mumuai:password@localhost:5432/mumuai_novel") +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://mumulingsi:password@localhost:5432/mumulingsi_novel") config_logger.debug(f"数据库类型: PostgreSQL") config_logger.debug(f"数据库URL: {DATABASE_URL}") @@ -39,7 +39,7 @@ class Settings(BaseSettings): log_backup_count: int = 30 # 保留30个备份文件 # CORS配置 - cors_origins: list[str] = ["http://localhost:8000", "http://127.0.0.1:8000"] + cors_origins: list[str] = ["http://localhost:3000", "http://127.0.0.1:8000"] # 数据库配置 - PostgreSQL database_url: str = DATABASE_URL @@ -126,7 +126,7 @@ class Settings(BaseSettings): # 提示词工坊配置 WORKSHOP_MODE: str = "client" # client: 本地部署实例, server: 云端中央服务器 - WORKSHOP_CLOUD_URL: str = "https://mumuverse.space:1566" # 云端服务地址 + WORKSHOP_CLOUD_URL: str = "" # 云端服务地址 WORKSHOP_API_TIMEOUT: int = 30 # 云端API请求超时时间(秒) class Config: diff --git a/backend/app/main.py b/backend/app/main.py index 7df5aa0..1792632 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -141,6 +141,12 @@ async def db_session_stats(request: Request): } +@app.get("/nanobot/sessions") +async def nanobot_sessions(): + """兼容层:为环境监控工具提供空的会话列表""" + return [] + + from app.api import ( projects, outlines, characters, chapters, wizard_stream, relationships, organizations, diff --git a/backend/app/security.py b/backend/app/security.py index c75bcc2..66ae261 100644 --- a/backend/app/security.py +++ b/backend/app/security.py @@ -24,7 +24,7 @@ def _session_secret() -> bytes: or settings.openai_api_key ) if not secret: - secret = "mumuainovel-development-session-secret" + secret = "mumulingsi-development-session-secret" return str(secret).encode("utf-8") diff --git a/backend/app/services/ai_service.py b/backend/app/services/ai_service.py index d31e6f5..43da425 100644 --- a/backend/app/services/ai_service.py +++ b/backend/app/services/ai_service.py @@ -29,7 +29,7 @@ logger = get_logger(__name__) def normalize_provider(provider: Optional[str]) -> Optional[str]: """标准化 provider 名称,兼容渠道别名。""" - if provider == "mumu": + if provider == "xinmi": return "openai" return provider diff --git a/backend/app/services/cover_generation_service.py b/backend/app/services/cover_generation_service.py index 399e8f2..1a14e8f 100644 --- a/backend/app/services/cover_generation_service.py +++ b/backend/app/services/cover_generation_service.py @@ -225,11 +225,11 @@ class CoverGenerationService: return GeminiCoverProvider(api_key=api_key, base_url=normalized_base_url) if provider_value == "grok": return GrokCoverProvider(api_key=api_key, base_url=normalized_base_url) - if provider_value == "mumu": + if provider_value == "xinmi": if normalized_base_url.endswith("/v1beta"): return GeminiCoverProvider(api_key=api_key, base_url=normalized_base_url) - return GrokCoverProvider(api_key=api_key, base_url=normalized_base_url or "https://api.mumuverse.space/v1") - raise HTTPException(status_code=400, detail="当前版本仅支持 Gemini、Grok 或 MuMuのAPI 作为封面图片 Provider") + return GrokCoverProvider(api_key=api_key, base_url=normalized_base_url or "v1") + raise HTTPException(status_code=400, detail="当前版本仅支持 Gemini、Grok 或 墨木灵思 API 作为封面图片 Provider") def _save_cover_file( self, diff --git a/backend/scripts/entrypoint.sh b/backend/scripts/entrypoint.sh index 53b7e0a..95839c8 100644 --- a/backend/scripts/entrypoint.sh +++ b/backend/scripts/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Docker 容器启动入口脚本 # 功能:等待数据库就绪,执行迁移,启动应用 @@ -31,8 +31,8 @@ echo "================================================" # 数据库配置(从环境变量读取) DB_HOST="${DB_HOST:-postgres}" DB_PORT="${DB_PORT:-5432}" -DB_USER="${POSTGRES_USER:-mumuai}" -DB_NAME="${POSTGRES_DB:-mumuai_novel}" +DB_USER="${POSTGRES_USER:-mumulingsi}" +DB_NAME="${POSTGRES_DB:-mumulingsi_novel}" # 等待数据库就绪 echo "⏳ 等待数据库启动..." diff --git a/backend/scripts/init_postgres.sql b/backend/scripts/init_postgres.sql index 87f149e..c17e5c6 100644 --- a/backend/scripts/init_postgres.sql +++ b/backend/scripts/init_postgres.sql @@ -8,7 +8,7 @@ CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- 模糊搜索和全文检索支 DO $$ BEGIN RAISE NOTICE '=================================================='; - RAISE NOTICE '墨木灵思 PostgreSQL 扩展安装完成'; + RAISE NOTICE 'mumulingsi PostgreSQL 扩展安装完成'; RAISE NOTICE '已安装扩展:'; RAISE NOTICE ' - uuid-ossp: UUID生成支持'; RAISE NOTICE ' - pg_trgm: 模糊搜索和全文检索支持'; diff --git a/backend/scripts/setup_postgres.py b/backend/scripts/setup_postgres.py index 7a486d8..461a190 100644 --- a/backend/scripts/setup_postgres.py +++ b/backend/scripts/setup_postgres.py @@ -56,8 +56,8 @@ class PostgreSQLSetup: port: int = 5432, admin_user: str = "postgres", admin_password: str = None, - db_name: str = "mumuai_novel", - db_user: str = "mumuai", + db_name: str = "xinmi_novel", + db_user: str = "mumulingsi", db_password: str = "123456" ): """ @@ -374,9 +374,9 @@ async def main(): admin_password = getpass(f"管理员密码: ") print("\n请输入要创建的数据库信息:\n") - db_name = input("数据库名 [mumuai_novel]: ").strip() or "mumuai_novel" - db_user = input("数据库用户名 [mumuai]: ").strip() or "mumuai" - db_password = getpass("数据库用户密码 [mumuai123]: ") or "mumuai123" + db_name = input("数据库名 [xinmi_novel]: ").strip() or "xinmi_novel" + db_user = input("数据库用户名 [mumulingsi]: ").strip() or "mumulingsi" + db_password = getpass("数据库用户密码 [xinmi123]: ") or "xinmi123" print(f"\n{'='*60}") print(f"配置摘要:") diff --git a/docker-compose.yml b/docker-compose.yml index a0472b0..ee337f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,10 @@ services: postgres: image: postgres:18-alpine - container_name: mumuainovel-postgres + container_name: mumulingsi-postgres environment: - POSTGRES_DB: ${POSTGRES_DB:-mumuai_novel} - POSTGRES_USER: ${POSTGRES_USER:-mumuai} + POSTGRES_DB: ${POSTGRES_DB:-mumulingsi_novel} + POSTGRES_USER: ${POSTGRES_USER:-mumulingsi} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-123456} POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" TZ: ${TZ:-Asia/Shanghai} @@ -15,7 +15,7 @@ services: - "${POSTGRES_PORT:-5432}:5432" restart: unless-stopped healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mumuai} -d ${POSTGRES_DB:-mumuai_novel}"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mumulingsi} -d ${POSTGRES_DB:-mumulingsi_novel}"] interval: 10s timeout: 5s retries: 5 @@ -49,12 +49,14 @@ services: - -c - max_wal_size=${POSTGRES_MAX_WAL_SIZE:-4GB} - mumuainovel: + mumulingsi: build: context: . dockerfile: Dockerfile - image: mumujie/mumuainovel:latest - container_name: mumuainovel + args: + - USE_CN_MIRROR=${USE_CN_MIRROR:-false} + image: mumulingsi-project/mumulingsi:latest + container_name: mumulingsi depends_on: postgres: condition: service_healthy @@ -76,11 +78,13 @@ services: - DEBUG=${DEBUG:-false} # 数据库配置 - - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-mumuai}:${POSTGRES_PASSWORD:-123456}@postgres:5432/${POSTGRES_DB:-mumuai_novel} + - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER:-mumulingsi}:${POSTGRES_PASSWORD:-123456}@postgres:5432/${POSTGRES_DB:-mumulingsi_novel} - # 数据库连接信息(用于 entrypoint.sh) + # 数据库连接信息(用于 entrypoint.sh,须与 postgres 服务一致) - DB_HOST=postgres - DB_PORT=5432 + - POSTGRES_USER=${POSTGRES_USER:-mumulingsi} + - POSTGRES_DB=${POSTGRES_DB:-mumulingsi_novel} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-123456} # PostgreSQL 连接池配置 diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..74470b2 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# XinMi API 文档地址(默认 https://api.xinmi.cloud,一般无需修改) +# VITE_OFFICIAL_API_DOC_URL=https://api.xinmi.cloud diff --git a/frontend/index.html b/frontend/index.html index 73130fb..d14ca90 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,9 @@ + + + 墨木灵思 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a3661a8..27305fc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -27,14 +27,11 @@ import Login from './pages/Login'; import AuthCallback from './pages/AuthCallback'; import ProtectedRoute from './components/ProtectedRoute'; import AppFooter from './components/AppFooter'; -import SpringFestival from './components/SpringFestival'; import './App.css'; function App() { return ( <> - {/* 🧧 春节喜庆装饰 */} - void; - onDoNotShowToday: () => void; - onNeverShow: () => void; -} - -export default function AnnouncementModal({ visible, onClose, onDoNotShowToday, onNeverShow }: AnnouncementModalProps) { - const [qqImageError, setQqImageError] = useState(false); - const [wxImageError, setWxImageError] = useState(false); - const { token } = theme.useToken(); - const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`; - - useEffect(() => { - if (visible) { - setQqImageError(false); - setWxImageError(false); - } - }, [visible]); - - const handleDoNotShowToday = () => { - onDoNotShowToday(); - onClose(); - }; - - const handleNeverShow = () => { - onNeverShow(); - onClose(); - }; - - return ( - - 🎉 欢迎使用 AI小说创作助手 - - } - open={visible} - onCancel={onClose} - footer={ - - - - - } - width={700} - centered - styles={{ - body: { - padding: '20px', - background: token.colorBgContainer, - }, - header: { - background: `linear-gradient(135deg, ${alphaColor(token.colorPrimary, 0.1)} 0%, ${alphaColor(token.colorBgContainer, 0.98)} 100%)`, - borderBottom: `1px solid ${token.colorBorderSecondary}`, - padding: '16px 24px', - }, - footer: { - background: token.colorBgContainer, - borderTop: `1px solid ${token.colorBorderSecondary}`, - padding: '16px 24px', - }, - }} - > -
-
-

👋 欢迎加入我们的交流群!在这里你可以:

-
    -
  • 💬 与其他创作者交流心得
  • -
  • 💡 获取最新功能更新和使用技巧
  • -
  • 🐛 反馈问题和建议
  • -
  • 📚 分享创作经验和灵感
  • -
-

- 扫描下方二维码加入交流群: -

-
- -
- {/* QQ 二维码 */} -
-

- QQ交流群 -

- {!qqImageError ? ( -
- QQ交流群二维码 setQqImageError(true)} - /> -
- ) : ( -
-

二维码加载失败

-
- )} -
- - {/* 微信二维码 */} -
-

- 微信交流群 -

- {!wxImageError ? ( -
- 微信交流群二维码 setWxImageError(true)} - /> -
- ) : ( -
-

二维码加载失败

-
- )} -
-
- -
- 💡 提示:选择"今日内不再展示"当天不再显示,选择"永不再展示"将永久隐藏此公告 -
-
-
- ); -} \ No newline at end of file diff --git a/frontend/src/components/AppFooter.tsx b/frontend/src/components/AppFooter.tsx index 68bf63b..c176084 100644 --- a/frontend/src/components/AppFooter.tsx +++ b/frontend/src/components/AppFooter.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; -import { Typography, Space, Divider, Badge, Button, Grid, theme } from 'antd'; -import { GithubOutlined, CopyrightOutlined, HeartFilled, ClockCircleOutlined, GiftOutlined } from '@ant-design/icons'; +import { Typography, Divider, Badge, Grid, theme } from 'antd'; +import { ClockCircleOutlined, CopyrightOutlined } from '@ant-design/icons'; import { VERSION_INFO, getVersionString } from '../config/version'; import { checkLatestVersion } from '../services/versionService'; @@ -21,7 +21,6 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) { const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`; useEffect(() => { - // 检查版本更新(每次都重新检查) const checkVersion = async () => { try { const result = await checkLatestVersion(); @@ -33,19 +32,16 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) { } }; - // 延迟3秒后检查,避免影响首次加载 const timer = setTimeout(checkVersion, 3000); return () => clearTimeout(timer); }, []); - // 点击版本号查看更新 const handleVersionClick = () => { if (hasUpdate && releaseUrl) { window.open(releaseUrl, '_blank'); } }; - // 计算左边距:桌面端有侧边栏时需要偏移 const leftOffset = isMobile ? 0 : sidebarWidth; return ( @@ -61,8 +57,8 @@ export default function AppFooter({ sidebarWidth = 0 }: AppFooterProps) { padding: isMobile ? '8px 12px' : '10px 16px', zIndex: 100, boxShadow: `0 -2px 16px ${alphaColor(token.colorText, 0.08)}`, - backgroundColor: alphaColor(token.colorBgContainer, 0.82), // 半透明背景以支持 backdrop-filter - transition: 'left 0.3s ease', // 平滑过渡 + backgroundColor: alphaColor(token.colorBgContainer, 0.82), + transition: 'left 0.3s ease', }} >
- {isMobile ? ( - // 移动端:紧凑单行布局 -
- - - {VERSION_INFO.projectName} - {getVersionString()} - - - - - - - - - - - {VERSION_INFO.buildTime} - -
- ) : ( - // PC端:完整布局 - } + {/* 版本信息 */} + + - {/* 版本信息 */} - - { - if (hasUpdate) { - e.currentTarget.style.transform = 'scale(1.05)'; - } - }} - onMouseLeave={(e) => { - if (hasUpdate) { - e.currentTarget.style.transform = 'scale(1)'; - } - }} - title={hasUpdate ? `发现新版本 v${latestVersion},点击查看` : '当前版本'} - > - {VERSION_INFO.projectName} - {getVersionString()} - - + {VERSION_INFO.projectName} + {getVersionString()} + + - {/* GitHub 链接 */} - - - GitHub - + - {/* 资源模块 */} - - 源码库 - + {/* 许可证 */} + + + {VERSION_INFO.license} + - {/* LinuxDO 社区 */} - - LinuxDO 社区 - + - {/* 赞助按钮 */} - - - {/* 许可证 */} - - - {VERSION_INFO.license} - - - {/* 更新时间 */} - - - {VERSION_INFO.buildTime} - - - {/* 致谢信息 */} - - Made with - - by {VERSION_INFO.author} - - - )} + {/* 更新时间 */} + + + {VERSION_INFO.buildTime} +
- ); } \ No newline at end of file diff --git a/frontend/src/components/CardStyles.tsx b/frontend/src/components/CardStyles.tsx index 162b8f3..5ad5b3d 100644 --- a/frontend/src/components/CardStyles.tsx +++ b/frontend/src/components/CardStyles.tsx @@ -20,10 +20,7 @@ const bookshelfNewHoverShadow = ` inset 0 1px 0 color-mix(in srgb, var(--ant-color-bg-container) 90%, transparent) `; -const promptTemplateBaseShadow = ` - 0 6px 16px color-mix(in srgb, var(--ant-color-text) 11%, transparent), - 0 1px 0 color-mix(in srgb, var(--ant-color-white) 42%, transparent) inset -`; +const promptTemplateBaseShadow = '4px 4px 0 color-mix(in srgb, var(--ant-color-text) 10%, transparent)'; // BookshelfPage 样式(书架/书本卡片) export const bookshelfCardStyles = { @@ -107,28 +104,25 @@ export const bookshelfCardHoverHandlers = { export const promptTemplateCardStyles = { templateCard: { height: '100%', - borderRadius: 14, + borderRadius: 2, overflow: 'hidden', - border: '1px solid color-mix(in srgb, var(--ant-color-text) 8%, transparent)', - background: 'linear-gradient(180deg, color-mix(in srgb, var(--ant-color-bg-container) 97%, var(--ant-color-primary) 3%) 0%, var(--ant-color-bg-container) 100%)', + border: '1px solid color-mix(in srgb, var(--ant-color-text) 14%, transparent)', + background: 'var(--ant-color-bg-container)', boxShadow: promptTemplateBaseShadow, - transition: 'transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.28s cubic-bezier(0.22, 1, 0.36, 1), border-color 0.28s ease', + transition: 'transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease', } as CSSProperties, }; export const promptTemplateCardHoverHandlers = { onMouseEnter: (e: React.MouseEvent) => { const target = e.currentTarget; - target.style.transform = 'translateY(-6px)'; - target.style.boxShadow = ` - 0 14px 24px color-mix(in srgb, var(--ant-color-text) 16%, transparent), - 0 1px 0 color-mix(in srgb, var(--ant-color-white) 48%, transparent) inset - `; + target.style.transform = 'translate(-2px, -2px)'; + target.style.boxShadow = '6px 6px 0 color-mix(in srgb, var(--ant-color-primary) 25%, transparent)'; target.style.borderColor = 'color-mix(in srgb, var(--ant-color-primary) 24%, transparent)'; }, onMouseLeave: (e: React.MouseEvent) => { const target = e.currentTarget; - target.style.transform = 'translateY(0)'; + target.style.transform = 'translate(0, 0)'; target.style.boxShadow = promptTemplateBaseShadow; target.style.borderColor = 'color-mix(in srgb, var(--ant-color-text) 8%, transparent)'; }, diff --git a/frontend/src/components/ChangelogFloatingButton.tsx b/frontend/src/components/ChangelogFloatingButton.tsx deleted file mode 100644 index 799ed7a..0000000 --- a/frontend/src/components/ChangelogFloatingButton.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useState } from 'react'; -import { FloatButton, Grid } from 'antd'; -import { FileTextOutlined } from '@ant-design/icons'; -import ChangelogModal from './ChangelogModal'; - -const { useBreakpoint } = Grid; - -export default function ChangelogFloatingButton() { - const [showChangelog, setShowChangelog] = useState(false); - const screens = useBreakpoint(); - const isMobile = !screens.md; - - return ( - <> - } - type="primary" - tooltip="查看更新日志" - style={{ - // 桌面端时,确保按钮在主内容区域内(侧边栏右侧) - right: 24, - bottom: 100, - // 移动端无侧边栏,不需要额外处理 - ...(isMobile ? {} : { - // 确保 zIndex 低于侧边栏但高于内容 - zIndex: 999, - }), - }} - onClick={() => setShowChangelog(true)} - /> - - setShowChangelog(false)} - /> - - ); -} \ No newline at end of file diff --git a/frontend/src/components/ChangelogModal.tsx b/frontend/src/components/ChangelogModal.tsx deleted file mode 100644 index 94f37df..0000000 --- a/frontend/src/components/ChangelogModal.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import { Modal, Timeline, Tag, Avatar, Empty, Spin, Button, Space } from 'antd'; -import { useState, useEffect } from 'react'; -import { - BugOutlined, - StarOutlined, - FileTextOutlined, - BgColorsOutlined, - ThunderboltOutlined, - ExperimentOutlined, - ToolOutlined, - QuestionCircleOutlined, - GithubOutlined, - ReloadOutlined, - ClockCircleOutlined, - SyncOutlined, -} from '@ant-design/icons'; -import { - fetchChangelog, - groupChangelogByDate, - cacheChangelog, - clearChangelogCache, - type ChangelogEntry, -} from '../services/changelogService'; - -interface ChangelogModalProps { - visible: boolean; - onClose: () => void; -} - -// 提交类型图标和颜色配置 -const typeConfig: Record = { - feature: { icon: , color: 'green', label: '新功能' }, - update: { icon: , color: 'geekblue', label: '更新' }, - fix: { icon: , color: 'red', label: '修复' }, - docs: { icon: , color: 'blue', label: '文档' }, - style: { icon: , color: 'purple', label: '样式' }, - refactor: { icon: , color: 'orange', label: '重构' }, - perf: { icon: , color: 'gold', label: '性能' }, - test: { icon: , color: 'cyan', label: '测试' }, - chore: { icon: , color: 'default', label: '杂项' }, - other: { icon: , color: 'default', label: '其他' }, -}; - -export default function ChangelogModal({ visible, onClose }: ChangelogModalProps) { - const [changelog, setChangelog] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(true); - - // 加载更新日志 - // 每次用户打开窗口时才同步获取最新数据,不自动刷新 - const loadChangelog = async (pageNum: number = 1, append: boolean = false) => { - setLoading(true); - setError(null); - - try { - // 每次打开都从网络获取最新数据 - const entries = await fetchChangelog(pageNum, 30); - - if (entries.length === 0) { - setHasMore(false); - } else { - if (append) { - setChangelog(prev => [...prev, ...entries]); - } else { - setChangelog(entries); - // 缓存第一页数据(用于分页加载时的数据持久化) - if (pageNum === 1) { - cacheChangelog(entries); - } - } - } - } catch (err) { - setError(err instanceof Error ? err.message : '获取更新日志失败'); - } finally { - setLoading(false); - } - }; - - // 初始加载 - useEffect(() => { - if (visible) { - loadChangelog(1, false); - setPage(1); - setHasMore(true); - } - }, [visible]); - - // 加载更多 - const handleLoadMore = () => { - const nextPage = page + 1; - setPage(nextPage); - loadChangelog(nextPage, true); - }; - - // 刷新(清除缓存并重新加载) - const handleRefresh = () => { - clearChangelogCache(); - setPage(1); - setHasMore(true); - loadChangelog(1, false); - }; - - // 按日期分组 - const groupedChangelog = groupChangelogByDate(changelog); - const sortedDates = Array.from(groupedChangelog.keys()).sort((a, b) => b.localeCompare(a)); - - // 格式化日期 - const formatDate = (dateStr: string) => { - const date = new Date(dateStr); - const now = new Date(); - const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) return '今天'; - if (diffDays === 1) return '昨天'; - if (diffDays < 7) return `${diffDays} 天前`; - - return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }); - }; - - // 格式化时间 - const formatTime = (dateStr: string) => { - return new Date(dateStr).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }); - }; - - return ( - - - 更新日志 - - - ) - } - - { - !hasMore && changelog.length > 0 && ( -
- 已显示所有更新日志 -
- ) - } - - )} - -
- 💡 提示:每次打开窗口时自动获取最新更新日志,数据来源于 GitHub 提交历史 -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/components/CharacterCareerCard.tsx b/frontend/src/components/CharacterCareerCard.tsx index d585021..c2ad408 100644 --- a/frontend/src/components/CharacterCareerCard.tsx +++ b/frontend/src/components/CharacterCareerCard.tsx @@ -6,7 +6,7 @@ import axios from 'axios'; const { TextArea } = Input; const { Text, Paragraph } = Typography; -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'; +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; interface CareerDetail { id: string; diff --git a/frontend/src/config/version.ts b/frontend/src/config/version.ts index 360d12c..fa3edfd 100644 --- a/frontend/src/config/version.ts +++ b/frontend/src/config/version.ts @@ -16,9 +16,12 @@ export const VERSION_INFO = { projectName: '墨木灵思', projectFullName: '墨木灵思 - AI 智能小说创作助手', - // 链接信息 - githubUrl: 'https://www.xinmi.cloud/', - linuxDoUrl: 'https://linux.do/t/topic/1106333', + // 链接信息(不在代码里写死 GitHub;需要时在 .env 里配置 Vite 变量) + linuxDoUrl: '', + /** XinMi API 官方地址(侧栏入口、设置页文档链接) */ + xinmiApiBaseUrl: 'http://api.xinmi.cloud', + officialApiDocUrl: + String(import.meta.env.VITE_OFFICIAL_API_DOC_URL ?? '').trim() || 'http://api.xinmi.cloud', // 许可证 license: 'GPL v3.0', diff --git a/frontend/src/index.css b/frontend/src/index.css index 5bb84b7..5fdf4cb 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -5,7 +5,14 @@ body, } :root { - font-family: "PingFang SC", "Microsoft YaHei", "Heiti SC", Inter, system-ui, sans-serif; + --app-font-serif: 'Noto Serif SC', 'Songti SC', 'SimSun', serif; + --app-font-sans: 'Source Sans 3', 'PingFang SC', 'Microsoft YaHei', sans-serif; + --app-font-mono: 'IBM Plex Mono', 'Consolas', monospace; + --app-ink: #292524; + --app-parchment: #f5f0e6; + --app-copper: #b45309; + --app-gold: #f59e0b; + font-family: var(--app-font-sans); line-height: 1.5715; font-weight: 400; font-synthesis: none; @@ -16,6 +23,41 @@ body, -ms-text-size-adjust: 100%; } +[data-theme-resolved='dark'] { + --app-ink: #e7e5e4; + --app-parchment: #0c0a09; + --app-copper: #f59e0b; + --app-gold: #fbbf24; +} + +.app-serif-title { + font-family: var(--app-font-serif); + letter-spacing: 0.02em; +} + +.app-shell-sider .ant-menu-dark, +.app-shell-sider .ant-menu { + background: transparent !important; +} + +.app-shell-sider .ant-menu-item-selected { + border-left: 3px solid var(--app-gold) !important; +} + +.app-login-tabs .ant-tabs-nav::before { + border-bottom-color: color-mix(in srgb, var(--app-ink) 12%, transparent); +} + +.app-login-tabs .ant-tabs-tab-active .ant-tabs-tab-btn { + color: var(--app-copper) !important; + font-weight: 700; +} + +.app-prompt-header { + border-left: 4px solid var(--app-copper); + padding-left: 20px; +} + body { margin: 0; min-height: 100vh; @@ -219,8 +261,8 @@ body { } .ant-tooltip .ant-tooltip-inner { - background: var(--app-tooltip-bg, #884d5c); - border-radius: 8px; + background: var(--app-tooltip-bg, #292524); + border-radius: 2px; padding: 8px 16px; font-weight: 500; box-shadow: 0 4px 12px var(--app-tooltip-shadow, rgba(136, 77, 92, 0.3)); diff --git a/frontend/src/pages/About.tsx b/frontend/src/pages/About.tsx index 81d888b..ec0648c 100644 --- a/frontend/src/pages/About.tsx +++ b/frontend/src/pages/About.tsx @@ -8,6 +8,8 @@ import { SmileOutlined } from '@ant-design/icons'; +import { VERSION_INFO } from '../config/version'; + const { Title, Paragraph, Text } = Typography; export default function About() { @@ -52,7 +54,7 @@ export default function About() {
关于 墨木灵思 - 墨木灵思 (MoMu LingSi) 是一款基于人工智能的智能小说创作助手,旨在帮助创作者更高效、更具创意地完成文学作品。 + 墨木灵思 是一款基于人工智能的智能小说创作助手,旨在帮助创作者更高效、更具创意地完成文学作品。
@@ -140,7 +142,16 @@ export default function About() {
- © 2026 墨木灵思团队 | 了解更多 + © 2026 墨木灵思团队 + {VERSION_INFO.officialApiDocUrl ? ( + <> + {' '} + |{' '} + + 了解更多 + + + ) : null}
diff --git a/frontend/src/pages/AuthCallback.tsx b/frontend/src/pages/AuthCallback.tsx index ed2786d..986e667 100644 --- a/frontend/src/pages/AuthCallback.tsx +++ b/frontend/src/pages/AuthCallback.tsx @@ -2,16 +2,15 @@ import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Spin, Result, Button, Modal, Input, message, theme } from 'antd'; import { authApi } from '../services/api'; -import AnnouncementModal from '../components/AnnouncementModal'; export default function AuthCallback() { const navigate = useNavigate(); const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading'); const [errorMessage, setErrorMessage] = useState(''); - const [showAnnouncement, setShowAnnouncement] = useState(false); const [showPasswordModal, setShowPasswordModal] = useState(false); const { token } = theme.useToken(); const alphaColor = (color: string, alpha: number) => `color-mix(in srgb, ${color} ${(alpha * 100).toFixed(0)}%, transparent)`; + interface PasswordStatus { has_password: boolean; has_custom_password: boolean; @@ -26,64 +25,33 @@ export default function AuthCallback() { useEffect(() => { const handleCallback = async () => { try { - // 后端会通过 Cookie 自动设置认证信息 - // 这里只需要验证登录状态 const currentUser = await authApi.getCurrentUser(); - - // 检查是否是首次登录(通过 Cookie 标记) const isFirstLogin = document.cookie.includes('first_login=true'); setStatus('success'); if (isFirstLogin) { - // 首次登录:生成默认密码并显示提示 const defaultPassword = `${currentUser.username}@666`; - const pwdStatus = { + setPasswordStatus({ has_password: false, has_custom_password: false, username: currentUser.username, default_password: defaultPassword - }; - setPasswordStatus(pwdStatus); - - // 清除首次登录标记 Cookie + }); document.cookie = 'first_login=; path=/; max-age=0'; - - // 显示密码初始化弹窗 - setTimeout(() => { - setShowPasswordModal(true); - }, 1000); + setTimeout(() => setShowPasswordModal(true), 1000); return; } - // 非首次登录:正常流程 - // 从 sessionStorage 获取重定向地址 const redirect = sessionStorage.getItem('login_redirect') || '/'; sessionStorage.removeItem('login_redirect'); - - // 检查是否永久隐藏公告或今日已隐藏 - const hideForever = localStorage.getItem('announcement_hide_forever'); - const hideToday = localStorage.getItem('announcement_hide_today'); - const today = new Date().toDateString(); - - if (hideForever === 'true' || hideToday === today) { - // 延迟一下再跳转,让用户看到成功提示 - setTimeout(() => { - navigate(redirect); - }, 1000); - } else { - // 延迟一下再显示公告,让用户看到成功提示 - setTimeout(() => { - setShowAnnouncement(true); - }, 1000); - } + setTimeout(() => navigate(redirect), 1000); } catch (error) { console.error('登录失败:', error); setStatus('error'); setErrorMessage('登录失败,请重试'); } }; - handleCallback(); }, [navigate]); @@ -98,9 +66,7 @@ export default function AuthCallback() { }}>
-
- 正在处理登录... -
+
正在处理登录...
); @@ -119,55 +85,21 @@ export default function AuthCallback() { status="error" title="登录失败" subTitle={errorMessage} - extra={ - - } + extra={} style={{ background: token.colorBgContainer, padding: 40, borderRadius: 8 }} /> ); } - const handleAnnouncementClose = () => { - setShowAnnouncement(false); - const redirect = sessionStorage.getItem('login_redirect') || '/'; - sessionStorage.removeItem('login_redirect'); - navigate(redirect); - }; - - const handleDoNotShowToday = () => { - // 设置今日不再显示 - const today = new Date().toDateString(); - localStorage.setItem('announcement_hide_today', today); - }; - - const handleNeverShow = () => { - // 设置永久不再显示 - localStorage.setItem('announcement_hide_forever', 'true'); - }; - const handleSetPassword = async () => { - // 如果没有输入新密码,使用默认密码 const passwordToSet = newPassword || passwordStatus?.default_password; - - if (!passwordToSet) { - message.error('请输入新密码'); - return; - } - if (passwordToSet.length < 6) { - message.error('密码长度至少为6个字符'); - return; - } - if (newPassword && newPassword !== confirmPassword) { - message.error('两次输入的密码不一致'); - return; - } + if (!passwordToSet) { message.error('请输入新密码'); return; } + if (passwordToSet.length < 6) { message.error('密码长度至少为6个字符'); return; } + if (newPassword && newPassword !== confirmPassword) { message.error('两次输入的密码不一致'); return; } setSettingPassword(true); try { - // 首次登录使用初始化接口,后续使用修改接口 const isFirstLogin = !passwordStatus?.has_password; if (isFirstLogin) { await authApi.initializePassword(passwordToSet); @@ -177,24 +109,9 @@ export default function AuthCallback() { message.success('密码设置成功'); } setShowPasswordModal(false); - - // 继续后续流程 const redirect = sessionStorage.getItem('login_redirect') || '/'; sessionStorage.removeItem('login_redirect'); - - const hideForever = localStorage.getItem('announcement_hide_forever'); - const hideToday = localStorage.getItem('announcement_hide_today'); - const today = new Date().toDateString(); - - if (hideForever === 'true' || hideToday === today) { - setTimeout(() => { - navigate(redirect); - }, 500); - } else { - setTimeout(() => { - setShowAnnouncement(true); - }, 500); - } + setTimeout(() => navigate(redirect), 500); } catch { message.error('密码设置失败,请重试'); } finally { @@ -203,46 +120,19 @@ export default function AuthCallback() { }; const handleSkipPasswordSetting = async () => { - // 首次登录时,如果跳过设置,使用默认密码初始化 const isFirstLogin = !passwordStatus?.has_password; if (isFirstLogin && passwordStatus?.default_password) { - try { - await authApi.initializePassword(passwordStatus.default_password); - } catch (error) { - console.error('初始化默认密码失败:', error); - } + try { await authApi.initializePassword(passwordStatus.default_password); } + catch (error) { console.error('初始化默认密码失败:', error); } } - setShowPasswordModal(false); - - // 继续后续流程 const redirect = sessionStorage.getItem('login_redirect') || '/'; sessionStorage.removeItem('login_redirect'); - - const hideForever = localStorage.getItem('announcement_hide_forever'); - const hideToday = localStorage.getItem('announcement_hide_today'); - const today = new Date().toDateString(); - - if (hideForever === 'true' || hideToday === today) { - setTimeout(() => { - navigate(redirect); - }, 500); - } else { - setTimeout(() => { - setShowAnnouncement(true); - }, 500); - } + setTimeout(() => navigate(redirect), 500); }; return ( <> - -
-

您已成功通过 Linux DO 授权登录!

-

系统已为您自动生成默认密码,您可以选择设置自定义密码或继续使用默认密码。

+

您已成功登录!

+

您可以选择设置自定义密码或继续使用默认密码。

{passwordStatus?.default_password && ( -
+
账号:{passwordStatus.username}
- 默认密码:{passwordStatus.default_password} + 默认密码:{passwordStatus.default_password}
)}
-
- setNewPassword(e.target.value)} - placeholder="请输入新密码" - style={{ marginTop: 4 }} - /> + setNewPassword(e.target.value)} placeholder="请输入新密码" style={{ marginTop: 4 }} />
- setConfirmPassword(e.target.value)} - placeholder="请再次输入密码" - style={{ marginTop: 4 }} - /> + setConfirmPassword(e.target.value)} placeholder="请再次输入密码" style={{ marginTop: 4 }} />
@@ -308,10 +176,10 @@ export default function AuthCallback() {
); -} \ No newline at end of file +} diff --git a/frontend/src/pages/BookshelfPage.tsx b/frontend/src/pages/BookshelfPage.tsx index 659e560..ad740e8 100644 --- a/frontend/src/pages/BookshelfPage.tsx +++ b/frontend/src/pages/BookshelfPage.tsx @@ -1,10 +1,11 @@ -import { Card, Button, Spin, Space, Tag, Typography, Alert, theme } from 'antd'; -import { BookOutlined, RocketOutlined, BulbOutlined, UploadOutlined, DownloadOutlined, LoadingOutlined, CalendarOutlined, DeleteOutlined, CheckCircleOutlined, EditOutlined, PauseCircleOutlined, PictureOutlined, SwapOutlined, ReloadOutlined } from '@ant-design/icons'; +import { Card, Button, Spin, Space, Tag, Typography, theme } from 'antd'; +import { BookOutlined, RocketOutlined, BulbOutlined, UploadOutlined, DownloadOutlined, LoadingOutlined, CalendarOutlined, DeleteOutlined, CheckCircleOutlined, EditOutlined, PauseCircleOutlined, PictureOutlined, SwapOutlined, ReloadOutlined, InfoCircleOutlined, CloseOutlined } from '@ant-design/icons'; import { useState } from 'react'; import type { ReactNode } from 'react'; import type { Project } from '../types'; import { bookshelfCardStyles, bookshelfCardHoverHandlers } from '../components/CardStyles'; import { useThemeMode } from '../theme/useThemeMode'; +import { VERSION_INFO } from '../config/version'; const { Paragraph } = Typography; @@ -215,42 +216,91 @@ export default function BookshelfPage({ - - {showApiTip && projects.length === 0 && ( - - - 在开始创作之前,请先配置您的AI接口(支持 OpenAI / Anthropic)。 - - + +
+
+ 欢迎使用 {VERSION_INFO.projectName} +
+
+ 在开始创作之前,请先配置您的 AI 接口(支持 OpenAI 兼容、Gemini 等)。 +
+
+ + + + - - - - {linuxdoEnabled ? ( - <> - 第三方登录 - {renderLinuxDOLogin()} - - ) : null} - + 登录系统 + + + ); const renderEmailLogin = () => { @@ -429,13 +357,8 @@ export default function Login() { - -
+ + - @@ -470,10 +389,7 @@ export default function Login() { } placeholder="请输入新密码" /> @@ -485,9 +401,7 @@ export default function Login() { { required: true, message: '请再次输入新密码' }, ({ getFieldValue }) => ({ validator(_, value) { - if (!value || getFieldValue('new_password') === value) { - return Promise.resolve(); - } + if (!value || getFieldValue('new_password') === value) return Promise.resolve(); return Promise.reject(new Error('两次输入的新密码不一致')); }, }), @@ -525,7 +439,7 @@ export default function Login() { prefix={} placeholder="请输入已注册邮箱" autoComplete="email" - style={{ height: 46, borderRadius: 12 }} + style={{ height: 46, borderRadius: 2 }} /> @@ -543,15 +457,10 @@ export default function Login() { prefix={} placeholder="请输入 6 位登录验证码" maxLength={6} - style={{ height: 46, borderRadius: '12px 0 0 12px' }} + style={{ height: 46, borderRadius: '2px 0 0 2px' }} /> - @@ -563,15 +472,7 @@ export default function Login() { htmlType="submit" loading={loading} block - style={{ - height: 46, - fontSize: 16, - fontWeight: 600, - background: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${alphaColor(token.colorPrimary, 0.86)} 100%)`, - border: 'none', - borderRadius: '12px', - boxShadow: primaryButtonShadow, - }} + style={primaryButtonStyle} > 验证码登录 @@ -606,7 +507,7 @@ export default function Login() { prefix={} placeholder="请输入注册邮箱" autoComplete="email" - style={{ height: 46, borderRadius: 12 }} + style={{ height: 46, borderRadius: 2 }} /> @@ -624,46 +525,34 @@ export default function Login() { prefix={} placeholder="请输入 6 位验证码" maxLength={6} - style={{ height: 46, borderRadius: '12px 0 0 12px' }} + style={{ height: 46, borderRadius: '2px 0 0 2px' }} /> - - + } placeholder="选填,默认使用邮箱前缀" autoComplete="nickname" - style={{ height: 46, borderRadius: 12 }} + style={{ height: 46, borderRadius: 2 }} /> } placeholder="请输入登录密码" autoComplete="new-password" - style={{ height: 46, borderRadius: 12 }} + style={{ height: 46, borderRadius: 2 }} /> @@ -675,9 +564,7 @@ export default function Login() { { required: true, message: '请再次输入登录密码' }, ({ getFieldValue }) => ({ validator(_, value) { - if (!value || getFieldValue('password') === value) { - return Promise.resolve(); - } + if (!value || getFieldValue('password') === value) return Promise.resolve(); return Promise.reject(new Error('两次输入的密码不一致')); }, }), @@ -687,7 +574,7 @@ export default function Login() { prefix={} placeholder="请再次输入登录密码" autoComplete="new-password" - style={{ height: 46, borderRadius: 12 }} + style={{ height: 46, borderRadius: 2 }} /> @@ -697,361 +584,89 @@ export default function Login() { htmlType="submit" loading={loading} block - style={{ - height: 46, - fontSize: 16, - fontWeight: 600, - background: `linear-gradient(90deg, ${token.colorPrimary} 0%, ${alphaColor(token.colorPrimary, 0.86)} 100%)`, - border: 'none', - borderRadius: '12px', - boxShadow: primaryButtonShadow, - }} + style={primaryButtonStyle} > 注册并登录 - 验证码将发送到你填写的邮箱,若未收到请检查垃圾箱或稍后重试。注册后可通过邮箱验证码登录,也支持邮箱重置密码。 + 验证码将发送到你填写的邮箱,若未收到请检查垃圾箱或稍后重试。 ); - const renderLinuxDOLogin = () => ( -
- -
- ); - const authTabs = [ - ...(localAuthEnabled - ? [ - { - key: 'local-login', - label: '本地登录', - children: renderLocalLogin(), - }, - ] - : []), - ...(emailAuthEnabled - ? [ - { - key: 'email-login', - label: '邮箱登录', - children: renderEmailLogin(), - }, - ] - : []), - ...(emailAuthEnabled && emailRegisterEnabled - ? [ - { - key: 'email-register', - label: '邮箱注册', - children: renderEmailRegister(), - }, - ] - : []), + ...(localAuthEnabled ? [{ key: 'local-login', label: '本地登录', children: renderLocalLogin() }] : []), + ...(emailAuthEnabled ? [{ key: 'email-login', label: '邮箱登录', children: renderEmailLogin() }] : []), + ...(emailAuthEnabled && emailRegisterEnabled ? [{ key: 'email-register', label: '邮箱注册', children: renderEmailRegister() }] : []), ]; if (checking) { return ( -
+
); } + const pageTexture = `repeating-linear-gradient(-12deg, transparent, transparent 31px, ${alphaColor(token.colorText, 0.035)} 31px, ${alphaColor(token.colorText, 0.035)} 32px)`; + return ( - <> - - -
- -
- - -
-
- -
- -
- 墨木灵思 -
- - 墨木灵思 - -
- - -
- - 基于 AI 的 - <br /> - <span - style={{ - backgroundImage: `linear-gradient(90deg, ${token.colorPrimary} 0%, #d946ef 100%)`, - WebkitBackgroundClip: 'text', - backgroundClip: 'text', - WebkitTextFillColor: 'transparent', - color: token.colorPrimary, - }} - > - 智能小说创作助手 - </span> - - - 从灵感到成稿,围绕「多模型协同、创作流程自动化、角色关系管理、章节精修」构建一体化创作工作台。 - -
- - - {featureItems.map((item) => ( - - - - - {item.icon} - {item.title} - - - {item.description} - - - - - ))} - -
- - - OpenAI - Gemini - Claude - LinuxDO OAuth - Docker Compose - PostgreSQL - -
- - - © 2026 墨木灵思 · GPLv3 License - -
- - + +
+ +
+
+
+ +
+ 墨木灵思 +
+ 墨木灵思 +
+ 执笔于 AI,落墨成章 + 多模型协同 · 世界观构建 · 角色关系 · 章节精修 +
+ -
-
- - - 欢迎回来 - - - 登录 墨木灵思,继续你的小说创作项目。 - - - -
- {authTabs.length > 0 ? ( - - ) : null} - - {!localAuthEnabled && !linuxdoEnabled && !emailAuthEnabled ? ( - - ) : null} - - {emailAuthEnabled && !emailRegisterEnabled ? ( - - ) : null} - - - } - style={{ background: alphaColor(token.colorPrimary, 0.06), borderRadius: 12 }} - message="登录说明" - description={( -
    - {loginTips.map((tip) => ( -
  • - {tip} -
  • - ))} -
- )} - /> +
+ {featureItems.map((item, index) => ( +
+ + {item.icon} +
+ {item.title} + {item.description} +
+
+ ))} + + {['OpenAI', 'Gemini', 'Claude', 'Docker', 'PostgreSQL'].map((label) => ( + {label} + ))} + +
+ + + + + 进入创作台 + 登录后继续你的小说项目 + +
+ {authTabs.length > 0 ? : null} + {authTabs.length === 0 ? : null} + {emailAuthEnabled && !emailRegisterEnabled ? : null} + + } style={{ background: token.colorFillTertiary, border: `1px solid ${token.colorBorder}`, borderRadius: 2 }} message="登录说明" description={
    {loginTips.map((tip) =>
  • {tip}
  • )}
} />
-
+
- - + © 2026 墨木灵思 · GPLv3 +
+
); } diff --git a/frontend/src/pages/ProjectDetail.tsx b/frontend/src/pages/ProjectDetail.tsx index c551635..0b5b8e4 100644 --- a/frontend/src/pages/ProjectDetail.tsx +++ b/frontend/src/pages/ProjectDetail.tsx @@ -23,6 +23,7 @@ import { import { useStore } from '../store'; import { useCharacterSync, useOutlineSync, useChapterSync } from '../store/hooks'; import { projectApi } from '../services/api'; +import { VERSION_INFO } from '../config/version'; import ThemeSwitch from '../components/ThemeSwitch'; import { useThemeMode } from '../theme/useThemeMode'; import { getStoredSidebarCollapsed, setStoredSidebarCollapsed } from '../utils/sidebarState'; @@ -198,7 +199,13 @@ export default function ProjectDetail() { { key: 'source-code', icon: , - label: 源码库, + label: VERSION_INFO.officialApiDocUrl ? ( + + API 文档 + + ) : ( + API 文档 + ), }, ], }, diff --git a/frontend/src/pages/ProjectList.tsx b/frontend/src/pages/ProjectList.tsx index 774cef6..4accb13 100644 --- a/frontend/src/pages/ProjectList.tsx +++ b/frontend/src/pages/ProjectList.tsx @@ -9,7 +9,6 @@ import { eventBus, EventNames } from '../store/eventBus'; import type { ReactNode } from 'react'; import type { Project, User } from '../types'; import UserMenu from '../components/UserMenu'; -import ChangelogFloatingButton from '../components/ChangelogFloatingButton'; import ThemeSwitch from '../components/ThemeSwitch'; import { useThemeMode } from '../theme/useThemeMode'; import SettingsPage from './Settings'; @@ -19,6 +18,8 @@ import PromptTemplates from './PromptTemplates'; import BookImport from './BookImport'; import BookshelfPage from './BookshelfPage'; import { getStoredSidebarCollapsed, setStoredSidebarCollapsed } from '../utils/sidebarState'; +import { VERSION_INFO } from '../config/version'; +import { shellColors } from '../theme/themeConfig'; const { Text } = Typography; @@ -386,10 +387,11 @@ export default function ProjectList() { }; const isMobile = window.innerWidth <= 768; - const headerHeight = isMobile ? 56 : 70; - const expandedSiderWidth = 220; - const collapsedSiderWidth = 60; + const headerHeight = isMobile ? 56 : 64; + const expandedSiderWidth = 232; + const collapsedSiderWidth = 64; const desktopSiderWidth = collapsed ? collapsedSiderWidth : expandedSiderWidth; + const shell = shellColors[resolvedMode]; const currentViewTitle = activeView === 'projects' ? '我的书架' @@ -447,9 +449,9 @@ export default function ProjectList() { label: '系统设置', }] : []), { - key: 'mumu-api', + key: 'xinmi-api', icon: , - label: 'MuMuのAPI', + label: 'XinMi API', }, ], }, @@ -487,12 +489,16 @@ export default function ProjectList() { label: '系统设置', }] : []), { - key: 'mumu-api', + key: 'xinmi-api', icon: , - label: 'MuMuのAPI', + label: 'XinMi API', }, ]; + const openXinmiApi = () => { + window.open(VERSION_INFO.officialApiDocUrl, '_blank', 'noopener,noreferrer'); + }; + return (
} onClick={() => setCollapsed(false)} style={{ - color: token.colorWhite, + color: shell.siderText, width: '100%', height: '100%', padding: 0, @@ -549,39 +557,53 @@ export default function ProjectList() { /> ) : ( <> -
+
- - 墨木灵思 - +
+ + {VERSION_INFO.projectName} + + + 铜墨编辑部 + +
- - @@ -343,7 +306,6 @@ export default function PromptTemplates() { - {/* 使用提示 */} @@ -354,20 +316,20 @@ export default function PromptTemplates() { description={
- • 系统默认模板(灰色头部):始终启用,无需手动开关。点击"编辑"后将创建您的自定义副本。 + • 系统默认模板(灰底标题):始终启用,无需手动开关。点击「编辑」后将创建您的自定义副本。 - • 已自定义模板(紫色头部):可通过开关控制启用/禁用,使用 {'{variable_name}'} 格式表示变量占位符。点击"重置"可恢复为系统默认。 + • 已自定义模板(赭石标题):可通过开关控制启用/禁用,使用 {'{variable_name}'} 格式表示变量占位符。点击「重置」可恢复为系统默认。
} type="info" showIcon={false} style={{ - marginTop: isMobile ? 16 : 24, - borderRadius: 12, - background: token.colorInfoBg, - border: `1px solid ${token.colorInfoBorder}` + marginTop: isMobile ? 16 : 20, + borderRadius: 2, + background: token.colorFillTertiary, + border: `1px solid ${token.colorBorder}`, }} /> @@ -381,8 +343,9 @@ export default function PromptTemplates() { variant="borderless" style={{ background: token.colorBgContainer, - borderRadius: isMobile ? 12 : 16, - boxShadow: token.boxShadowSecondary, + borderRadius: 2, + border: `1px solid ${token.colorBorder}`, + boxShadow: 'none', marginBottom: isMobile ? 16 : 24 }} styles={{ body: { padding: isMobile ? '12px' : '16px' } }} @@ -407,8 +370,9 @@ export default function PromptTemplates() { variant="borderless" style={{ background: token.colorBgContainer, - borderRadius: isMobile ? 12 : 16, - boxShadow: token.boxShadowSecondary, + borderRadius: 2, + border: `1px solid ${token.colorBorder}`, + boxShadow: 'none', }} > } onClick={() => handleEdit(template)} size={isMobile ? 'small' : 'middle'} - style={{ borderRadius: 6 }} + style={{ borderRadius: 2 }} > 编辑 @@ -498,7 +462,7 @@ export default function PromptTemplates() { icon={} onClick={() => handleReset(template.template_key)} size={isMobile ? 'small' : 'middle'} - style={{ borderRadius: 6 }} + style={{ borderRadius: 2 }} > 重置 diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 5326015..569cb68 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -4,6 +4,7 @@ import { SaveOutlined, DeleteOutlined, ReloadOutlined, InfoCircleOutlined, Check import { settingsApi, mcpPluginApi } from '../services/api'; import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types'; import { eventBus, EventNames } from '../store/eventBus'; +import { VERSION_INFO } from '../config/version'; const { Title, Text } = Typography; const { Option } = Select; @@ -284,25 +285,29 @@ export default function SettingsPage() { }); }; - const mumuTextDefaultUrl = 'https://api.mumuverse.space/v1'; - const mumuRegisterUrl = 'https://api.mumuverse.space/register?aff=4NN8'; - const mumuCoverBaseUrlOptions = [ - { value: 'https://api.mumuverse.space/v1beta', label: 'https://api.mumuverse.space/v1beta', defaultModel: 'gemini-3.1-flash-image-preview' }, - { value: 'https://api.mumuverse.space/v1', label: 'https://api.mumuverse.space/v1', defaultModel: 'gpt-image-1.5' }, + const xinmiApiHost = VERSION_INFO.xinmiApiBaseUrl.replace(/\/$/, ''); + const xinmiTextDefaultUrl = `${xinmiApiHost}/v1`; + const officialApiDocUrl = VERSION_INFO.officialApiDocUrl; + const openOfficialApiDoc = () => { + window.open(officialApiDocUrl, '_blank', 'noopener,noreferrer'); + }; + const xinmiCoverBaseUrlOptions = [ + { value: `${xinmiApiHost}/v1beta`, label: 'v1beta', defaultModel: 'gemini-3.1-flash-image-preview' }, + { value: `${xinmiApiHost}/v1`, label: 'v1', defaultModel: 'gpt-image-1.5' }, ]; const defaultCoverSettings = { cover_enabled: false, - cover_api_provider: 'mumu', + cover_api_provider: 'xinmi', cover_api_key: '', - cover_api_base_url: mumuCoverBaseUrlOptions[0].value, - cover_image_model: mumuCoverBaseUrlOptions[0].defaultModel, + cover_api_base_url: xinmiCoverBaseUrlOptions[0].value, + cover_image_model: xinmiCoverBaseUrlOptions[0].defaultModel, }; const apiProviders = [ { - value: 'mumu', - label: 'MuMuのAPI', - defaultUrl: mumuTextDefaultUrl, + value: 'xinmi', + label: 'XinMi API', + defaultUrl: xinmiTextDefaultUrl, defaultModel: 'gemini-3-flash-preview' }, { value: 'openai', label: 'OpenAI Compatible', defaultUrl: 'https://api.openai.com/v1' }, @@ -321,7 +326,7 @@ export default function SettingsPage() { if (provider.defaultUrl) { nextValues.api_base_url = provider.defaultUrl; } - if (provider.value === 'mumu') { + if (provider.value === 'xinmi') { nextValues.api_key = ''; nextValues.llm_model = provider.defaultModel || 'gemini-3-flash-preview'; } @@ -334,10 +339,10 @@ export default function SettingsPage() { const coverApiProviders = [ { - value: 'mumu', - label: 'MuMuのAPI', - defaultUrl: mumuCoverBaseUrlOptions[0].value, - defaultModel: mumuCoverBaseUrlOptions[0].defaultModel, + value: 'xinmi', + label: 'XinMi API', + defaultUrl: xinmiCoverBaseUrlOptions[0].value, + defaultModel: xinmiCoverBaseUrlOptions[0].defaultModel, }, { value: 'gemini', label: 'Google Gemini', defaultUrl: 'https://generativelanguage.googleapis.com/v1beta' }, { value: 'grok', label: 'Grok', defaultUrl: 'https://api.x.ai/v1' }, @@ -354,20 +359,20 @@ export default function SettingsPage() { if (provider.defaultUrl) { nextValues.cover_api_base_url = provider.defaultUrl; } - if (provider.value === 'mumu') { + if (provider.value === 'xinmi') { nextValues.cover_api_key = ''; - nextValues.cover_image_model = provider.defaultModel || mumuCoverBaseUrlOptions[0].defaultModel; + nextValues.cover_image_model = provider.defaultModel || xinmiCoverBaseUrlOptions[0].defaultModel; } form.setFieldsValue(nextValues); setCoverTestResult(null); }; - const handleMumuCoverBaseUrlChange = (value: string) => { - const option = mumuCoverBaseUrlOptions.find(item => item.value === value); + const handleXinmiCoverBaseUrlChange = (value: string) => { + const option = xinmiCoverBaseUrlOptions.find(item => item.value === value); form.setFieldsValue({ cover_api_base_url: value, - cover_image_model: option?.defaultModel || mumuCoverBaseUrlOptions[0].defaultModel, + cover_image_model: option?.defaultModel || xinmiCoverBaseUrlOptions[0].defaultModel, }); setCoverTestResult(null); }; @@ -611,7 +616,7 @@ export default function SettingsPage() { if (provider.defaultUrl) { nextValues.api_base_url = provider.defaultUrl; } - if (provider.value === 'mumu') { + if (provider.value === 'xinmi') { nextValues.api_key = ''; nextValues.llm_model = provider.defaultModel || 'gemini-3-flash-preview'; } @@ -923,7 +928,7 @@ export default function SettingsPage() { // return 'purple'; case 'gemini': return 'green'; - case 'mumu': + case 'xinmi': return 'magenta'; default: return 'default'; @@ -1228,11 +1233,11 @@ export default function SettingsPage() { - {selectedProvider === 'mumu' && ( + {selectedProvider === 'xinmi' && ( @@ -1241,9 +1246,9 @@ export default function SettingsPage() {
@@ -1740,22 +1745,22 @@ export default function SettingsPage() { - {selectedCoverProvider === 'mumu' && ( + {selectedCoverProvider === 'xinmi' && ( - 已固定提供 MuMuのAPI 图片接口地址选项,切换地址时会自动带出推荐模型。API Key 需前往 MuMuのAPI 站点注册获取。 + 已固定提供墨木灵思 API 图片接口地址选项,切换地址时会自动带出推荐模型。API Key 请按文档在控制台获取。
@@ -1765,15 +1770,15 @@ export default function SettingsPage() { )} - + - {selectedCoverProvider === 'mumu' ? ( + {selectedCoverProvider === 'xinmi' ? ( - {selectedPresetProvider === 'mumu' && ( + {selectedPresetProvider === 'xinmi' && ( @@ -1897,9 +1902,9 @@ export default function SettingsPage() {
diff --git a/frontend/src/services/changelogService.ts b/frontend/src/services/changelogService.ts deleted file mode 100644 index c6cede4..0000000 --- a/frontend/src/services/changelogService.ts +++ /dev/null @@ -1,276 +0,0 @@ -/** - * GitHub 提交日志获取服务 - * 用于从 GitHub API 获取项目的提交历史并转换为更新日志 - */ - -export interface GitHubCommit { - sha: string; - commit: { - author: { - name: string; - email: string; - date: string; - }; - message: string; - }; - html_url: string; - author: { - login: string; - avatar_url: string; - } | null; -} - -export interface ChangelogEntry { - id: string; - date: string; - version?: string; - author: { - name: string; - avatar?: string; - username?: string; - }; - message: string; - commitUrl: string; - type: 'feature' | 'fix' | 'docs' | 'style' | 'refactor' | 'perf' | 'test' | 'chore' | 'update' | 'other'; - scope?: string; -} - -const GITHUB_API_BASE = 'https://api.github.com'; -const REPO_OWNER = 'xiamuceer-j'; -const REPO_NAME = '墨木灵思'; - -/** - * 提交类型映射表 - * 统一不同别名到标准类型 - */ -const TYPE_MAPPING: Record = { - // 功能类 - 'feat': 'feature', - 'feature': 'feature', - 'update': 'update', - - // 修复类 - 'fix': 'fix', - - // 文档类 - 'docs': 'docs', - 'doc': 'docs', - - // 样式类 - 'style': 'style', - - // 重构类 - 'refactor': 'refactor', - - // 性能类 - 'perf': 'perf', - - // 测试类 - 'test': 'test', - - // 杂项类 - 'chore': 'chore', -}; - -/** - * 从提交信息中解析类型和作用域 - * - * 匹配优先级(从高到低): - * 1. 标准 Conventional Commits 格式: type(scope): message 或 type: message - * 2. 方括号格式: [type] message - * 3. 简单前缀格式: type: message(支持中文冒号) - * 4. 关键词模糊匹配(中英文) - */ -function parseCommitType(message: string): { type: ChangelogEntry['type']; scope?: string; cleanMessage: string } { - const lowerMessage = message.toLowerCase().trim(); - - // 优先级1:标准 Conventional Commits 格式 - type(scope): message 或 type: message - // 匹配所有支持的类型 - const conventionalPattern = new RegExp( - `^(${Object.keys(TYPE_MAPPING).join('|')})(?:\\(([^)]+)\\))?\\s*[:\\::]\\s*(.+)`, - 'i' - ); - const conventionalMatch = message.match(conventionalPattern); - if (conventionalMatch) { - const typeStr = conventionalMatch[1].toLowerCase(); - const mappedType = TYPE_MAPPING[typeStr] || 'other'; - return { - type: mappedType, - scope: conventionalMatch[2], - cleanMessage: conventionalMatch[3].trim(), - }; - } - - // 优先级2:方括号格式 - [type] message - const bracketPattern = new RegExp( - `^\\[(${Object.keys(TYPE_MAPPING).join('|')})\\]\\s*(.+)`, - 'i' - ); - const bracketMatch = message.match(bracketPattern); - if (bracketMatch) { - const typeStr = bracketMatch[1].toLowerCase(); - const mappedType = TYPE_MAPPING[typeStr] || 'other'; - return { - type: mappedType, - cleanMessage: bracketMatch[2].trim(), - }; - } - - // 优先级3:简单前缀格式 - type: message(支持英文和中文冒号) - for (const [key, value] of Object.entries(TYPE_MAPPING)) { - const prefixPattern = new RegExp(`^${key}\\s*[:\\::]\\s*`, 'i'); - if (prefixPattern.test(lowerMessage)) { - const cleanMsg = message.replace(prefixPattern, '').trim(); - return { type: value, cleanMessage: cleanMsg }; - } - } - - // 优先级4:关键词模糊匹配(仅当前面都不匹配时) - const keywordMap: Array<{ keywords: string[]; type: ChangelogEntry['type'] }> = [ - { keywords: ['修复', 'fix'], type: 'fix' }, - { keywords: ['优化', 'perf'], type: 'perf' }, - { keywords: ['文档', 'document'], type: 'docs' }, - { keywords: ['新增', '添加', '增加', 'add'], type: 'feature' }, - { keywords: ['更新', 'update'], type: 'update' }, - { keywords: ['样式', 'style'], type: 'style' }, - { keywords: ['重构', 'refactor'], type: 'refactor' }, - { keywords: ['测试', 'test'], type: 'test' }, - ]; - - for (const { keywords, type } of keywordMap) { - if (keywords.some(keyword => lowerMessage.includes(keyword))) { - return { type, cleanMessage: message }; - } - } - - // 默认类型 - return { type: 'other', cleanMessage: message }; -} - -/** - * 获取GitHub提交历史 - */ -export async function fetchGitHubCommits(page: number = 1, perPage: number = 30): Promise { - try { - const url = `${GITHUB_API_BASE}/repos/${REPO_OWNER}/${REPO_NAME}/commits?author=${REPO_OWNER}&page=${page}&per_page=${perPage}`; - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Accept': 'application/vnd.github.v3+json', - }, - cache: 'no-cache', - }); - - if (!response.ok) { - throw new Error(`GitHub API 请求失败: ${response.status} ${response.statusText}`); - } - - return await response.json(); - } catch (error) { - console.error('获取 GitHub 提交历史失败:', error); - throw error; - } -} - -/** - * 将GitHub提交转换为更新日志条目 - */ -export function convertCommitsToChangelog(commits: GitHubCommit[]): ChangelogEntry[] { - return commits.map(commit => { - const { type, scope, cleanMessage } = parseCommitType(commit.commit.message); - - return { - id: commit.sha, - date: commit.commit.author.date, - author: { - name: commit.commit.author.name, - avatar: commit.author?.avatar_url, - username: commit.author?.login, - }, - message: cleanMessage, - commitUrl: commit.html_url, - type, - scope, - }; - }); -} - -/** - * 获取格式化的更新日志 - */ -export async function fetchChangelog(page: number = 1, perPage: number = 30): Promise { - const commits = await fetchGitHubCommits(page, perPage); - return convertCommitsToChangelog(commits); -} - -/** - * 按日期分组更新日志 - */ -export function groupChangelogByDate(entries: ChangelogEntry[]): Map { - const grouped = new Map(); - - entries.forEach(entry => { - const date = new Date(entry.date).toISOString().split('T')[0]; - const existing = grouped.get(date) || []; - existing.push(entry); - grouped.set(date, existing); - }); - - return grouped; -} - -/** - * 检查是否应该获取更新日志(避免频繁请求) - */ -export function shouldFetchChangelog(): boolean { - const lastFetch = localStorage.getItem('changelog_last_fetch'); - - if (!lastFetch) { - return true; - } - - const lastFetchTime = new Date(lastFetch).getTime(); - const now = Date.now(); - const oneHourMs = 60 * 60 * 1000; // 1小时 - - return now - lastFetchTime >= oneHourMs; -} - -/** - * 记录更新日志获取时间 - */ -export function markChangelogFetched(): void { - localStorage.setItem('changelog_last_fetch', new Date().toISOString()); -} - -/** - * 获取缓存的更新日志 - */ -export function getCachedChangelog(): ChangelogEntry[] | null { - const cached = localStorage.getItem('changelog_cache'); - if (cached) { - try { - return JSON.parse(cached); - } catch { - return null; - } - } - return null; -} - -/** - * 缓存更新日志 - */ -export function cacheChangelog(entries: ChangelogEntry[]): void { - localStorage.setItem('changelog_cache', JSON.stringify(entries)); -} - -/** - * 清除更新日志缓存 - * 用于强制刷新数据 - */ -export function clearChangelogCache(): void { - localStorage.removeItem('changelog_cache'); - localStorage.removeItem('changelog_last_fetch'); -} \ No newline at end of file diff --git a/frontend/src/services/versionService.ts b/frontend/src/services/versionService.ts index 4eb94dc..06705bb 100644 --- a/frontend/src/services/versionService.ts +++ b/frontend/src/services/versionService.ts @@ -32,7 +32,7 @@ function compareVersion(v1: string, v2: string): number { export async function checkLatestVersion(): Promise { try { // 使用 shields.io 的 GitHub release badge API - const badgeUrl = 'https://img.shields.io/github/v/release/xiamuceer-j/墨木灵思'; + const badgeUrl = 'https://img.shields.io/github/v/release/mumulingsi-project/mumulingsi'; const response = await fetch(badgeUrl, { method: 'GET', @@ -63,7 +63,7 @@ export async function checkLatestVersion(): Promise { return { hasUpdate, latestVersion, - releaseUrl: `https://github.com/xiamuceer-j/墨木灵思/releases/tag/v${latestVersion}`, + releaseUrl: '', }; } } @@ -74,7 +74,7 @@ export async function checkLatestVersion(): Promise { return { hasUpdate: false, latestVersion: VERSION_INFO.version, - releaseUrl: VERSION_INFO.githubUrl, + releaseUrl: '', }; } } diff --git a/frontend/src/theme/themeConfig.ts b/frontend/src/theme/themeConfig.ts index 09aa7ec..92a9ea2 100644 --- a/frontend/src/theme/themeConfig.ts +++ b/frontend/src/theme/themeConfig.ts @@ -4,23 +4,48 @@ import type { ThemeMode } from './themeStorage'; export type ResolvedThemeMode = Exclude; +/** 铜墨编辑部 — 暖赭石 + 纸感底色,与原先蓝紫圆角风区分 */ const sharedToken: ThemeConfig['token'] = { - colorPrimary: '#4D8088', - borderRadius: 8, + colorPrimary: '#B45309', + colorInfo: '#0D9488', + colorSuccess: '#15803D', + colorWarning: '#CA8A04', + colorError: '#B91C1C', + borderRadius: 2, wireframe: false, - fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif", + fontFamily: "'Source Sans 3', 'PingFang SC', 'Microsoft YaHei', sans-serif", + fontFamilyCode: "'IBM Plex Mono', 'Consolas', monospace", }; const sharedComponents: ThemeConfig['components'] = { Button: { - borderRadius: 8, - controlHeight: 36, + borderRadius: 2, + controlHeight: 40, + fontWeight: 600, + primaryShadow: 'none', }, Card: { - borderRadiusLG: 12, + borderRadiusLG: 2, + boxShadowTertiary: 'none', + }, + Menu: { + itemBorderRadius: 0, + itemHeight: 44, + iconSize: 16, + }, + Tabs: { + inkBarColor: '#B45309', + titleFontSize: 14, + }, + Input: { + borderRadius: 2, + controlHeight: 42, + }, + Layout: { + headerHeight: 64, }, Tooltip: { - colorBgSpotlight: sharedToken.colorPrimary, + colorBgSpotlight: '#292524', }, }; @@ -28,17 +53,34 @@ const lightThemeConfig: ThemeConfig = { algorithm: theme.defaultAlgorithm, token: { ...sharedToken, - colorBgBase: '#F8F6F1', - colorTextBase: '#2B2B2B', - colorBgLayout: '#F8F6F1', - colorBgContainer: '#FFFFFF', + colorBgBase: '#F5F0E6', + colorTextBase: '#292524', + colorBgLayout: '#EDE8DC', + colorBgContainer: '#FFFCF7', + colorBorder: '#C9BFB0', + colorBorderSecondary: '#DDD5C8', + colorFillSecondary: '#E8E2D6', + colorFillTertiary: '#F0EBE1', + colorPrimaryBg: '#FEF3C7', + colorPrimaryBorder: '#D97706', + colorPrimaryHover: '#92400E', + colorLink: '#9A3412', + colorLinkHover: '#7C2D12', }, components: { ...sharedComponents, Layout: { - bodyBg: '#F8F6F1', - headerBg: '#FFFFFF', - siderBg: '#FFFFFF', + bodyBg: '#EDE8DC', + headerBg: '#FFFCF7', + siderBg: '#1C1917', + }, + Menu: { + ...sharedComponents.Menu, + darkItemBg: 'transparent', + darkItemColor: '#D6D3D1', + darkItemSelectedBg: 'rgba(180, 83, 9, 0.22)', + darkItemSelectedColor: '#FCD34D', + darkItemHoverBg: 'rgba(255, 255, 255, 0.06)', }, }, }; @@ -47,15 +89,34 @@ const darkThemeConfig: ThemeConfig = { algorithm: theme.darkAlgorithm, token: { ...sharedToken, - colorBgBase: '#141414', - colorTextBase: '#f5f5f5', + colorPrimary: '#F59E0B', + colorBgBase: '#0C0A09', + colorTextBase: '#E7E5E4', + colorBgLayout: '#0C0A09', + colorBgContainer: '#1C1917', + colorBorder: '#44403C', + colorBorderSecondary: '#292524', + colorFillSecondary: '#292524', + colorFillTertiary: '#1C1917', + colorPrimaryBg: 'rgba(245, 158, 11, 0.12)', + colorPrimaryBorder: '#B45309', + colorLink: '#FBBF24', + colorLinkHover: '#FCD34D', }, components: { ...sharedComponents, Layout: { - bodyBg: '#0f1115', - headerBg: '#141414', - siderBg: '#141414', + bodyBg: '#0C0A09', + headerBg: '#1C1917', + siderBg: '#0C0A09', + }, + Menu: { + ...sharedComponents.Menu, + darkItemBg: 'transparent', + darkItemColor: '#A8A29E', + darkItemSelectedBg: 'rgba(245, 158, 11, 0.18)', + darkItemSelectedColor: '#FCD34D', + darkItemHoverBg: 'rgba(255, 255, 255, 0.05)', }, }, }; @@ -63,3 +124,25 @@ const darkThemeConfig: ThemeConfig = { export const getThemeConfig = (mode: ResolvedThemeMode): ThemeConfig => { return mode === 'dark' ? darkThemeConfig : lightThemeConfig; }; + +/** 侧栏等壳层用色(不依赖 ant token 的固定值) */ +export const shellColors = { + light: { + siderBg: '#1C1917', + siderBorder: '#44403C', + siderAccent: '#F59E0B', + siderText: '#E7E5E4', + siderMuted: '#A8A29E', + headerBorder: '#C9BFB0', + inkPattern: 'rgba(28, 25, 23, 0.04)', + }, + dark: { + siderBg: '#0C0A09', + siderBorder: '#292524', + siderAccent: '#FBBF24', + siderText: '#E7E5E4', + siderMuted: '#78716C', + headerBorder: '#44403C', + inkPattern: 'rgba(251, 191, 36, 0.06)', + }, +} as const; diff --git a/frontend/src/theme/themeStorage.ts b/frontend/src/theme/themeStorage.ts index b109b74..98f1565 100644 --- a/frontend/src/theme/themeStorage.ts +++ b/frontend/src/theme/themeStorage.ts @@ -1,6 +1,7 @@ export type ThemeMode = 'light' | 'dark' | 'system'; -const THEME_MODE_STORAGE_KEY = 'mumu_theme_mode'; +const THEME_MODE_STORAGE_KEY = 'mumulingsi_theme_mode'; +const LEGACY_THEME_MODE_STORAGE_KEY = 'xinmi_theme_mode'; const isThemeMode = (value: string | null): value is ThemeMode => { return value === 'light' || value === 'dark' || value === 'system'; @@ -8,7 +9,18 @@ const isThemeMode = (value: string | null): value is ThemeMode => { export const getStoredThemeMode = (): ThemeMode => { try { - const value = localStorage.getItem(THEME_MODE_STORAGE_KEY); + let value = localStorage.getItem(THEME_MODE_STORAGE_KEY); + if (!value) { + value = localStorage.getItem(LEGACY_THEME_MODE_STORAGE_KEY); + if (isThemeMode(value)) { + localStorage.setItem(THEME_MODE_STORAGE_KEY, value); + try { + localStorage.removeItem(LEGACY_THEME_MODE_STORAGE_KEY); + } catch { + // ignore + } + } + } if (isThemeMode(value)) { return value; } diff --git a/frontend/src/utils/sidebarState.ts b/frontend/src/utils/sidebarState.ts index b5dcc46..895bb4e 100644 --- a/frontend/src/utils/sidebarState.ts +++ b/frontend/src/utils/sidebarState.ts @@ -1,4 +1,5 @@ -const SIDEBAR_COLLAPSED_STORAGE_KEY = 'mumu_sidebar_collapsed'; +const SIDEBAR_COLLAPSED_STORAGE_KEY = 'mumulingsi_sidebar_collapsed'; +const LEGACY_SIDEBAR_COLLAPSED_STORAGE_KEY = 'xinmi_sidebar_collapsed'; export const getStoredSidebarCollapsed = (): boolean => { if (typeof window === 'undefined') { @@ -6,7 +7,21 @@ export const getStoredSidebarCollapsed = (): boolean => { } try { - return localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY) === '1'; + const v = localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY); + if (v === '1' || v === '0') { + return v === '1'; + } + const legacy = localStorage.getItem(LEGACY_SIDEBAR_COLLAPSED_STORAGE_KEY); + if (legacy === '1' || legacy === '0') { + localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, legacy); + try { + localStorage.removeItem(LEGACY_SIDEBAR_COLLAPSED_STORAGE_KEY); + } catch { + // ignore + } + return legacy === '1'; + } + return false; } catch (error) { console.warn('读取侧边栏状态失败:', error); return false; diff --git a/images/image-20260518142402652.png b/images/image-20260518142402652.png new file mode 100644 index 0000000..8fd933f Binary files /dev/null and b/images/image-20260518142402652.png differ diff --git a/images/image-20260518142411086.png b/images/image-20260518142411086.png new file mode 100644 index 0000000..b64b5eb Binary files /dev/null and b/images/image-20260518142411086.png differ diff --git a/images/image-20260518142620791.png b/images/image-20260518142620791.png new file mode 100644 index 0000000..cead91e Binary files /dev/null and b/images/image-20260518142620791.png differ diff --git a/images/image-20260518142641345.png b/images/image-20260518142641345.png new file mode 100644 index 0000000..465355c Binary files /dev/null and b/images/image-20260518142641345.png differ diff --git a/images/image-20260518143108652.png b/images/image-20260518143108652.png new file mode 100644 index 0000000..91879f8 Binary files /dev/null and b/images/image-20260518143108652.png differ diff --git a/images/image-20260518143113951.png b/images/image-20260518143113951.png new file mode 100644 index 0000000..b1253cb Binary files /dev/null and b/images/image-20260518143113951.png differ diff --git a/images/image-20260518143117651.png b/images/image-20260518143117651.png new file mode 100644 index 0000000..fa66a2c Binary files /dev/null and b/images/image-20260518143117651.png differ diff --git a/images/image-20260518143121175.png b/images/image-20260518143121175.png new file mode 100644 index 0000000..b247d0c Binary files /dev/null and b/images/image-20260518143121175.png differ diff --git a/images/image-20260518143125197.png b/images/image-20260518143125197.png new file mode 100644 index 0000000..412d3ab Binary files /dev/null and b/images/image-20260518143125197.png differ diff --git a/images/image-20260518143134898.png b/images/image-20260518143134898.png new file mode 100644 index 0000000..ce63588 Binary files /dev/null and b/images/image-20260518143134898.png differ diff --git a/install-termux.sh b/install-termux.sh index bb4aad2..8053f54 100644 --- a/install-termux.sh +++ b/install-termux.sh @@ -7,10 +7,10 @@ set -e # ── 路径配置 ────────────────────────────────────────────────────────────────── -INSTALL_DIR="$HOME/墨木灵思" # 项目安装目录 -DATA_DIR="$HOME/mumuainovel/data" # 数据库目录 -LOG_DIR="$HOME/mumuainovel/logs" # 日志目录 -REPO="https://ghfast.top/https://github.com/xiamuceer-j/墨木灵思.git" # GitHub 镜像 +INSTALL_DIR="$HOME/mumulingsi" # 项目安装目录 +DATA_DIR="$HOME/mumulingsi/data" # 数据库目录 +LOG_DIR="$HOME/mumulingsi/logs" # 日志目录 +REPO="https://ghfast.top/https://github.com/mumulingsi-project/mumulingsi.git" # GitHub 镜像 # ── 输出函数 ────────────────────────────────────────────────────────────────── GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; NC='\033[0m' @@ -118,7 +118,7 @@ LOG="$TMPDIR/patch.log" # ── 4a. 修补 memory_service.py ────────────────────────────────────────────── python3 << 'PYEOF' import os -f = os.path.expanduser("~/墨木灵思/backend/app/services/memory_service.py") +f = os.path.expanduser("~/mumulingsi/backend/app/services/memory_service.py") with open(f) as fh: c = fh.read() @@ -150,11 +150,11 @@ python3 << 'PYEOF' import os home = os.path.expanduser("~") files = [ - f"{home}/墨木灵思/backend/app/api/chapters.py", - f"{home}/墨木灵思/backend/app/api/memories.py", - f"{home}/墨木灵思/backend/app/api/outlines.py", - f"{home}/墨木灵思/backend/app/api/projects.py", - f"{home}/墨木灵思/backend/app/services/foreshadow_service.py", + f"{home}/mumulingsi/backend/app/api/chapters.py", + f"{home}/mumulingsi/backend/app/api/memories.py", + f"{home}/mumulingsi/backend/app/api/outlines.py", + f"{home}/mumulingsi/backend/app/api/projects.py", + f"{home}/mumulingsi/backend/app/services/foreshadow_service.py", ] old = 'from app.services.memory_service import memory_service' new = 'try:\n from app.services.memory_service import memory_service\nexcept ImportError:\n memory_service = None' @@ -181,17 +181,17 @@ DEBUG=false TZ=Asia/Shanghai # SQLite 数据库(替代 PostgreSQL) -DATABASE_URL=sqlite+aiosqlite:///data/data/com.termux/files/home/mumuainovel/data/ai_story.db +DATABASE_URL=sqlite+aiosqlite:///data/data/com.termux/files/home/mumulingsi/data/ai_story.db # 日志 LOG_LEVEL=INFO LOG_TO_FILE=true -LOG_FILE_PATH=/data/data/com.termux/files/home/mumuainovel/logs/app.log +LOG_FILE_PATH=/data/data/com.termux/files/home/mumulingsi/logs/app.log LOG_MAX_BYTES=10485760 LOG_BACKUP_COUNT=5 # CORS -CORS_ORIGINS=["http://localhost:8000","http://127.0.0.1:8000"] +CORS_ORIGINS=["http://localhost:3000","http://127.0.0.1:8000"] # ⚠️ 请填入你的 API Key OPENAI_API_KEY=*** @@ -295,10 +295,10 @@ grep -E "built in" "$LOG" | sed 's/^/ /' # ============================================================================= # 步骤 9: 创建启动脚本 -# 说明: 生成 ~/mumuainovel-start.sh,支持前台/后台运行 +# 说明: 生成 ~/mumulingsi-start.sh,支持前台/后台运行 # ============================================================================= step 9 $TOTAL "创建启动脚本" -cat > "$HOME/mumuainovel-start.sh" << STARTEOF +cat > "$HOME/mumulingsi-start.sh" << STARTEOF #!/bin/bash # 墨木灵思 Termux 启动脚本 set -e @@ -316,10 +316,10 @@ if [ "\$1" = "--bg" ]; then echo "🚀 后台启动 墨木灵思 (端口 8000)..." nohup "\$PYTHON" -m uvicorn app.main:app --host 0.0.0.0 --port 8000 \\ > "\$LOG_DIR/app.log" 2>&1 & - echo \$! > "$HOME/mumuainovel.pid" + echo \$! > "$HOME/mumulingsi.pid" sleep 2 - if kill -0 \$(cat "$HOME/mumuainovel.pid") 2>/dev/null; then - echo "✅ 已启动, PID: \$(cat $HOME/mumuainovel.pid)" + if kill -0 \$(cat "$HOME/mumulingsi.pid") 2>/dev/null; then + echo "✅ 已启动, PID: \$(cat $HOME/mumulingsi.pid)" else echo "❌ 启动失败,查看日志: \$LOG_DIR/app.log" exit 1 @@ -329,8 +329,8 @@ else exec "\$PYTHON" -m uvicorn app.main:app --host 0.0.0.0 --port 8000 fi STARTEOF -chmod +x "$HOME/mumuainovel-start.sh" -info "启动脚本已创建: ~/mumuainovel-start.sh" +chmod +x "$HOME/mumulingsi-start.sh" +info "启动脚本已创建: ~/mumulingsi-start.sh" # ============================================================================= # 安装完成 @@ -341,16 +341,16 @@ echo -e "${GREEN}║ 🎉 墨木灵思 安装完成! ║${N echo -e "${GREEN}╠══════════════════════════════════════════════╣${NC}" echo -e "${GREEN}║ ║${NC}" echo -e "${GREEN}║ 前台运行(Ctrl+C 停止): ║${NC}" -echo -e "${GREEN}║ bash ~/mumuainovel-start.sh ║${NC}" +echo -e "${GREEN}║ bash ~/mumulingsi-start.sh ║${NC}" echo -e "${GREEN}║ ║${NC}" echo -e "${GREEN}║ 后台运行: ║${NC}" -echo -e "${GREEN}║ bash ~/mumuainovel-start.sh --bg ║${NC}" +echo -e "${GREEN}║ bash ~/mumulingsi-start.sh --bg ║${NC}" echo -e "${GREEN}║ ║${NC}" echo -e "${GREEN}║ 停止后台: ║${NC}" -echo -e "${GREEN}║ kill \$(cat ~/mumuainovel.pid) ║${NC}" +echo -e "${GREEN}║ kill \$(cat ~/mumulingsi.pid) ║${NC}" echo -e "${GREEN}║ ║${NC}" echo -e "${GREEN}║ 查看日志: ║${NC}" -echo -e "${GREEN}║ tail -f ~/mumuainovel/logs/app.log ║${NC}" +echo -e "${GREEN}║ tail -f ~/mumulingsi/logs/app.log ║${NC}" echo -e "${GREEN}║ ║${NC}" echo -e "${GREEN}║ 🌐 访问: http://127.0.0.1:8000 ║${NC}" echo -e "${GREEN}║ 🔑 账号: admin / admin123 ║${NC}"