版本1.0.4 - 发布前准备

- 修复system.py缩进错误
- 优化前端页面样式(待重构)
- 改进API接口结构
- 完善文档和自动化脚本
- 平台基本功能稳定运行
This commit is contained in:
lt
2026-04-29 09:32:43 +08:00
parent 0a31ae09af
commit e1ba31afda
96 changed files with 165251 additions and 376 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ from datetime import datetime, date
router = APIRouter(prefix="/api/articles", tags=["articles"])
PROJECT_ROOT = Path('/root/.openclaw/workspaces/yzr-yxl/projects/yu-zhi-ran')
PROJECT_ROOT = Path('/root/openclaw-workspace/projects/yu-zhi-ran')
@router.get("/drafts")
def list_drafts(publish_date: str = None):
+10 -11
View File
@@ -138,17 +138,7 @@ def login(login_data: LoginRequest, request: Request, db: Session = Depends(get_
)
return TokenResponse(token=token, role=user.role, user=UserResponse.from_orm(user))
@router.get("/me")
def get_current_user(request: Request, db: Session = Depends(get_db)):
"""获取当前登录用户信息"""
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(status_code=401, detail="未提供认证令牌")
token = auth_header.split(" ")[1]
user = verify_token(token, db)
return {"user": UserResponse.from_orm(user)}
def get_current_user(request: Request, db: Session = Depends(get_db)):
def get_current_user(request: Request, db: Session = Depends(get_db)) -> User:
"""依赖项:验证用户登录"""
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
@@ -156,3 +146,12 @@ def get_current_user(request: Request, db: Session = Depends(get_db)):
token = auth_header.split(" ")[1]
user = verify_token(token, db)
return user
@router.get("/me")
def get_me(
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""获取当前登录用户信息"""
return {"user": UserResponse.from_orm(current_user)}
+4 -4
View File
@@ -31,10 +31,10 @@ def get_status(db: Session = Depends(get_db)):
# 确保返回所有状态
status_map = {
'待处理': by_status.get('pending', 0),
'待审查': by_status.get('review', 0),
'待发布': by_status.get('ready', 0),
'已发布': by_status.get('published', 0)
'pending': by_status.get('pending', 0),
'review': by_status.get('review', 0),
'ready': by_status.get('ready', 0),
'published': by_status.get('published', 0)
}
# 计算今日新增
+9 -2
View File
@@ -183,8 +183,15 @@ def generate_packages(topic_id: str):
手动触发单个选题的发布包生成。
相当于执行 publisher.py 针对单个选题。
"""
# TODO: 实际调用 publisher.py 逻辑,这里先返回模拟响应
return {"message": "Package generation triggered", "topic_id": topic_id, "status": "pending"}
from ..core.publisher import run_publisher
result = run_publisher(topic_id)
if not result["ok"]:
raise HTTPException(status_code=500, detail=result["error"])
return {
"message": f"Package generation completed for topic {topic_id}",
"topic_id": topic_id,
"output": result.get("result", "")
}
@router.delete("/{topic_id}")
def delete_topic(topic_id: str, db: Session = Depends(get_db)):
+44 -11
View File
@@ -1,11 +1,11 @@
"""
NVIDIA 专用 LLM 客户端(fixed configuration
使用 OpenAI 兼容接口调用 stepfun-ai/step-3.5-flash
NVIDIA 专用 LLM 客户端(优化配置
支持 Google Gemma 和其他 NVIDIA 模型
"""
import requests
import json
from typing import Optional
from typing import Optional, Dict, Any
class LLMError(Exception):
pass
@@ -13,36 +13,61 @@ class LLMError(Exception):
# 固定配置(你的可用 key
CONFIG = {
"base_url": "https://integrate.api.nvidia.com/v1",
"api_key": "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",
"model": "stepfun-ai/step-3.5-flash",
"api_key": "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",
}
def call_llm(
prompt: str,
model: str = "google/gemma-3n-e4b-it",
system_prompt: str = "你是一个专业的内容创作助手。",
temperature: float = 0.7,
max_tokens: int = 2000,
temperature: float = 0.20,
max_tokens: int = 512,
top_p: float = 0.70,
frequency_penalty: float = 0.00,
presence_penalty: float = 0.00,
stream: bool = False,
additional_params: Optional[Dict[str, Any]] = None,
) -> str:
"""
调用 NVIDIA LLM 生成文本
参数:
prompt: 用户提示词
model: 模型名称,默认 google/gemma-3n-e4b-it
system_prompt: 系统提示词
temperature: 温度参数 (0.0-2.0),默认 0.20
max_tokens: 最大生成 token 数,默认 512
top_p: 核采样参数 (0.0-1.0),默认 0.70
frequency_penalty: 频率惩罚 (0.0-2.0),默认 0.00
presence_penalty: 存在惩罚 (0.0-2.0),默认 0.00
stream: 是否流式输出,默认 False
additional_params: 额外参数(如 reasoning_effort
"""
endpoint = f"{CONFIG['base_url'].rstrip('/')}/chat/completions"
headers = {
"Authorization": f"Bearer {CONFIG['api_key']}",
"Content-Type": "application/json"
}
# 基础参数
payload = {
"model": CONFIG["model"],
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
"temperature": temperature,
"max_tokens": max_tokens,
"top_p": top_p,
"frequency_penalty": frequency_penalty,
"presence_penalty": presence_penalty,
"stream": stream,
}
# 添加额外参数(如 reasoning_effort
if additional_params:
payload.update(additional_params)
try:
resp = requests.post(endpoint, json=payload, headers=headers, timeout=120, stream=stream)
if resp.status_code != 200:
@@ -103,7 +128,15 @@ def expand_content_with_llm(topic: dict, section_title: str, section_content: st
prompt = f"# 参考资料\n{context}\n\n{prompt}"
try:
result = call_llm(prompt, temperature=0.8, max_tokens=2000)
result = call_llm(
prompt,
model="google/gemma-3n-e4b-it",
temperature=0.20,
max_tokens=2000,
top_p=0.70,
frequency_penalty=0.00,
presence_penalty=0.00
)
return result.strip()
except Exception as e:
return f"## {section_title}\n\nLLM 调用失败:{e},请手动补充)"
@@ -111,7 +144,7 @@ def expand_content_with_llm(topic: dict, section_title: str, section_content: st
# 测试
if __name__ == "__main__":
try:
print(f"[nvidia_client] 使用模型: {CONFIG['model']}")
print(f"[nvidia_client] 使用模型: google/gemma-3n-e4b-it")
resp = call_llm("你好,请用一句话介绍你自己。", max_tokens=50)
print(f"[nvidia_client] 响应: {resp}")
except Exception as e:
@@ -0,0 +1,118 @@
"""
NVIDIA 专用 LLM 客户端(fixed configuration
使用 OpenAI 兼容接口调用 stepfun-ai/step-3.5-flash
"""
import requests
import json
from typing import Optional
class LLMError(Exception):
pass
# 固定配置(你的可用 key
CONFIG = {
"base_url": "https://integrate.api.nvidia.com/v1",
"api_key": "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",
"model": "stepfun-ai/step-3.5-flash",
}
def call_llm(
prompt: str,
system_prompt: str = "你是一个专业的内容创作助手。",
temperature: float = 0.7,
max_tokens: int = 2000,
stream: bool = False,
) -> str:
"""
调用 NVIDIA LLM 生成文本
"""
endpoint = f"{CONFIG['base_url'].rstrip('/')}/chat/completions"
headers = {
"Authorization": f"Bearer {CONFIG['api_key']}",
"Content-Type": "application/json"
}
payload = {
"model": CONFIG["model"],
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt}
],
"temperature": temperature,
"max_tokens": max_tokens,
"stream": stream,
}
try:
resp = requests.post(endpoint, json=payload, headers=headers, timeout=120, stream=stream)
if resp.status_code != 200:
raise LLMError(f"HTTP {resp.status_code}: {resp.text[:200]}")
if stream:
full = []
for line in resp.iter_lines():
if not line:
continue
if line.startswith(b'data: '):
data = line[6:]
if data == b'[DONE]':
break
try:
chunk = json.loads(data)
delta = chunk['choices'][0]['delta']
# 支持 reasoning_content 或 reasoning 字段
if 'reasoning_content' in delta and delta['reasoning_content']:
full.append(delta['reasoning_content'])
if 'content' in delta and delta['content']:
full.append(delta['content'])
except Exception:
continue
return "".join(full)
else:
data = resp.json()
msg = data["choices"][0]["message"]
content = msg.get('content') or msg.get('reasoning') or msg.get('reasoning_content')
return content.strip() if content else ''
except requests.RequestException as e:
raise LLMError(f"Request failed: {e}")
def expand_content_with_llm(topic: dict, section_title: str, section_content: str, context: str = "") -> str:
"""扩写大纲章节,返回包含 ## 标题的完整 Markdown"""
prompt = f"""你是一个专业的内容创作者。请将以下大纲扩展为完整的文章章节。
# 选题信息
- 标题:{topic.get('title')}
- 领域:{topic.get('field')}
- 核心观点:{topic.get('core_concept', '')}
- 受众痛点:{topic.get('audience_pain', '')}
- 独特视角:{topic.get('unique_angle', '')}
# 当前章节
## {section_title}
{section_content}
# 要求
- 以 `## {section_title}` 作为章节标题开头
- 字数:300-500字
- 风格:客观、专业、易懂
- 使用 Markdown 格式
- 包含具体数据或案例(如果有)
- 保持与整体文章调性一致
直接输出完整的 Markdown 章节(包括 ## 标题和正文段落)。"""
if context:
prompt = f"# 参考资料\n{context}\n\n{prompt}"
try:
result = call_llm(prompt, temperature=0.8, max_tokens=2000)
return result.strip()
except Exception as e:
return f"## {section_title}\n\nLLM 调用失败:{e},请手动补充)"
# 测试
if __name__ == "__main__":
try:
print(f"[nvidia_client] 使用模型: {CONFIG['model']}")
resp = call_llm("你好,请用一句话介绍你自己。", max_tokens=50)
print(f"[nvidia_client] 响应: {resp}")
except Exception as e:
print(f"[nvidia_client] 错误: {e}")
+49
View File
@@ -0,0 +1,49 @@
from pathlib import Path
import subprocess
import sys
PROJECT_ROOT = Path(__file__).resolve().parents[4]
def run_publisher(topic_id: str = None):
"""运行发布包生成脚本
Args:
topic_id: 可选,指定单个选题ID
Returns:
dict: 包含 ok, result/error, topic_id 等字段
"""
try:
script_path = PROJECT_ROOT / "scripts" / "publisher.py"
if not script_path.exists():
return {"ok": False, "error": f"Publisher script not found: {script_path}"}
cmd = [sys.executable, str(script_path)]
if topic_id:
cmd.extend(["--topic-id", topic_id])
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=PROJECT_ROOT,
timeout=300 # 5分钟超时
)
if result.returncode == 0:
return {
"ok": True,
"result": result.stdout,
"topic_id": topic_id
}
else:
return {
"ok": False,
"error": result.stderr,
"result": result.stdout,
"topic_id": topic_id
}
except subprocess.TimeoutExpired:
return {"ok": False, "error": "Publisher timed out after 5 minutes", "topic_id": topic_id}
except Exception as e:
return {"ok": False, "error": str(e), "topic_id": topic_id}
+22 -7
View File
@@ -5,15 +5,30 @@ import os
from pathlib import Path
# 计算项目根目录(backend/app/database.py -> yu-zhi-ran
# __file__: platform/backend/app/database.py
# parents[0]=app, [1]=backend, [2]=platform, [3]=yu-zhi-ran
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
DATA_DIR = os.getenv('DATA_DIR', str(PROJECT_ROOT / 'data'))
os.makedirs(DATA_DIR, exist_ok=True)
DB_PATH = os.path.join(DATA_DIR, 'yzr.db')
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_PATH}"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
# 数据库配置:通过环境变量控制
USE_POSTGRES = os.getenv('USE_POSTGRES', 'false').lower() == 'true'
if USE_POSTGRES:
# PostgreSQL 生产数据库
POSTGRES_CONFIG = {
'host': os.getenv('PG_HOST', '127.0.0.1'),
'port': os.getenv('PG_PORT', '5432'),
'database': os.getenv('PG_DATABASE', 'yzr_nr'),
'user': os.getenv('PG_USER', 'yzr_nr'),
'password': os.getenv('PG_PASSWORD', 'aTX3WKKnPfRnM5PC')
}
SQLALCHEMY_DATABASE_URL = f"postgresql://{POSTGRES_CONFIG['user']}:{POSTGRES_CONFIG['password']}@{POSTGRES_CONFIG['host']}:{POSTGRES_CONFIG['port']}/{POSTGRES_CONFIG['database']}"
engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True)
else:
# SQLite 开发数据库
DATA_DIR = os.getenv('DATA_DIR', str(PROJECT_ROOT / 'data'))
os.makedirs(DATA_DIR, exist_ok=True)
DB_PATH = os.path.join(DATA_DIR, 'yzr.db')
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_PATH}"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
+1 -1
View File
@@ -39,7 +39,7 @@ app.include_router(admin.router)
app.include_router(audit.router)
# 挂载前端
FRONTEND_DIR = Path(__file__).parent.parent.parent / "frontend"
FRONTEND_DIR = Path(__file__).parent.parent / "static"
if FRONTEND_DIR.exists() and (FRONTEND_DIR / "index.html").exists():
app.mount("/", StaticFiles(directory=str(FRONTEND_DIR), html=True), name="frontend")
logger.info(f"Frontend mounted at / from {FRONTEND_DIR}")