from __future__ import annotations import logging from typing import Any import httpx from app.integrations.base import BaseClient logger = logging.getLogger("connecthub.integrations.seeyon") class SeeyonClient(BaseClient): """ 致远 OA REST Client: - POST /seeyon/rest/token 获取 token(id) - 业务请求 header 自动携带 token - 遇到 401/Invalid token 自动刷新 token 并重试一次 """ def __init__(self, *, base_url: str, rest_user: str, rest_password: str, loginName: str | None = None) -> None: super().__init__(base_url=base_url) self.rest_user = rest_user self.rest_password = rest_password self.loginName = loginName self._token: str | None = None def authenticate(self) -> str: body: dict[str, Any] = { "userName": self.rest_user, "password": self.rest_password, } if self.loginName: body["loginName"] = self.loginName # 文档:POST /seeyon/rest/token resp = super().request( "POST", "/seeyon/rest/token", json=body, headers={"Accept": "application/json", "Content-Type": "application/json"}, ) data = resp.json() token = str(data.get("id", "") or "") if not token or token == "-1": raise RuntimeError("Seeyon auth failed (token id missing or -1)") self._token = token logger.info("Seeyon token acquired") return token def _get_token(self) -> str: return self._token or self.authenticate() def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response: # type: ignore[override] token = self._get_token() headers = dict(kwargs.pop("headers", {}) or {}) headers["token"] = token try: return super().request(method, path, headers=headers, **kwargs) except httpx.HTTPStatusError as e: # token 失效:401 或返回包含 Invalid token resp = e.response text = "" try: text = resp.text or "" except Exception: text = "" if resp.status_code == 401 or ("Invalid token" in text): logger.info("Seeyon token invalid, refreshing and retrying once") self._token = None token2 = self._get_token() headers["token"] = token2 # 仅重试一次;仍失败则抛出 return super().request(method, path, headers=headers, **kwargs) raise def export_cap4_form_soap( self, *, templateCode: str, senderLoginName: str | None = None, rightId: str | None = None, doTrigger: str | bool | None = None, param: str | None = None, extra: dict[str, Any] | None = None, ) -> httpx.Response: """ 无流程表单导出(CAP4): POST /seeyon/rest/cap4/form/soap/export 返回 httpx.Response,调用方可自行读取 resp.text / resp.headers 等信息。 """ body: dict[str, Any] = {"templateCode": templateCode} if senderLoginName: body["senderLoginName"] = senderLoginName if rightId: body["rightId"] = rightId if doTrigger is not None: body["doTrigger"] = doTrigger if param is not None: body["param"] = param if extra: # 兜底扩展字段:仅当 key 不冲突时注入,避免覆盖已显式指定的参数 for k, v in extra.items(): if k not in body: body[k] = v return self.request( "POST", "/seeyon/rest/cap4/form/soap/export", json=body, headers={"Content-Type": "application/json"}, ) def batch_update_cap4_form_soap( self, *, formCode: str, loginName: str, rightId: str, dataList: list[dict[str, Any]], uniqueFiled: list[str] | None = None, doTrigger: bool | None = None, ) -> httpx.Response: """ 无流程批量更新: POST /seeyon/rest/cap4/form/soap/batch-update 参数对齐致远接口: - formCode/loginName/rightId/dataList 必填 - uniqueFiled/doTrigger 可选 """ form_code = str(formCode or "").strip() login_name = str(loginName or "").strip() right_id = str(rightId or "").strip() if not form_code: raise ValueError("formCode is required") if not login_name: raise ValueError("loginName is required") if not right_id: raise ValueError("rightId is required") if not isinstance(dataList, list) or len(dataList) == 0: raise ValueError("dataList is required and must be a non-empty list") if uniqueFiled is not None and not isinstance(uniqueFiled, list): raise ValueError("uniqueFiled must be a list if provided") body: dict[str, Any] = { "formCode": form_code, "loginName": login_name, "rightId": right_id, "dataList": dataList, } if uniqueFiled is not None: body["uniqueFiled"] = uniqueFiled if doTrigger is not None: body["doTrigger"] = doTrigger return self.request( "POST", "/seeyon/rest/cap4/form/soap/batch-update", json=body, headers={"Content-Type": "application/json"}, )