chore: opencode冗余清理 + LLM任务级模型选择 + systemd服务化
- 删除 opencode_search.py / mcp_search_server.py 及所有 MCP 引用 - 移除搜索缓存定时任务(scheduled_refresh_search_cache) - 清理前后端所有 opencode/MCP 代码和注释 - LLM 提供商量换:opencode-go→nvidia(默认)+sensenova(合规审查) - llm_configs 新增 is_default 字段,API 层互斥逻辑 - 所有定时任务支持独立 LLM 模型选择(LLM_TASK_PROVIDER env) - compliance_optimizer.py 修复:import os / 解硬编码 / 关键词过滤 - Scheduler 日志修复:始终 INSERT,避免僵尸 running 行 - Systemd 服务化:Restart=always / 单 worker / Type=exec - 搜索提供商:替换 opencode→360/搜狗/微信(免 Key) - 更新 AGENTS.md / PROGRESS.md
This commit is contained in:
@@ -9,6 +9,14 @@ from .auth import get_current_admin
|
||||
|
||||
router = APIRouter(prefix="/api/admin/llmconfigs", tags=["admin"])
|
||||
|
||||
def _apply_default_exclusive(config: LLMConfig, db: Session):
|
||||
"""当 config.is_default=True 时,将其他所有配置的 is_default 置为 False"""
|
||||
if config.is_default:
|
||||
db.query(LLMConfig).filter(LLMConfig.id != config.id).update(
|
||||
{"is_default": False}, synchronize_session=False
|
||||
)
|
||||
db.flush()
|
||||
|
||||
@router.get("", response_model=List[LLMConfigResponse])
|
||||
def list_llm_configs(
|
||||
request: Request,
|
||||
@@ -42,6 +50,8 @@ def create_llm_config(
|
||||
"""创建 LLM 配置"""
|
||||
config = LLMConfig(**config_data.model_dump())
|
||||
db.add(config)
|
||||
db.flush()
|
||||
_apply_default_exclusive(config, db)
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
return config
|
||||
@@ -61,6 +71,7 @@ def update_llm_config(
|
||||
update_data = config_update.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(config, field, value)
|
||||
_apply_default_exclusive(config, db)
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
return config
|
||||
|
||||
@@ -119,16 +119,6 @@ def test_provider(provider_id: int, data: dict = {}, db: Session = Depends(get_d
|
||||
if resp.status_code != 200:
|
||||
return {"ok": False, "error": f"HTTP {resp.status_code}: {resp.text[:200]}"}
|
||||
return {"ok": True, "results": resp.json().get("webPages", {}).get("value", [])[:3]}
|
||||
elif p.provider_type == "mcp":
|
||||
import subprocess, json as _json
|
||||
mcp_script = Path(__file__).resolve().parent.parent.parent.parent.parent / "scripts" / "mcp_search_server.py"
|
||||
r = subprocess.run(
|
||||
[sys.executable, str(mcp_script), "--query", query],
|
||||
capture_output=True, text=True, timeout=90,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
return {"ok": False, "error": f"子进程失败: {r.stderr[:200]}"}
|
||||
return {"ok": True, "results": _json.loads(r.stdout)[:3]}
|
||||
return {"ok": False, "error": f"Unknown provider_type: {p.provider_type}"}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": str(e)}
|
||||
|
||||
@@ -280,28 +280,6 @@ def trigger_metrics_sync():
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/refresh-search-cache/run")
|
||||
def trigger_refresh_search_cache(db: Session = Depends(get_db), current_user=Depends(get_current_user)):
|
||||
try:
|
||||
import sys as sys_mod
|
||||
scripts_dir = PROJECT_ROOT / "scripts"
|
||||
from ..database import SessionLocal as _ss
|
||||
proc = subprocess.Popen(
|
||||
[sys_mod.executable, str(scripts_dir / "opencode_search.py"), "--refresh-cache"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
|
||||
cwd=str(PROJECT_ROOT)
|
||||
)
|
||||
logger.info("Search cache refresh started (pid=%s)", proc.pid)
|
||||
log = TaskLog(module_id="scheduled_refresh_search_cache", task_name="🔍 搜索缓存", status="running", message="搜索缓存刷新已启动", triggered_by="manual", started_at=datetime.now(timezone.utc), result_data={"pid": proc.pid})
|
||||
db.add(log)
|
||||
db.commit()
|
||||
log_id = log.id
|
||||
t = threading.Thread(target=_monitor_subprocess, args=(log_id, proc, "scheduled_refresh_search_cache", "🔍 搜索缓存", _ss), daemon=True)
|
||||
t.start()
|
||||
return {"message": "搜索缓存刷新已后台启动", "pid": proc.pid, "log_id": log_id}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@router.post("/trends/run")
|
||||
def trigger_trends_refresh(db: Session = Depends(get_db), current_user=Depends(get_current_user)):
|
||||
try:
|
||||
@@ -370,7 +348,6 @@ def get_modules_status(db: Session = Depends(get_db)):
|
||||
config_map = {c.module_id: c for c in configs}
|
||||
|
||||
MODULE_META = {
|
||||
"scheduled_refresh_search_cache": {"name": "🔍 搜索缓存", "cron": "01:00", "params_desc": {"refresh_queries": "搜索关键词列表"}},
|
||||
"scheduled_fetch_trends": {"name": "🔥 热点趋势", "cron": "01:10", "params_desc": {}},
|
||||
"scheduled_collect": {"name": "📡 内容采集", "cron": "01:30", "params_desc": {"max_topics": "最大选题数", "categories": "采集类别"}},
|
||||
"scheduled_generate": {"name": "🤖 内容创作", "cron": "02:00", "params_desc": {"auto_review": "自动合规审查"}},
|
||||
|
||||
@@ -11,14 +11,13 @@ from .auth import get_current_admin
|
||||
router = APIRouter(prefix="/api/admin/task-configs", tags=["admin"])
|
||||
|
||||
DEFAULT_CONFIGS = {
|
||||
"scheduled_refresh_search_cache": {"name": "🔍 搜索缓存", "cron": "01:00", "params": {}},
|
||||
"scheduled_fetch_trends": {"name": "🔥 热点趋势", "cron": "01:10", "params": {}},
|
||||
"scheduled_collect": {"name": "📡 内容采集", "cron": "01:30", "params": {"max_topics": 20}},
|
||||
"scheduled_generate": {"name": "🤖 内容创作", "cron": "02:00", "params": {"auto_review": True}},
|
||||
"scheduled_optimize": {"name": "🔍 合规审查", "cron": "03:00", "params": {"auto_pass_threshold": 80}},
|
||||
"scheduled_optimize_sources": {"name": "📡 信息源优化", "cron": "05:00", "params": {}},
|
||||
"scheduled_metrics_sync": {"name": "📊 指标同步", "cron": "06:00", "params": {}},
|
||||
"scheduled_task_monitor": {"name": "⏰ 任务监控", "cron": "*", "params": {}},
|
||||
"scheduled_fetch_trends": {"name": "🔥 热点趋势", "cron": "01:10", "params": {"llm_provider": "nvidia"}},
|
||||
"scheduled_collect": {"name": "📡 内容采集", "cron": "01:30", "params": {"max_topics": 20, "llm_provider": "nvidia"}},
|
||||
"scheduled_generate": {"name": "🤖 内容创作", "cron": "02:00", "params": {"auto_review": True, "llm_provider": "nvidia"}},
|
||||
"scheduled_optimize": {"name": "🔍 合规审查", "cron": "03:00", "params": {"auto_pass_threshold": 80, "llm_provider": "sensenova"}},
|
||||
"scheduled_optimize_sources": {"name": "📡 信息源优化", "cron": "05:00", "params": {"llm_provider": "nvidia"}},
|
||||
"scheduled_metrics_sync": {"name": "📊 指标同步", "cron": "06:00", "params": {"llm_provider": "nvidia"}},
|
||||
"scheduled_task_monitor": {"name": "⏰ 任务监控", "cron": "*", "params": {"llm_provider": "nvidia"}},
|
||||
}
|
||||
|
||||
def _attach_last_log(resp: TaskConfigResponse, db: Session, module_id: str) -> TaskConfigResponse:
|
||||
|
||||
@@ -12,7 +12,6 @@ from .auth import get_current_admin
|
||||
router = APIRouter(prefix="/api/admin/task-logs", tags=["admin"])
|
||||
|
||||
MODULES = {
|
||||
"scheduled_refresh_search_cache": "🔍 搜索缓存",
|
||||
"scheduled_fetch_trends": "🔥 热点趋势",
|
||||
"scheduled_collect": "📡 内容采集",
|
||||
"scheduled_generate": "🤖 内容创作",
|
||||
@@ -79,7 +78,6 @@ def list_log_types(db: Session = Depends(get_db), admin_user=Depends(get_current
|
||||
for mid, name in MODULES.items():
|
||||
if mid in used_ids or True:
|
||||
log_file_map = {
|
||||
"scheduled_refresh_search_cache": "opencode_search",
|
||||
"scheduled_fetch_trends": "trends",
|
||||
"scheduled_collect": "collector",
|
||||
"scheduled_generate": "creator",
|
||||
|
||||
@@ -236,7 +236,6 @@ def _get_module_detail_data(module_id: str, db, ROOT, DATA_DIR, LOGS_DIR, today_
|
||||
import json as json_mod
|
||||
import re as re_mod
|
||||
MODULE_META = {
|
||||
"scheduled_refresh_search_cache": {"name": "🔍 搜索缓存", "description": "通过 opencode webfetch 联网搜索,刷新 8 个分类的搜索缓存,供内容采集器使用"},
|
||||
"scheduled_fetch_trends": {"name": "🔥 热点趋势", "description": "从百度、微博、知乎实时热搜 API 抓取当天热点,LLM 补充,存入 trends.json"},
|
||||
"scheduled_collect": {"name": "📡 内容采集", "description": "读取搜索缓存 + 热点趋势 + 历史表现 + AI 建议,经 LLM 分析后生成选题"},
|
||||
"scheduled_generate": {"name": "🤖 内容创作", "description": "基于选题,LLM 生成三平台文章(知乎、微信、小红书),存入 articles 表"},
|
||||
@@ -253,23 +252,7 @@ def _get_module_detail_data(module_id: str, db, ROOT, DATA_DIR, LOGS_DIR, today_
|
||||
outputs = {}
|
||||
history = []
|
||||
|
||||
if module_id == "scheduled_refresh_search_cache":
|
||||
cache_file = DATA_DIR / "search_cache.json"
|
||||
if cache_file.exists():
|
||||
try:
|
||||
cache = json_mod.loads(cache_file.read_text(encoding="utf-8"))
|
||||
meta_ = cache.pop("_metadata", {})
|
||||
for q, results in cache.items():
|
||||
inputs.setdefault("搜索词", []).append(q)
|
||||
outputs.setdefault("各分类结果", []).append({
|
||||
"query": q, "count": len(results),
|
||||
"samples": [r.get("title","")[:50] for r in results[:3]]
|
||||
})
|
||||
outputs["更新时间"] = meta_.get("updated_at", "")
|
||||
outputs["结果总数"] = sum(len(v) for v in cache.values())
|
||||
except Exception:
|
||||
pass
|
||||
# try reading queries from yaml
|
||||
# try reading queries from yaml
|
||||
try:
|
||||
import yaml
|
||||
cfg_path = ROOT / "config" / "sources.yaml"
|
||||
@@ -390,7 +373,6 @@ def _get_module_detail_data(module_id: str, db, ROOT, DATA_DIR, LOGS_DIR, today_
|
||||
|
||||
# History from log files
|
||||
log_map = {
|
||||
"scheduled_refresh_search_cache": LOGS_DIR / f"opencode_search_{today_str}.log",
|
||||
"scheduled_fetch_trends": LOGS_DIR / f"trends_{today_str}.log",
|
||||
"scheduled_collect": LOGS_DIR / f"collector_{today_str}.log",
|
||||
"scheduled_generate": LOGS_DIR / f"creator_{today_str}.log",
|
||||
|
||||
@@ -35,11 +35,18 @@ _FALLBACK = {
|
||||
}
|
||||
|
||||
def _get_active_provider() -> str:
|
||||
"""从 DB 读取活跃供应商,DB 不可用时回退环境变量"""
|
||||
"""从 DB 读取活跃供应商,优先取 is_default=True;DB 不可用时回退环境变量"""
|
||||
try:
|
||||
from ..database import SessionLocal
|
||||
from ..models import LLMConfig
|
||||
db = SessionLocal()
|
||||
# 优先取默认
|
||||
default = db.query(LLMConfig).filter(
|
||||
LLMConfig.is_default == True, LLMConfig.is_active == True
|
||||
).first()
|
||||
if default and default.provider:
|
||||
db.close()
|
||||
return default.provider
|
||||
active = db.query(LLMConfig).filter(LLMConfig.is_active == True).first()
|
||||
db.close()
|
||||
if active and active.provider:
|
||||
@@ -58,7 +65,7 @@ def _get_provider_config(provider: Optional[str] = None) -> dict:
|
||||
from ..database import SessionLocal
|
||||
from ..models import LLMConfig
|
||||
db = SessionLocal()
|
||||
cfg = db.query(LLMConfig).filter(LLMConfig.provider == p).order_by(LLMConfig.is_active.desc()).first()
|
||||
cfg = db.query(LLMConfig).filter(LLMConfig.provider == p).order_by(LLMConfig.is_default.desc(), LLMConfig.is_active.desc()).first()
|
||||
if cfg:
|
||||
db_model = cfg.model
|
||||
db_base_url = cfg.base_url
|
||||
@@ -107,7 +114,7 @@ def _get_provider_fallback_list() -> List[str]:
|
||||
return providers
|
||||
except Exception:
|
||||
pass
|
||||
return ["opencode-go", "nvidia"]
|
||||
return ["nvidia", "sensenova", "opencode-go"]
|
||||
|
||||
def call_llm(
|
||||
prompt: str,
|
||||
@@ -128,6 +135,10 @@ def call_llm(
|
||||
system_prompt = system_prompt if system_prompt is not None else defaults["system_prompt"]
|
||||
|
||||
providers_to_try = [provider] if provider else _get_provider_fallback_list()
|
||||
# LLM_TASK_PROVIDER 环境变量可覆盖任务级别的模型选择
|
||||
if not provider and os.getenv("LLM_TASK_PROVIDER"):
|
||||
task_provider = os.getenv("LLM_TASK_PROVIDER")
|
||||
providers_to_try = [task_provider] + [p for p in providers_to_try if p != task_provider]
|
||||
last_error = None
|
||||
for p in providers_to_try:
|
||||
try:
|
||||
|
||||
@@ -19,8 +19,25 @@ from .collector import run_collector_blocking
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def _set_task_llm_provider(module_id: str):
|
||||
"""从 TaskConfig 读取 llm_provider 并设为环境变量,供子进程和 call_llm 读取"""
|
||||
try:
|
||||
from ..database import SessionLocal
|
||||
from ..models import TaskConfig
|
||||
db = SessionLocal()
|
||||
cfg = db.query(TaskConfig).filter(TaskConfig.module_id == module_id).first()
|
||||
db.close()
|
||||
if cfg and cfg.params:
|
||||
provider = cfg.params.get("llm_provider")
|
||||
if provider:
|
||||
os.environ["LLM_TASK_PROVIDER"] = provider
|
||||
logger.debug("[%s] LLM provider set to %s", module_id, provider)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
os.environ.pop("LLM_TASK_PROVIDER", None)
|
||||
|
||||
MODULES = {
|
||||
"scheduled_refresh_search_cache": {"name": "🔍 搜索缓存", "cron": "01:00"},
|
||||
"scheduled_fetch_trends": {"name": "🔥 热点趋势", "cron": "01:10"},
|
||||
"scheduled_collect": {"name": "📡 内容采集", "cron": "01:30"},
|
||||
"scheduled_generate": {"name": "🤖 内容创作", "cron": "02:00"},
|
||||
@@ -32,7 +49,6 @@ MODULES = {
|
||||
}
|
||||
|
||||
LOG_FILE_MAP = {
|
||||
"scheduled_refresh_search_cache": "opencode_search",
|
||||
"scheduled_fetch_trends": "trends",
|
||||
"scheduled_collect": "collector",
|
||||
"scheduled_generate": "creator",
|
||||
@@ -149,7 +165,6 @@ class TaskScheduler:
|
||||
db.close()
|
||||
|
||||
MODULE_JOBS = [
|
||||
("scheduled_refresh_search_cache", self._run_refresh_search_cache, "搜索缓存"),
|
||||
("scheduled_fetch_trends", self._run_fetch_trends, "热点趋势"),
|
||||
("scheduled_collect", self._run_collect, "内容采集"),
|
||||
("scheduled_generate", self._run_generate, "内容创作"),
|
||||
@@ -200,6 +215,7 @@ class TaskScheduler:
|
||||
|
||||
def _run_fetch_trends(self):
|
||||
"""定时刷新热点趋势(百度/微博/知乎实时热搜 + LLM补充)"""
|
||||
_set_task_llm_provider("scheduled_fetch_trends")
|
||||
started = datetime.now(timezone.utc)
|
||||
log_id = _log_task("scheduled_fetch_trends", "running", started_at=started)
|
||||
try:
|
||||
@@ -229,48 +245,8 @@ class TaskScheduler:
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
logger.exception("[Scheduled] Trends refresh error: %s", e)
|
||||
|
||||
def _run_refresh_search_cache(self):
|
||||
"""定时刷新搜索缓存(通过 opencode webfetch)"""
|
||||
started = datetime.now(timezone.utc)
|
||||
log_id = _log_task("scheduled_refresh_search_cache", "running", started_at=started)
|
||||
try:
|
||||
logger.info("[Scheduled] Refreshing search cache via opencode...")
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(PROJECT_ROOT / "scripts" / "opencode_search.py"), "--refresh-cache"],
|
||||
capture_output=True, text=True, timeout=600
|
||||
)
|
||||
for line in result.stdout.strip().split("\n"):
|
||||
if line.strip():
|
||||
logger.info("[SearchCache] %s", line.strip())
|
||||
for line in result.stderr.strip().split("\n"):
|
||||
if line.strip():
|
||||
logger.warning("[SearchCache] %s", line.strip())
|
||||
if result.returncode == 0:
|
||||
_log_task("scheduled_refresh_search_cache", "success", log_id=log_id,
|
||||
message="搜索缓存刷新成功",
|
||||
result_data={"output_lines": len(result.stdout.splitlines())},
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
logger.info("[Scheduled] Search cache refreshed")
|
||||
else:
|
||||
_log_task("scheduled_refresh_search_cache", "failed", log_id=log_id,
|
||||
message="部分失败",
|
||||
error_trace=result.stderr[-500:],
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
logger.warning("[Scheduled] Search cache refresh may have partial failures")
|
||||
except subprocess.TimeoutExpired:
|
||||
_log_task("scheduled_refresh_search_cache", "failed", log_id=log_id,
|
||||
message="超时",
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
logger.warning("[Scheduled] Search cache refresh timed out")
|
||||
except Exception as e:
|
||||
_log_task("scheduled_refresh_search_cache", "failed", log_id=log_id,
|
||||
message=str(e),
|
||||
error_trace=traceback.format_exc(),
|
||||
started_at=started, finished_at=datetime.now(timezone.utc))
|
||||
logger.exception("[Scheduled] Search cache refresh error: %s", e)
|
||||
|
||||
def _run_generate(self):
|
||||
_set_task_llm_provider("scheduled_generate")
|
||||
started = datetime.now(timezone.utc)
|
||||
log_id = _log_task("scheduled_generate", "running", started_at=started)
|
||||
try:
|
||||
@@ -296,6 +272,7 @@ class TaskScheduler:
|
||||
logger.exception("[Scheduled] Generation pipeline failed: %s", e)
|
||||
|
||||
def _run_optimize(self):
|
||||
_set_task_llm_provider("scheduled_optimize")
|
||||
started = datetime.now(timezone.utc)
|
||||
log_id = _log_task("scheduled_optimize", "running", started_at=started)
|
||||
try:
|
||||
@@ -314,6 +291,7 @@ class TaskScheduler:
|
||||
logger.exception("[Scheduled] Review failed: %s", e)
|
||||
|
||||
def _run_collect(self):
|
||||
_set_task_llm_provider("scheduled_collect")
|
||||
started = datetime.now(timezone.utc)
|
||||
log_id = _log_task("scheduled_collect", "running", started_at=started)
|
||||
try:
|
||||
@@ -334,6 +312,7 @@ class TaskScheduler:
|
||||
|
||||
def _run_optimize_sources(self, triggered_by="scheduler"):
|
||||
"""AI自动优化采集类别与信息源:对比市场热点和当前配置,给出调整建议"""
|
||||
_set_task_llm_provider("scheduled_optimize_sources")
|
||||
started = datetime.now(timezone.utc)
|
||||
log_id = _log_task("scheduled_optimize_sources", "running", started_at=started, triggered_by=triggered_by)
|
||||
try:
|
||||
@@ -397,6 +376,7 @@ class TaskScheduler:
|
||||
|
||||
def _run_metrics_sync(self, triggered_by="scheduler"):
|
||||
"""定时从各平台公开API获取发布文章的效果数据(当前仅支持知乎)"""
|
||||
_set_task_llm_provider("scheduled_metrics_sync")
|
||||
started = datetime.now(timezone.utc)
|
||||
log_id = _log_task("scheduled_metrics_sync", "running", started_at=started, triggered_by=triggered_by)
|
||||
try:
|
||||
|
||||
@@ -47,6 +47,7 @@ def init_db():
|
||||
conn.execute(text("ALTER TABLE llm_configs ADD COLUMN IF NOT EXISTS provider VARCHAR DEFAULT 'opencode-go'"))
|
||||
conn.execute(text("ALTER TABLE llm_configs ADD COLUMN IF NOT EXISTS base_url VARCHAR"))
|
||||
conn.execute(text("ALTER TABLE llm_configs ADD COLUMN IF NOT EXISTS api_key VARCHAR"))
|
||||
conn.execute(text("ALTER TABLE llm_configs ADD COLUMN IF NOT EXISTS is_default BOOLEAN DEFAULT FALSE"))
|
||||
try:
|
||||
conn.execute(text("ALTER TABLE articles ADD COLUMN IF NOT EXISTS images JSON DEFAULT '{}'::json"))
|
||||
except Exception:
|
||||
|
||||
@@ -35,7 +35,7 @@ def import_initial_data():
|
||||
db.commit()
|
||||
print(f"✅ 创建默认管理员: {DEFAULT_ADMIN_USERNAME}")
|
||||
|
||||
# 补充或更新 LLM 供应商配置(opencode-go 为主,nvidia 为备)
|
||||
# 补充或更新 LLM 供应商配置(nvidia 为主,opencode-go 为备)
|
||||
expected = {
|
||||
"opencode-go": dict(provider="opencode-go", model="deepseek-v4-flash",
|
||||
base_url="https://opencode.ai/zen/go/v1", temperature=0.7, max_tokens=131072, is_active=True,
|
||||
@@ -74,11 +74,10 @@ def import_initial_data():
|
||||
# 初始化默认搜索 API 提供商
|
||||
if db.query(SearchProvider).count() == 0:
|
||||
providers = [
|
||||
SearchProvider(name="百度千帆", provider_type="baidu", api_key="", api_url="https://qianfan.baidubce.com/v2/ai_search/web_search", console_url="https://console.bce.baidu.com/qianfan/ais/console/onlineService", priority=1, enabled=True, daily_limit=50),
|
||||
SearchProvider(name="opencode云搜索", provider_type="mcp", api_key="", api_url="", console_url="https://opencode.ai", priority=2, enabled=True, daily_limit=99999),
|
||||
SearchProvider(name="360搜索", provider_type="360", api_key="", api_url="", console_url="https://www.so.com", priority=3, enabled=True, daily_limit=200),
|
||||
SearchProvider(name="搜狗搜索", provider_type="sogou", api_key="", api_url="", console_url="https://sogou.com", priority=4, enabled=True, daily_limit=200),
|
||||
SearchProvider(name="微信搜一搜", provider_type="wechat", api_key="", api_url="", console_url="https://wx.sogou.com/weixin", priority=5, enabled=True, daily_limit=200),
|
||||
SearchProvider(name="百度千帆", provider_type="baidu", api_key="", api_url="https://qianfan.baidubce.com/v2/ai_search/web_search", console_url="https://console.bce.baidu.com/qianfan/ais/console/onlineService", priority=1, enabled=True, daily_limit=200),
|
||||
SearchProvider(name="360搜索", provider_type="360", api_key="", api_url="", console_url="https://www.so.com", priority=0, enabled=True, daily_limit=99999),
|
||||
SearchProvider(name="搜狗搜索", provider_type="sogou", api_key="", api_url="", console_url="https://sogou.com", priority=1, enabled=True, daily_limit=99999),
|
||||
SearchProvider(name="微信搜一搜", provider_type="wechat", api_key="", api_url="", console_url="https://wx.sogou.com/weixin", priority=2, enabled=True, daily_limit=99999),
|
||||
]
|
||||
for p in providers:
|
||||
db.add(p)
|
||||
|
||||
@@ -627,6 +627,7 @@ class LLMConfig(Base):
|
||||
base_url = Column(String, nullable=True)
|
||||
api_key = Column(String, nullable=True)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_default = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||
|
||||
@@ -643,6 +644,7 @@ class LLMConfig(Base):
|
||||
"base_url": self.base_url,
|
||||
"api_key": f"{self.api_key[:8]}..." if self.api_key else None,
|
||||
"is_active": self.is_active,
|
||||
"is_default": self.is_default,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
@@ -522,6 +522,7 @@ class LLMConfigBase(BaseModel):
|
||||
base_url: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
is_active: bool = True
|
||||
is_default: bool = False
|
||||
|
||||
|
||||
class LLMConfigResponse(LLMConfigBase):
|
||||
|
||||
@@ -136,10 +136,15 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="temperature" label="温度" width="70"></el-table-column>
|
||||
<el-table-column prop="max_tokens" label="最大Token" width="100"></el-table-column>
|
||||
<el-table-column prop="is_active" label="状态" width="90">
|
||||
<el-table-column prop="is_active" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.is_active" type="success" size="small" effect="dark">当前使用</el-tag>
|
||||
<el-tag v-else type="info" size="small">未启用</el-tag>
|
||||
<el-tag v-if="scope.row.is_active" type="success" size="small" effect="dark">启用</el-tag>
|
||||
<el-tag v-else type="info" size="small">停用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_default" label="默认" width="70">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.is_default" type="warning" size="small" effect="dark">默认</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="系统提示词" min-width="150">
|
||||
@@ -556,7 +561,6 @@
|
||||
<el-col :span="12"> <el-form-item label="类型">
|
||||
<el-select v-model="searchProviderForm.provider_type" style="width:100%">
|
||||
<el-option label="百度千帆" value="baidu"></el-option>
|
||||
<el-option label="opencode云搜索" value="mcp"></el-option>
|
||||
<el-option label="360搜索" value="360"></el-option>
|
||||
<el-option label="搜狗搜索" value="sogou"></el-option>
|
||||
<el-option label="微信搜一搜" value="wechat"></el-option>
|
||||
@@ -623,7 +627,8 @@
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12"><el-form-item label="最大Token"><el-input-number v-model="llmConfigForm.max_tokens" :min="1" :max="999999" style="width:100%"/></el-form-item></el-col>
|
||||
<el-col :span="12"><el-form-item label="激活"><el-switch v-model="llmConfigForm.is_active"/></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="激活"><el-switch v-model="llmConfigForm.is_active"/></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="默认"><el-switch v-model="llmConfigForm.is_default"/></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="24"><el-form-item label="系统提示词"><el-input type="textarea" v-model="llmConfigForm.system_prompt" :rows="3" placeholder="你是一个专业的内容创作助手。"/></el-form-item></el-col>
|
||||
@@ -706,7 +711,7 @@ const llmConfigs = ref([]);
|
||||
const llmConfigsLoading = ref(false);
|
||||
const llmConfigDialogVisible = ref(false);
|
||||
const llmConfigDialogTitle = ref('新增 LLM 配置');
|
||||
const llmConfigForm = reactive({ id: null, name: '', system_prompt: '', user_prompt_template: '', temperature: 0.7, max_tokens: 131072, model: '', provider: 'opencode-go', base_url: '', api_key: '', is_active: true });
|
||||
const llmConfigForm = reactive({ id: null, name: '', system_prompt: '', user_prompt_template: '', temperature: 0.7, max_tokens: 131072, model: '', provider: 'opencode-go', base_url: '', api_key: '', is_active: true, is_default: false });
|
||||
const editingLLMConfigId = ref(null);
|
||||
const loadLLMConfigs = async () => {
|
||||
llmConfigsLoading.value = true;
|
||||
@@ -721,7 +726,7 @@ const llmConfigs = ref([]);
|
||||
llmConfigDialogTitle.value = '新增配置'; editingLLMConfigId.value = null;
|
||||
llmConfigForm.id = null; llmConfigForm.name = ''; llmConfigForm.provider = 'sensenova'; llmConfigForm.model = 'deepseek-v4-flash';
|
||||
llmConfigForm.base_url = 'https://token.sensenova.cn/v1'; llmConfigForm.api_key = ''; llmConfigForm.temperature = 0.3;
|
||||
llmConfigForm.max_tokens = 4000; llmConfigForm.system_prompt = ''; llmConfigForm.user_prompt_template = ''; llmConfigForm.is_active = true;
|
||||
llmConfigForm.max_tokens = 4000; llmConfigForm.system_prompt = ''; llmConfigForm.user_prompt_template = ''; llmConfigForm.is_active = true; llmConfigForm.is_default = false;
|
||||
}
|
||||
llmConfigDialogVisible.value = true;
|
||||
};
|
||||
@@ -1101,12 +1106,12 @@ const llmConfigs = ref([]);
|
||||
});
|
||||
|
||||
const defaultProvider = computed(() => {
|
||||
const sorted = [...llmConfigs.value].filter(x => x.is_active).sort((a, b) => a.id - b.id);
|
||||
return sorted.length ? sorted[0].provider : '-';
|
||||
const d = [...llmConfigs.value].filter(x => x.is_default);
|
||||
return d.length ? d[0].provider : '-';
|
||||
});
|
||||
const defaultModel = computed(() => {
|
||||
const sorted = [...llmConfigs.value].filter(x => x.is_active).sort((a, b) => a.id - b.id);
|
||||
return sorted.length ? sorted[0].model : '-';
|
||||
const d = [...llmConfigs.value].filter(x => x.is_default);
|
||||
return d.length ? d[0].model : '-';
|
||||
});
|
||||
const complianceProvider = computed(() => {
|
||||
const c = [...llmConfigs.value].filter(x => x.is_active && x.model === 'deepseek-v4-flash');
|
||||
|
||||
@@ -384,7 +384,6 @@
|
||||
this.runningModule = modId;
|
||||
const endpoints = {
|
||||
scheduled_collect: '/api/system/collect/run',
|
||||
scheduled_refresh_search_cache: '/api/system/refresh-search-cache/run',
|
||||
scheduled_fetch_trends: '/api/system/trends/run',
|
||||
scheduled_generate: '/api/system/generate/run',
|
||||
scheduled_optimize: '/api/system/review/run',
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
<div><span>最后运行</span><span>{{ mod.last_run || '从未' }}<span v-if="mod.last_status" :style="{marginLeft:'6px',padding:'1px 6px',borderRadius:'8px',fontSize:'11px',fontWeight:500}"><span v-if="mod.last_status==='success'" style="color:#67c23a;">✅成功</span><span v-else-if="mod.last_status==='failed'" style="color:#f56c6c;">❌失败</span><span v-else-if="mod.last_status==='running'" style="color:#e6a23c;">⏳运行中</span></span></span></div>
|
||||
<div><span>下次运行</span><span>{{ mod.next_run || '—' }}</span></div>
|
||||
<div><span>累计运行</span><span>{{ mod.total_runs }} 次 <span style="color:#67c23a;">{{ mod.success_runs }} 成功</span> <span style="color:#f56c6c;">{{ mod.failed_runs }} 失败</span><span v-if="mod.running > 0" style="color:#e6a23c;"> {{ mod.running }} 运行中</span></span></div>
|
||||
<div><span>LLM 模型</span><span>{{ (mod.params && mod.params.llm_provider) || (defaultLlmLabel || '默认') }}</span></div>
|
||||
<div style="margin-top:10px; border-bottom:none;">
|
||||
<el-button size="small" type="primary" @click.stop="triggerModule(mod.module_id)" :loading="runningModule === mod.module_id" :disabled="!mod.enabled">立即运行</el-button>
|
||||
<el-button size="small" @click.stop="openModuleDetail(mod)">查看详情</el-button>
|
||||
@@ -346,10 +347,17 @@
|
||||
<el-input v-model="drawerData.schedule" size="small" style="width:120px;" placeholder="HH:MM" :disabled="savingConfig"></el-input>
|
||||
<span style="font-size:12px;color:#909399;">每日执行时间(HH:MM)</span>
|
||||
</div>
|
||||
<div style="display:flex;gap:16px;align-items:center;margin-bottom:12px;">
|
||||
<span style="font-size:13px;color:#606266;width:60px;">LLM</span>
|
||||
<el-select v-model="drawerData.params.llm_provider" size="small" style="width:200px;" clearable placeholder="系统默认" @change="saveModuleConfig" :disabled="savingConfig">
|
||||
<el-option v-for="c in llmConfigs.filter(x=>x.is_active)" :key="c.id" :label="c.provider + ' (' + c.model + ')' + (c.is_default ? ' (默认)' : '')" :value="c.provider"/>
|
||||
</el-select>
|
||||
<span style="font-size:12px;color:#909399;">留空则使用系统默认模型</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="Object.keys(drawerData.params || {}).length > 0">
|
||||
<div v-if="Object.keys(drawerData.params || {}).filter(k => k !== 'llm_provider').length > 0">
|
||||
<div style="font-size:13px;font-weight:600;margin-bottom:12px;">参数配置</div>
|
||||
<div v-for="(val, key) in drawerData.params" :key="key" style="margin-bottom:12px;display:flex;align-items:center;gap:12px;">
|
||||
<div v-for="(val, key) in drawerData.params" v-if="key !== 'llm_provider'" :key="key" style="margin-bottom:12px;display:flex;align-items:center;gap:12px;">
|
||||
<span style="font-size:13px;color:#606266;width:120px;">{{ key }}</span>
|
||||
<el-input v-if="typeof val === 'string'" v-model="drawerData.params[key]" size="small" style="flex:1;" @change="saveModuleConfig" :disabled="savingConfig"></el-input>
|
||||
<el-input-number v-else-if="typeof val === 'number'" v-model="drawerData.params[key]" size="small" :disabled="savingConfig" @change="saveModuleConfig"></el-input-number>
|
||||
@@ -552,13 +560,11 @@ const TasksApp = {
|
||||
'scheduled_optimize': { icon: 'IconSearch', name: '合规审查', defaultTime: '04:30' },
|
||||
'scheduled_optimize_sources': { icon: 'IconSetting', name: '信息源优化', defaultTime: '05:00' },
|
||||
'scheduled_metrics_sync': { icon: 'IconDashboard', name: '指标同步', defaultTime: '06:00' },
|
||||
'scheduled_refresh_search_cache': { icon: 'IconRefresh', name: '搜索缓存', defaultTime: '01:00' },
|
||||
'scheduled_fetch_trends': { icon: 'IconRefresh', name: '热点趋势', defaultTime: '01:10' },
|
||||
'scheduled_task_monitor': { icon: 'IconRefresh', name: '任务监控', defaultTime: '*' },
|
||||
};
|
||||
const MODULE_TRIGGER_ENDPOINTS = {
|
||||
scheduled_collect: '/api/system/collect/run',
|
||||
scheduled_refresh_search_cache: '/api/system/refresh-search-cache/run',
|
||||
scheduled_fetch_trends: '/api/system/trends/run',
|
||||
scheduled_generate: '/api/system/generate/run',
|
||||
scheduled_optimize: '/api/system/review/run',
|
||||
@@ -592,6 +598,7 @@ const TasksApp = {
|
||||
sourceForm: { name: '', source_type: 'web_search', query: '', credibility: 'medium', focus: '', sort_order: 0, is_active: true },
|
||||
sourcePage: 1, sourcePageSize: 10,
|
||||
SCHEDULER_JOBS, MODULE_TRIGGER_ENDPOINTS,
|
||||
llmConfigs: [], defaultLlmLabel: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -645,17 +652,28 @@ const TasksApp = {
|
||||
async loadModules() {
|
||||
this.moduleLoading = true;
|
||||
try {
|
||||
const data = await this.api('/api/system/modules/status');
|
||||
const [data, llmConfigs] = await Promise.all([
|
||||
this.api('/api/system/modules/status'),
|
||||
this.loadLLMConfigs(),
|
||||
]);
|
||||
if (!data) return;
|
||||
this.modules = data.modules || [];
|
||||
this.schedulerRunning = data.scheduler && data.scheduler.running === true;
|
||||
} catch (e) { console.error(e); }
|
||||
finally { this.moduleLoading = false; }
|
||||
},
|
||||
async loadLLMConfigs() {
|
||||
try {
|
||||
const configs = await this.api('/api/admin/llmconfigs');
|
||||
this.llmConfigs = configs || [];
|
||||
const def = (configs || []).find(c => c.is_default);
|
||||
this.defaultLlmLabel = def ? def.provider + ' (' + def.model + ')' : '';
|
||||
} catch (e) { console.error('loadLLMConfigs error:', e); this.llmConfigs = []; }
|
||||
},
|
||||
async openModuleDetail(mod) {
|
||||
this.showDrawer = true;
|
||||
this.drawerTitle = mod.title + ' 详情';
|
||||
this.drawerData = { ...mod };
|
||||
this.drawerData = { ...mod, params: { ...(mod.params || {}) } };
|
||||
this.drawerError = '';
|
||||
this.drawerLoading = true;
|
||||
this.drawerTab = 'inputs';
|
||||
@@ -667,7 +685,7 @@ const TasksApp = {
|
||||
this.api('/api/admin/task-configs/history/' + mod.module_id + '?limit=20'),
|
||||
this.api('/api/admin/prompt-configs?module_id=' + mod.module_id),
|
||||
]);
|
||||
this.drawerData = { ...mod, ...detail };
|
||||
this.drawerData = { ...mod, ...detail, params: { ...((detail.params || mod.params || {})) } };
|
||||
this.drawerHistory = history || [];
|
||||
this.drawerPrompts = prompts || [];
|
||||
} catch (e) { this.drawerError = e.message; }
|
||||
|
||||
Reference in New Issue
Block a user