diff --git a/.env b/.env index 2903281..c56d78c 100644 --- a/.env +++ b/.env @@ -1,6 +1,6 @@ APP_NAME=ConnectHub DATA_DIR=/data -DB_URL=sqlite:////data/connecthub.db +DB_URL=postgresql+psycopg://connecthub:connecthub_pwd_change_me@postgres:5432/connecthub REDIS_URL=redis://redis:6379/0 FERNET_KEY_PATH=/data/fernet.key DEV_MODE=1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..640204f --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.db +*.log +pgdata/ +__pycache__/ +*.pyc \ No newline at end of file diff --git a/README.md b/README.md index 599c7cc..48b7dc0 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ ConnectHub 是一个轻量级企业集成中间件:统一管理多系统集成 - `env.example`:环境变量示例(由于环境限制,仓库中使用该文件名;本地运行时请手动创建 `.env` 并参考此文件) - 关键变量: - `DATA_DIR=/data`:容器内数据目录 - - `DB_URL=sqlite:////data/connecthub.db`:SQLite DB 文件 + - `DB_URL=postgresql+psycopg://connecthub:connecthub_pwd_change_me@postgres:5432/connecthub`:PostgreSQL 连接串(容器内通过 service name `postgres` 访问) - `REDIS_URL=redis://redis:6379/0`:Celery Broker/Backend - - `FERNET_KEY_PATH=/data/fernet.key`:Fernet key 文件(自动生成并持久化) + - `FERNET_KEY_PATH=/data/fernet.key`:Fernet key 文件(自动生成并持久化;**正式环境必须保留同一个 key,否则历史 secret_cfg 将无法解密**) - `LOG_DIR=/data/logs`:日志目录(可选) ### 核心框架实现要点 @@ -67,6 +67,44 @@ ConnectHub 是一个轻量级企业集成中间件:统一管理多系统集成 - 位置:`app/integrations/base.py` - 规范:业务 Job 禁止直接写 HTTP;必须通过 Client 访问外部系统(统一超时、重试、日志)。 +#### SeeyonClient(致远 OA) + +- 位置:`app/integrations/seeyon.py` +- 认证方式:`POST /seeyon/rest/token` 获取 `id` 作为 token,并在业务请求 header 中携带 `token: `(参考:[调用Rest接口](https://open.seeyoncloud.com/seeyonapi/781/))。 +- 最小配置示例: + - `public_cfg`: + +```json +{"base_url":"https://oa.example.com"} +``` + + - `secret_cfg`(会被加密落库): + +```json +{"rest_user":"REST帐号","rest_password":"REST密码","loginName":"可选-模拟登录名"} +``` + +- 注意:在 Admin 中保存 `public_cfg/secret_cfg` 时必须输入 **合法 JSON 对象(双引号、且为 `{...}`)**,否则会直接报错并阻止落库。 + +- token 失效处理:遇到 401 或响应包含 `Invalid token`,自动刷新 token 并重试一次。 + +#### 示例插件:sync_oa_to_didi(仅演示 token 获取日志) + +- 插件 Job:`extensions/sync_oa_to_didi/job.py` 的 `SyncOAToDidiTokenJob` +- 在 Admin 创建 Job 时可使用: + - `handler_path`: `extensions.sync_oa_to_didi.job:SyncOAToDidiTokenJob` + - `public_cfg`: + +```json +{"base_url":"https://oa.example.com"} +``` + + - `secret_cfg`(会被加密落库): + +```json +{"rest_user":"REST帐号","rest_password":"REST密码","loginName":"可选-模拟登录名"} +``` + #### Security(Fernet 加解密) - 位置:`app/security/fernet.py` diff --git a/app/admin/routes.py b/app/admin/routes.py index 27a3ff2..9eee2c5 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -1,15 +1,23 @@ from __future__ import annotations +from datetime import datetime +from urllib.parse import quote_plus + from fastapi import APIRouter, HTTPException, Request from starlette.responses import RedirectResponse from app.db import crud from app.db.engine import get_session +from app.db.models import JobStatus from app.tasks.execute import execute_job router = APIRouter() +def _redirect_with_error(referer: str, msg: str) -> RedirectResponse: + sep = "&" if "?" in referer else "?" + return RedirectResponse(f"{referer}{sep}error={quote_plus(msg)}", status_code=303) + @router.post("/admin/joblogs/{log_id}/retry") def retry_joblog(request: Request, log_id: int): @@ -18,20 +26,68 @@ def retry_joblog(request: Request, log_id: int): log = crud.get_job_log(session, log_id) if not log: raise HTTPException(status_code=404, detail="JobLog not found") - # 关键:用 snapshot_params 重新触发任务(其中 secret_cfg 仍为密文) - execute_job.delay(snapshot_params=log.snapshot_params) + if log.status == JobStatus.RUNNING: + referer = request.headers.get("Referer") or str(request.url_for("admin:details", identity="joblog", pk=str(log_id))) + return _redirect_with_error(referer, "该任务日志正在运行中,请结束后再重试。") + + # 创建新的 RUNNING JobLog,并跳转到该条详情页 + snapshot = dict(log.snapshot_params or {}) + meta = dict(snapshot.get("meta") or {}) + meta["trigger"] = "retry" + meta["started_at"] = datetime.utcnow().isoformat() + snapshot["meta"] = meta + + new_log = crud.create_job_log( + session, + job_id=str(log.job_id), + status=JobStatus.RUNNING, + snapshot_params=snapshot, + message="运行中", + traceback="", + run_log="", + celery_task_id="", + attempt=0, + started_at=datetime.utcnow(), + finished_at=None, + ) + execute_job.delay(snapshot_params=snapshot, log_id=int(new_log.id)) + url = request.url_for("admin:details", identity="job-log", pk=str(new_log.id)) + return RedirectResponse(url, status_code=303) finally: session.close() - referer = request.headers.get("Referer") or "/admin" - return RedirectResponse(referer, status_code=303) - - @router.post("/admin/jobs/{job_id}/run") def run_job(request: Request, job_id: str): - # 触发一次立即执行 - execute_job.delay(job_id=job_id) - referer = request.headers.get("Referer") or "/admin" - return RedirectResponse(referer, status_code=303) + session = get_session() + try: + job = crud.get_job(session, job_id) + if not job: + raise HTTPException(status_code=404, detail="Job not found") + + snapshot = { + "job_id": job.id, + "handler_path": job.handler_path, + "public_cfg": job.public_cfg or {}, + "secret_cfg": job.secret_cfg or "", + "meta": {"trigger": "run_now", "started_at": datetime.utcnow().isoformat()}, + } + new_log = crud.create_job_log( + session, + job_id=str(job.id), + status=JobStatus.RUNNING, + snapshot_params=snapshot, + message="运行中", + traceback="", + run_log="", + celery_task_id="", + attempt=0, + started_at=datetime.utcnow(), + finished_at=None, + ) + execute_job.delay(job_id=job.id, log_id=int(new_log.id)) + url = request.url_for("admin:details", identity="job-log", pk=str(new_log.id)) + return RedirectResponse(url, status_code=303) + finally: + session.close() diff --git a/app/admin/templates/job_edit.html b/app/admin/templates/job_edit.html new file mode 100644 index 0000000..6a4cc27 --- /dev/null +++ b/app/admin/templates/job_edit.html @@ -0,0 +1,44 @@ +{% extends "sqladmin/edit.html" %} + +{% block content %} + {{ super() }} + +
+
+
+ +
+ +
+ 出于安全考虑,编辑页不回显历史密文。留空表示不修改;填写 JSON 对象将覆盖原值并重新加密保存。 +
+
+
+
+
+{% endblock %} + +{% block tail %} + {{ super() }} + +{% endblock %} + diff --git a/app/admin/templates/job_list.html b/app/admin/templates/job_list.html index 7152902..e9f41e1 100644 --- a/app/admin/templates/job_list.html +++ b/app/admin/templates/job_list.html @@ -22,7 +22,7 @@