Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
011f3a5
feat: 本地模型支持、叙事健康监控、Tiptap编辑器、自动驾驶面板全面升级
shenminglinyi Jun 5, 2026
a1e8500
chore: 工程规范化 — pip-installable、Docker、Makefile、pre-commit、测试整理
shenminglinyi Jun 5, 2026
001763b
build: Tauri 桌面应用构建支持
shenminglinyi Jun 5, 2026
b95c21e
fix: 前端 TypeScript 错误清零 + DAG 测试修复 + 一键启动脚本
shenminglinyi Jun 6, 2026
4fedd80
feat: 全方位 UI/UX 升级 — 禅模式、斜杠命令、AI内联建议、Corkboard、仪表盘、全局搜索
shenminglinyi Jun 6, 2026
5c067e5
feat: 全方位智能化 — 9 个 AI 分析节点 + 叙事健康大屏 + 写作教练面板
shenminglinyi Jun 6, 2026
004fd6e
feat: 工作流现代化 — 反思规划器 + 迭代写作 + 智能路由 + 并行审计 + 自适应重试
shenminglinyi Jun 6, 2026
e623f8b
feat: 工作流全面升级到 2025 SOTA — SuperWriter + MCTS + KG验证 + 编排工厂
shenminglinyi Jun 6, 2026
bb3ac56
feat: 工作流 2026 SOTA — BiT-MCTS + Writer-R1 + 熵控制 + 伏笔池 + 多样性 + 反扁平化 +…
shenminglinyi Jun 6, 2026
00cd00d
feat: Binder 树形导航 + 标签页 + 分屏编辑器
shenminglinyi Jun 6, 2026
b99eac4
fix: TS errors in App.vue/WorkArea/Dashboard, remove incomplete Narra…
shenminglinyi Jun 6, 2026
9490df3
fix: PersistenceQueue 导入缺失 + DB LLM 配置修复
shenminglinyi Jun 6, 2026
e834328
feat: 一键启停脚本 — 后端+前端协同生命周期管理
shenminglinyi Jun 7, 2026
17e119e
fix: 重写启停脚本 — 兼容CMD、用PS替代curl
shenminglinyi Jun 7, 2026
ada1d19
fix: beat_middleware.py 缺少 Any 类型导入 — 阻塞章节生成
shenminglinyi Jun 7, 2026
024d4a8
fix: Vite 代理 8010→8005 + TS 杂项修复
shenminglinyi Jun 7, 2026
a8a6f41
fix: 桌面原生启动 — BAT设置PYTHONPATH+跳过自清理,代理端口修正
shenminglinyi Jun 7, 2026
36f70ca
feat: 简化API接入 — 极简三键(Agnes/Mimo/DeepSeek)
shenminglinyi Jun 7, 2026
04e821b
chore: 清理构建产物
shenminglinyi Jun 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Python
.venv/
venv/
env/
.env/
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
build/
*.egg

# IDE
.idea/
.vscode/
*.swp
*.swo

# Testing
.pytest_cache/
htmlcov/
.coverage
.coverage.*

# Frontend (not needed in container)
frontend/
node_modules/

# Embedded Python (18MB zip, not needed in container)
tools/python-3.11.9-embed-amd64.zip
tools/python_embed/
tools/plotpilot/

# Git
.git/
.gitignore
34 changes: 33 additions & 1 deletion .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ on:
- "pytest.ini"

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install ruff
run: pip install ruff

- name: Ruff lint
run: ruff check . --output-format=github

- name: Ruff format check
run: ruff format --check .

test:
runs-on: ubuntu-latest
steps:
Expand All @@ -25,6 +43,20 @@ jobs:
- name: Run unit tests
run: pytest tests/unit -q --tb=short
env:
# 单测不需要真实 key,用占位值防止启动时报错
ANTHROPIC_API_KEY: "test-placeholder"
ARK_API_KEY: "test-placeholder"

security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install pip-audit
run: pip install pip-audit

- name: Audit dependencies
run: pip-audit -r requirements.txt --desc
24 changes: 24 additions & 0 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,27 @@ on:
- "frontend/**"

jobs:
lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: frontend/package-lock.json

- name: Install dependencies
run: npm ci

- name: Type check
run: npx vue-tsc --noEmit

build:
runs-on: ubuntu-latest
defaults:
Expand All @@ -24,5 +45,8 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Audit dependencies
run: npm audit --audit-level=high || true

- name: Build
run: npm run build
17 changes: 17 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-toml
- id: check-yaml
args: [--allow-multiple-documents]
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-merge-conflict
8 changes: 8 additions & 0 deletions .vite/deps/_metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"hash": "9dc2b885",
"configHash": "e9b0e9fc",
"lockfileHash": "e3b0c442",
"browserHash": "d66ed44f",
"optimized": {},
"chunks": {}
}
3 changes: 3 additions & 0 deletions .vite/deps/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
42 changes: 42 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# PlotPilot Dockerfile
# 多阶段构建:先安装依赖,再运行应用

FROM python:3.11-slim AS base

LABEL org.opencontainers.image.title="PlotPilot"
LABEL org.opencontainers.image.description="AI 辅助长篇小说创作叙事引擎内核"
LABEL org.opencontainers.image.source="https://github.com/shenminglinyi/PlotPilot"

