版本1.0.4 - 发布前准备
- 修复system.py缩进错误 - 优化前端页面样式(待重构) - 改进API接口结构 - 完善文档和自动化脚本 - 平台基本功能稳定运行
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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)}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
# 计算今日新增
|
||||
|
||||
@@ -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)):
|
||||
|
||||
@@ -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\n(LLM 调用失败:{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\n(LLM 调用失败:{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}")
|
||||
@@ -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}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user