feat: 完成布局优化 - 操作列固定、批量按钮自适应、分类标签带数量
优化内容: 1. 表格布局: - 使用 calc(100vw - 160px) 确保表格不超出视口 - 操作列 fixed='right' 固定在右侧,宽度 300px - 按钮 3 个后自动换行 (max-width: 200px) - 恢复合理列宽,不再过度压缩 2. 批量操作区域: - 容器改为 inline-block,宽度自适应按钮内容 - 背景宽度与按钮总宽度匹配 3. 分类标签: - 显示数量 (如 '待处理 (20)') - 点击切换筛选,去掉误导的 'X' 图标 4. 删除功能: - 操作列增加删除按钮 - 删除前弹出确认对话框 5. 系统日志: - 修复后端日志路径 (parents[4]) - 404 时显示友好提示 6. 其他: - 左侧菜单宽度 160px - 所有功能保留 (登录、用户管理、批量操作等)
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
审计日志记录模块
|
||||
|
||||
用法:
|
||||
from .audit_logger import audit_log
|
||||
audit_log(action="create_user", user=current_user, details={...}, request=request, db=db)
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from ..models import AuditLog
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def audit_log(
|
||||
action: str,
|
||||
*,
|
||||
user=None, # User 对象或 None
|
||||
username: Optional[str] = None,
|
||||
resource_type: Optional[str] = None,
|
||||
resource_id: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
user_agent: Optional[str] = None,
|
||||
db: Session = None
|
||||
) -> None:
|
||||
"""
|
||||
记录审计日志
|
||||
|
||||
参数:
|
||||
action: 操作类型(必填),如 "login", "create_user", "delete_user", "publish"
|
||||
user: 操作用户的 User 对象(可选,如果提供则自动填充 user_id 和 username)
|
||||
username: 直接指定用户名(如果 user 为 None 则必须提供)
|
||||
resource_type: 资源类型,如 "user", "topic", "publish_record"
|
||||
resource_id: 资源ID
|
||||
details: 操作详情字典(如变更前后的值)
|
||||
ip_address: IP 地址
|
||||
user_agent: User-Agent
|
||||
db: 数据库会话(必填)
|
||||
"""
|
||||
if db is None:
|
||||
raise ValueError("db session is required")
|
||||
|
||||
# 确定 user_id 和 username
|
||||
user_id = None
|
||||
if user is not None:
|
||||
user_id = getattr(user, 'id', None)
|
||||
username = getattr(user, 'username', username)
|
||||
if not username:
|
||||
username = "anonymous"
|
||||
|
||||
log = AuditLog(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
details=details or {},
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
created_at=datetime.utcnow()
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
# 不抛出异常,避免影响主流程
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 计算项目根目录(从本文件位置上升4层)
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[4]
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
# 允许环境变量覆盖(适合容器部署)
|
||||
if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
@@ -26,7 +26,7 @@ def run_creator(topic_id: str = None):
|
||||
cwd=str(PROJECT_ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=300 # 5分钟超时
|
||||
timeout=1800 # 30分钟超时,避免AI撰写超时
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Creator failed: {result.stderr}")
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
""" ModelScope 专用 LLM 客户端 """
|
||||
import requests
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
class LLMError(Exception):
|
||||
pass
|
||||
|
||||
# 临时使用 NVIDIA 端点(ModelScope 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:
|
||||
"""调用 ModelScope 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']
|
||||
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"[modelscope_client] 使用模型:{CONFIG['model']}")
|
||||
resp = call_llm("你好,请用一句话介绍你自己。", max_tokens=50)
|
||||
print(f"[modelscope_client] 响应:{resp}")
|
||||
except Exception as e:
|
||||
print(f"[modelscope_client] 错误:{e}")
|
||||
@@ -9,7 +9,7 @@ from typing import List
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 计算项目根目录(从本文件位置上升4层)
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[4]
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
if os.getenv('PROJECT_ROOT'):
|
||||
PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT'))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user