fix: preserve production task and auth integrations

This commit is contained in:
2026-08-19 16:23:39 +08:00
parent 2da7f4f328
commit ca8611113e
5 changed files with 326 additions and 65 deletions

92
app.py
View File

@@ -6,7 +6,9 @@ from flask import Flask, g, jsonify, redirect, request, send_file
from auth_service import (
build_dingtalk_login_url,
create_agent_access_token,
exchange_dingtalk_user,
get_user_by_agent_token,
get_user_by_session_token,
revoke_session,
upsert_user_and_session,
@@ -32,10 +34,12 @@ from task_service import (
TaskError,
cancel_task,
create_task,
delete_task,
get_file,
get_task,
get_task_extracted,
list_tasks,
merge_tasks,
recollect_task,
resume_task,
resume_unfinished_tasks,
@@ -50,12 +54,8 @@ def create_app() -> Flask:
if not Config.LOCAL_AUTH_BYPASS or request.remote_addr not in {"127.0.0.1", "::1"}:
return None
return {
"id": "local-dev",
"name": "本地开发用户",
"account": "local-dev",
"avatar": "",
"union_id": "",
"open_id": "",
"id": "local-dev", "name": "本地开发用户", "account": "local-dev",
"avatar": "", "union_id": "", "open_id": "",
}
@app.before_request
@@ -67,9 +67,16 @@ def create_app() -> Flask:
if request.path == "/health" or any(request.path.startswith(path) for path in open_paths):
return None
user = local_dev_user() or get_user_by_session_token(request.cookies.get(Config.SESSION_COOKIE_NAME))
if user is None:
authorization = request.headers.get("Authorization", "")
if authorization.lower().startswith("bearer "):
user = get_user_by_agent_token(authorization[7:].strip())
if user is None and request.path.startswith("/api/"):
return jsonify({
"error": {"code": "UNAUTHORIZED", "message": "请先通过钉钉授权登录"},
"error": {
"code": "UNAUTHORIZED",
"message": "登录或 AI 访问 Token 已失效,请重新登录后复制 Link AI",
},
"request_id": g.request_id,
}), 401
g.current_user = user
@@ -104,10 +111,14 @@ def create_app() -> Flask:
@app.route("/api/v1/auth/dingtalk/login", methods=["GET"])
def dingtalk_login():
return jsonify({
response = jsonify({
"data": build_dingtalk_login_url(request.args.get("redirect", "/tasks")),
"request_id": g.request_id,
})
# OAuth URL 带一次性 state且 scope/corpId 配置可能更新,禁止缓存。
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
response.headers["Pragma"] = "no-cache"
return response
@app.route("/api/v1/auth/dingtalk/callback", methods=["GET"])
def dingtalk_callback():
@@ -118,8 +129,13 @@ def create_app() -> Flask:
frontend_redirect = state.split(":", 1)[1] or frontend_redirect
if not code:
return redirect(f"{Config.FRONTEND_BASE_URL}/login?error={quote('钉钉授权缺少 code')}")
user_payload = exchange_dingtalk_user(code)
user, token, expires_at = upsert_user_and_session(user_payload)
try:
user_payload = exchange_dingtalk_user(code)
user, token, expires_at = upsert_user_and_session(user_payload)
except TaskError as exc:
return redirect(
f"{Config.FRONTEND_BASE_URL}/login?error={quote(exc.message)}"
)
response = redirect(f"{Config.FRONTEND_BASE_URL}{frontend_redirect}")
response.set_cookie(
Config.SESSION_COOKIE_NAME,
@@ -130,17 +146,6 @@ def create_app() -> Flask:
samesite=Config.SESSION_COOKIE_SAMESITE,
path="/",
)
# 给前端“Link AI”复制提示词使用。后端鉴权仍读取 hc_session
# 前端只读取这个辅助 Cookie 的 value并在提示词中拼成 hc_session=<value>。
response.set_cookie(
"hc_session_agent",
token,
expires=expires_at,
httponly=False,
secure=Config.SESSION_COOKIE_SECURE,
samesite=Config.SESSION_COOKIE_SAMESITE,
path="/",
)
app.logger.info("钉钉用户登录成功 user_id=%s name=%s", user["id"], user["name"])
return response
@@ -216,11 +221,34 @@ def create_app() -> Flask:
raise TaskError("UNAUTHORIZED", "请先通过钉钉授权登录", 401)
return jsonify({"data": {"user": user}, "request_id": g.request_id})
@app.route("/api/v1/auth/agent-token", methods=["POST"])
def auth_agent_token():
issued = create_agent_access_token(
request.cookies.get(Config.SESSION_COOKIE_NAME)
)
if not issued:
raise TaskError(
"UNAUTHORIZED",
"登录已失效,请重新完成钉钉登录后再复制 Link AI",
401,
)
token, expires_at = issued
response = jsonify({
"data": {
"token": token,
"expires_at": expires_at.isoformat(timespec="seconds"),
},
"request_id": g.request_id,
})
response.headers["Cache-Control"] = "no-store"
return response
@app.route("/api/v1/auth/logout", methods=["POST"])
def auth_logout():
revoke_session(request.cookies.get(Config.SESSION_COOKIE_NAME))
response = jsonify({"data": {"ok": True}, "request_id": g.request_id})
response.delete_cookie(Config.SESSION_COOKIE_NAME, path="/")
# 清理旧版本曾设置的可读辅助 Cookie。
response.delete_cookie("hc_session_agent", path="/")
return response
@@ -272,12 +300,27 @@ def create_app() -> Flask:
)
return jsonify({"data": result, "request_id": g.request_id})
@app.route("/api/v1/tasks/merge", methods=["POST"])
def merge_research_tasks():
payload = request.get_json(silent=True) or {}
task_ids = payload.get("task_ids")
if not isinstance(task_ids, list):
raise TaskError("MERGE_TASKS_REQUIRED", "task_ids 必须是任务 ID 数组", 422)
return jsonify({
"data": merge_tasks(task_ids),
"request_id": g.request_id,
})
@app.route("/api/v1/tasks/<task_id>", methods=["GET"])
def get_research_task(task_id: str):
if request.args.get("view") in {"extracted", "cleaned", "parsed"}:
return jsonify({"data": get_task_extracted(task_id), "request_id": g.request_id})
return jsonify({"data": get_task(task_id), "request_id": g.request_id})
@app.route("/api/v1/tasks/<task_id>", methods=["DELETE"])
def delete_research_task(task_id: str):
return jsonify({"data": delete_task(task_id), "request_id": g.request_id})
@app.route("/api/v1/tasks/<task_id>/extracted", methods=["GET"])
def get_research_task_extracted(task_id: str):
return jsonify({"data": get_task_extracted(task_id), "request_id": g.request_id})
@@ -297,8 +340,13 @@ def create_app() -> Flask:
@app.route("/api/v1/tasks/<task_id>/recollect", methods=["POST"])
def recollect_research_task(task_id: str):
current_user = getattr(g, "current_user", None) or {}
return jsonify({
"data": recollect_task(task_id),
"data": recollect_task(
task_id,
created_by_id=current_user.get("id"),
created_by_name=current_user.get("name"),
),
"request_id": g.request_id,
}), 201