# 系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
curl \
&& rm -rf /var/lib/apt/lists/*

# 工作目录
WORKDIR /app

# 复制依赖文件
COPY requirements.txt requirements-local.txt ./

# 安装 Python 依赖
RUN pip install --no-cache-dir -r requirements.txt \
&& pip install --no-cache-dir -r requirements-local.txt

# 复制应用代码
COPY . .

# 暴露端口
EXPOSE 8005

# 环境变量
ENV PYTHONUNBUFFERED=1
ENV PLOTPILOT_HOST=0.0.0.0
ENV PLOTPILOT_PORT=8005

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8005/health || exit 1

# 启动
CMD ["python", "-m", "uvicorn", "interfaces.main:app", "--host", "0.0.0.0", "--port", "8005"]
54 changes: 54 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# PlotPilot Makefile
# 常用开发命令速查

.PHONY: help install dev-install run test lint format clean docker-build docker-up docker-down

help: ## 显示帮助
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'

install: ## 安装核心依赖
pip install -r requirements.txt

dev-install: ## 安装全部开发依赖(含本地向量模型)
pip install -e ".[dev,local]"

run: ## 启动 API 服务器(开发模式,热重载)
uvicorn interfaces.main:app --host 127.0.0.1 --port 8005 --reload

serve: ## 启动 API 服务器(生产模式)
uvicorn interfaces.main:app --host 0.0.0.0 --port 8005

test: ## 运行单元测试
pytest tests/unit -v --tb=short

test-all: ## 运行全部测试
pytest tests/ -v --tb=short

lint: ## 代码检查
ruff check .

format: ## 代码格式化
ruff format .

lint-fix: ## 自动修复代码问题
ruff check --fix .

clean: ## 清理缓存与构建产物
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
find . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true
find . -type d -name .ruff_cache -exec rm -rf {} + 2>/dev/null || true
rm -rf build/ dist/ htmlcov/ .coverage

docker-build: ## 构建 Docker 镜像
docker compose build

docker-up: ## 启动 Docker 服务
docker compose up -d

docker-down: ## 停止 Docker 服务
docker compose down

db-reset: ## 重置数据库(危险操作)
rm -f data/*.sqlite data/*.sqlite-shm data/*.sqlite-wal
@echo "数据库已重置,重启服务即可自动重建"
28 changes: 28 additions & 0 deletions PlotPilot.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@echo off
cd /d "D:\PlotPilot"
title PlotPilot

echo Starting PlotPilot...

rem start backend with PYTHONPATH set
set PYTHONPATH=D:\PlotPilot
set PLOTPILOT_SKIP_PROCESS_CLEANUP=1
start "PlotPilot-Backend" /min .venv\Scripts\python.exe -m uvicorn interfaces.main:app --host 0.0.0.0 --port 8005

rem wait for backend to be ready (max 30 seconds)
echo Waiting for backend...
set /a n=0
:wait
timeout /t 2 /nobreak >nul
set /a n+=1
powershell -NoProfile -Command "try {$r=Invoke-WebRequest http://127.0.0.1:8005/health -UseBasicParsing -TimeoutSec 2; if($r.StatusCode -eq 200){exit 0}}catch{exit 1}" >nul 2>&1
if %errorlevel% neq 0 (
if %n% lss 15 goto wait
)

rem launch native desktop app
start "" "D:\PlotPilot\frontend\src-tauri\target\release\plotpilot.exe"

echo.
echo PlotPilot launched!
timeout /t 2 /nobreak >nul
17 changes: 15 additions & 2 deletions application/ai/llm_control_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,12 @@ def get_runtime_summary(self, config: Optional[LLMControlConfig] = None) -> LLMR
reason='未找到任何 LLM 配置',
)

if not profile.api_key.strip() or not profile.model.strip():
from application.ai.local_llm_utils import effective_local_api_key

api_key = effective_local_api_key(
profile.api_key, profile.base_url or '', profile.protocol or 'openai'
)
if not api_key.strip() or not profile.model.strip():
return LLMRuntimeSummary(
source='mock',
active_profile_id=profile.id,
Expand All @@ -431,15 +436,23 @@ async def test_profile_model(
profile: LLMProfile,
llm_service_factory: Callable[[LLMProfile], LLMService],
) -> LLMTestResult:
from application.ai.local_llm_utils import effective_local_api_key

resolved = self.resolve_profile(profile)
if not resolved.api_key.strip() or not resolved.model.strip():
api_key = effective_local_api_key(
resolved.api_key, resolved.base_url or '', resolved.protocol or 'openai'
)
if not api_key.strip() or not resolved.model.strip():
return LLMTestResult(
ok=False,
provider_label=resolved.name,
model=resolved.model or '',
latency_ms=0,
error='请先填写 API Key 与模型名后再测试',
)
resolved = LLMProfile(
**{**resolved.model_dump(), 'api_key': api_key},
)
started = perf_counter()
try:
llm_service = llm_service_factory(resolved)
Expand Down
30 changes: 30 additions & 0 deletions application/ai/local_llm_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""本机 OpenAI 兼容 LLM(Ollama / LM Studio 等)连接辅助。"""
from __future__ import annotations

from urllib.parse import urlparse

# Ollama / LM Studio 等通常不校验 Key,但 OpenAI SDK 要求非空
LOCAL_LLM_PLACEHOLDER_API_KEY = "ollama"


def is_local_openai_compatible_base_url(base_url: str) -> bool:
"""是否为本机回环地址上的 OpenAI 兼容端点。"""
raw = (base_url or "").strip()
if not raw:
return False
if "://" not in raw:
raw = f"http://{raw}"
host = (urlparse(raw).hostname or "").lower()
if host in ("127.0.0.1", "localhost", "::1", "0.0.0.0"):
return True
return host.endswith(".localhost")


def effective_local_api_key(api_key: str, base_url: str, protocol: str = "openai") -> str:
"""本地端点无 Key 时使用占位符,避免退回 MockProvider。"""
key = (api_key or "").strip()
if key:
return key
if protocol == "openai" and is_local_openai_compatible_base_url(base_url):
return LOCAL_LLM_PLACEHOLDER_API_KEY
return key
Loading