From e55619b6325f5b417abc1532de11b62a6c991be7 Mon Sep 17 00:00:00 2001 From: Marsway Date: Mon, 5 Jan 2026 17:01:26 +0800 Subject: [PATCH] new job --- README.md | 36 ++++++++ app/integrations/__init__.py | 3 +- .../__pycache__/__init__.cpython-312.pyc | Bin 255 -> 324 bytes .../__pycache__/base.cpython-312.pyc | Bin 3751 -> 3940 bytes .../__pycache__/seeyon.cpython-312.pyc | Bin 0 -> 3689 bytes app/integrations/base.py | 4 +- app/integrations/seeyon.py | 80 ++++++++++++++++++ data/logs/connecthub.log | 25 ++++++ extensions/sync_oa_to_didi/__init__.py | 3 + .../__pycache__/job.cpython-312.pyc | Bin 0 -> 2749 bytes extensions/sync_oa_to_didi/job.py | 58 +++++++++++++ 11 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 app/integrations/__pycache__/seeyon.cpython-312.pyc create mode 100644 app/integrations/seeyon.py create mode 100644 extensions/sync_oa_to_didi/__init__.py create mode 100644 extensions/sync_oa_to_didi/__pycache__/job.cpython-312.pyc create mode 100644 extensions/sync_oa_to_didi/job.py diff --git a/README.md b/README.md index 06f2d4c..150e728 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,42 @@ 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":"可选-模拟登录名"} +``` + +- 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/integrations/__init__.py b/app/integrations/__init__.py index 62b5580..ebbf4bc 100644 --- a/app/integrations/__init__.py +++ b/app/integrations/__init__.py @@ -1,5 +1,6 @@ """系统集成适配器""" from app.integrations.base import BaseClient +from app.integrations.seeyon import SeeyonClient -__all__ = ["BaseClient"] \ No newline at end of file +__all__ = ["BaseClient", "SeeyonClient"] \ No newline at end of file diff --git a/app/integrations/__pycache__/__init__.cpython-312.pyc b/app/integrations/__pycache__/__init__.cpython-312.pyc index 17af24df7567497c2292302ed4b9453798163f9a..e5d50bca5aee283602084df87ba2db75349d5350 100644 GIT binary patch delta 177 zcmey*c!Y`fG%qg~0}$|6MQ1LV$SbL*0pv_)NMVR#NMTH2%3;i9ieh2}v6*w2b6KKT zfNYi&)^w&Q)=D-__K9w~vYL#yc!E@m{D(V0L delta 111 zcmX@Y^q-OUG%qg~0}xy=iq6cK$SbKJ1>{U;NMVR#NMTH2%3;i9ieh49NMTNAjAE{2 z(PW(%qRUal3{>Q&$ue=C+Qdf|Y9Ip`fw))%NPJ*sWMurv#=t1^ltJeKw|IwMBYP1C GPyzraxE84Z diff --git a/app/integrations/__pycache__/base.cpython-312.pyc b/app/integrations/__pycache__/base.cpython-312.pyc index c1feeb483c3470d825ada835ff69849273525a4a..3223a0dae4ff4234f8423ddc42dfbcfcb88afc51 100644 GIT binary patch delta 1069 zcmZ`&O=uHA6rS15Ci|0(ZKe4m4gHr0CZ$43s`MvqJZM|c>Y+%L*j-H4wh6NWT{D76-r`fH^Y41&JXv^<&=P8RuWoI1Ox2Dw8}kISA6AC}06K zr2z>$+`cu4g*CVcHBhwE$6-0DWI#R&9>2yHsJj8Hv2iea3xJ|10ZSo2gaFyHi!DWo z0~i?2y)>%G6E4uMNf=@qwrjTgJmw$48?Y!_%{5XzzY|LZI-4dGgnFZ*Vztz$M(b^d zuSYiAwLo2CWrHR$fdI>fkK9!JDQ3>mIj4aQV8=D?I4D8vD8VjZ`Kx3Ax&fLkImC9&k{xX`coRGJUNl$ZG zHmxMG%v{<^mHBKYTRrO)O{kK4Vlb%cc?%`ar}U(zBeU#U(9uO*tNG-a*ym-<+8YJ2 z9iSfaM-0Hz#4iO8d4sF+iu^FPuD$mje&>yCqGk2N(1{)2;N9`niIoXztWWOx26s-L zS$2^DDb}QKMxUFnr*=L1&N+=DDM~N9+G*Vv9dTtW%56ubL_pXc9&Jf@gja6J&?6)Q zWKN#<_E28gW|*195RY2f%Y+$3$QL;RT)39W+XSD1WP`(QT-xH{kgGUV!n6C~*x*cXGx L;7`C1d1=O9-yqSx delta 999 zcmZ`&O-vI(6n?XR-5+*QghEIy6wp}23Lf~0e-#>o#(IzwF(fuDS_)D!yQZX>0_h18 zgXkQ%7~_FN6E5|nM-^_KOgL!IUc4E-n0Rq!*L54C@9^fEm+yV=XELAT&yQN~O;ZPE zTdAA#@6?ypu&m|}ZA)c2ELqW9 zD@Z6D4p4a7GA}^XLQ5D65Ycv-5;p{l(Z(Ac<#1L&Fa;72d0n^)RYc?}>IFCIlL7hw z&s21xv;F)j{y5UQW-DGX#or3i8|0OL?{9%E9rrg(+8NXcPnH=AZvnC+L@Bd%HZ0>T zn`0W-v+1k)gCEu3sDw(eB2GgICAI6AZ}$s-uB1>`?#{XSqFop7;YB*2X3gfh?zlPZ zI;D9xPq$PjZs0b?#c}59(sItt*Oi4*4&QNb7Yos}HW)P+=nf6@E#W@8p~d38tk61q zjCO7p9~Ga?9z^@^+qFb3QyZ@78zTLxjhWfn@N4Hod-lz3=IEHd-@}Dpjq4MJuxDtQ zA$hO$QcGq)-am=BACNOcwA)zh=wr0HRB(z*Nzrfe6fc~jJH~00rU%B;*eDN-Fqf1X z7+y3*H=E9&3Hr8a$~eYp{5RkWw96b^yU1ahxuoh1j?eRer|zK?zw*C;IkRl#U3YmU zbz{z1E-pE9N#(y(KZ(CEQx5gKhCDq_S@EUmfjMsQA@LY@=V-#J%17k(@BQ?JHHdoY cCo6%D)1Oukx6_U- z8`HGvSe|(^@6GI+_wk!|f3K);BWObQhp}xy-6NB7<1K<&z(9;61u0Ad4KO_nLufX^ z_OLKI5{?0`hqKrC9v;@XgfQUjaWaTSDjN@!4_q1*eu}YXJuVh~2PwiSq&QXQl*3+i zXV|d#h)lwa>4|7s(uf$bq^9qMxuZ+Vz~J5<(bXM^n5r4srf5>r)Tq&)KD;ib8EPN4 z#MkMnnn`L=>sYJjtbtkhE)b{TW-97o6a=?ndK?O?I4&}Zm`A;g!kzN|lrJ%Zw|^zf6b z4}N!2>Q^I*is382nL9K2VC+wG*M4bjvV|Z0k2V z53krubASB9qtj;}e)7TnYaT*GN49n$01@y;x71S7JdZ82h2D! zV6@sOpF?LMgmTPfX3E~fpd6b+GFz6(IbdeH93(lI+0H=_VlcBq5TOhTvqTT^F!)?f z<3w06c|Dy{u_?-OOp6(^Y`Tc?oem%!1>=qC+h#6?34qX9`aVQ*cdH549vX`Ax z_Nl{cj*WZD(o5r%O%6K@Vr<%Sj<~-}FO7<*xTb9VhZvm&OmUS%XPC=zn9p&F`^ai= zmd5~1tfqL%GA9`tLas8$2$iXLO_^F66>oVTge3?0Td_CHnj#4w66dD4JbE~irrF?s z=4mzn(UFKnDd2R(^`r*J2wl;rnliH99Z5hV&1gTENvW{T5x(iPQdT$Fm|~(V+uoUN zvm!`}q>X;5Hxf&zinQ9o0m#>Zn6AgPJ}HT%_RerQ-0Yl+uB%6N=9puK{+QyH4ANfkPjdyC- z6l>Q!;T@7Y&&`X-8#uT1%+`|B`YYchUw-FUL(#kP3jqbHMh8bvl)S-`S1PTjDm8ab z@A!T(^v=8s1)3KSA0WHEC=fcA8O^+VJkQMrD#vz??=I{MOlZ}u(AvqCnLvA< z|EE86o}UO^sF)2lO*g+a6MTEx`}W=H21riNtmr%E8TCwu+HQ+$Oa4&aiz`9nWr<0` zl_us^ApTogLNEs)$WVL{>p<9U<@A66WFP>kBk(K03^UaJI>AoKGOk+&$5|+rtyyN; zCB`>ET(5_21eN@i=Z=pazvb^duiRG<8mkoz^X$*M%fG1Pin9U9il^0QZQ4mUn7+RjnFt<+}hBVIc7QDES;VkbOb;FARA(vxvY%e^!M=td`PYLF@4B<$RaNtobOq-%sOR)CLZE0)y= z*$p->*-&KF4{If?_5vXE6HJO|3QP^0p#Y^t)vynT;Wz+`Sky2bsbq=*5hf{b^8JR9 z8Z!O64<6j#4K-a_r}$%v1Ye-OQ%%lLhYZuTYlr|SlzgT~r*d*M31!YpMB5h95yT36 zEQ}HcRHHwsm|QAi^qazw!3gftt(%cMnv9BD$sY0;n*zPd1`EFkMABEDb|W;;sHWM_ zvIMh1^qU~AKZ6cns}u;0`wRY=z)N|4HdsA&Y&=`YPJ|~9%+##ERrA`F?K8nmd1t9X z$~(u%(?8Fxa0O>8YYUqSZ%n*4xv^Ne=UUBl<(|KM8>ngTo~N_^&>eqk(ck(x^1Fg} zD?{U(3!C#hOO;jmo%5`tqP|qqFrF!7-aj$1y}&)4t!(@pF=VyweQw?X)U&(($~*q% zqQ80K)m#2mlW$y8if!9x1J&c6f@ivM|Lwr{N=+?e+|NCwx|JU{f7Covw`NQzy&N8M zo_7`l(*2so!eHUV#IZ@EShMfi52kDO{Y?PsAAx-(?}~rTH-lQw^xbg%tHCZey5X+e zvEFfGrEf=@ip_^?y&|$Y# zhBd*N*6T$nT98qS6h2uuJX%st+MCo=Q-D}YW6k8q4Dgb81QXG)h+i$!c;g~P@DP&{ zg-T|9H2&5du%j*PBK4yN?>*FyZdo2kD(M8Fy|R2X9ZA?6rz|VUs4UZJkD@aTaVw-N z?3yvECK9p?uuUIElE~Yva3TnYNk^`w15%);r_d9Y=f!y+{LF|mUm>1<vYbH`O*bk$Ecd~3$FWkmQw*v2ryvCUs1GSY+p4are^i2wiq literal 0 HcmV?d00001 diff --git a/app/integrations/base.py b/app/integrations/base.py index bee6795..1e30afa 100644 --- a/app/integrations/base.py +++ b/app/integrations/base.py @@ -42,11 +42,13 @@ class BaseClient: def request(self, method: str, path: str, **kwargs: Any) -> httpx.Response: url = path if path.startswith("/") else f"/{path}" + extra_headers = kwargs.pop("headers", None) or {} + merged_headers = {**self.headers, **extra_headers} if extra_headers else None last_exc: Exception | None = None for attempt in range(self.retries + 1): try: start = time.time() - resp = self._client.request(method=method, url=url, **kwargs) + resp = self._client.request(method=method, url=url, headers=merged_headers, **kwargs) elapsed_ms = int((time.time() - start) * 1000) logger.info("HTTP %s %s -> %s (%sms)", method, url, resp.status_code, elapsed_ms) resp.raise_for_status() diff --git a/app/integrations/seeyon.py b/app/integrations/seeyon.py new file mode 100644 index 0000000..2056b75 --- /dev/null +++ b/app/integrations/seeyon.py @@ -0,0 +1,80 @@ +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 + + diff --git a/data/logs/connecthub.log b/data/logs/connecthub.log index 71cd989..aec76ad 100644 --- a/data/logs/connecthub.log +++ b/data/logs/connecthub.log @@ -1562,3 +1562,28 @@ sqlalchemy.exc.OperationalError: (sqlite3.OperationalError) table job_logs has n 2026-01-05 08:33:48 INFO celery.app.trace Task connecthub.dispatcher.tick[8cb1b138-e3e8-49ab-a292-ff17132f99b3] succeeded in 0.03212751899991417s: {'triggered': 0} 2026-01-05 08:34:48 INFO celery.app.trace Task connecthub.dispatcher.tick[75708a69-d07b-44eb-9fb6-8ef2a684f213] succeeded in 0.008938294999097707s: {'triggered': 0} 2026-01-05 08:35:48 INFO celery.app.trace Task connecthub.dispatcher.tick[5c5dbfe5-5cf0-4ddb-bdfb-382150fa7f06] succeeded in 0.021925684002781054s: {'triggered': 0} +2026-01-05 08:36:48 INFO celery.app.trace Task connecthub.dispatcher.tick[025cd0f2-5488-42b3-9cef-23ab24abbbee] succeeded in 0.009755851999216247s: {'triggered': 0} +2026-01-05 08:37:48 INFO celery.app.trace Task connecthub.dispatcher.tick[7ee0c72c-1ad1-4a45-9bc8-156f8fd1c804] succeeded in 0.002885051999328425s: {'triggered': 0} +2026-01-05 08:38:48 INFO celery.app.trace Task connecthub.dispatcher.tick[dc017644-0743-4b29-abf6-822f0e7e7641] succeeded in 0.011809330000687623s: {'triggered': 0} +2026-01-05 08:39:48 INFO celery.app.trace Task connecthub.dispatcher.tick[3e9e5624-aecf-40d7-8eea-6e39238e2a5b] succeeded in 0.048935678001726046s: {'triggered': 0} +2026-01-05 08:40:48 INFO celery.app.trace Task connecthub.dispatcher.tick[84ccbb27-cd52-4482-a489-ccef837921c0] succeeded in 0.002327479000086896s: {'triggered': 0} +2026-01-05 08:41:48 INFO celery.app.trace Task connecthub.dispatcher.tick[6d6737e4-01ab-4599-9181-7a3b72adf732] succeeded in 0.008618384999863338s: {'triggered': 0} +2026-01-05 08:42:48 INFO celery.app.trace Task connecthub.dispatcher.tick[eb711ffb-55fb-4a62-8529-994f25c972f5] succeeded in 0.007941265997942537s: {'triggered': 0} +2026-01-05 08:43:48 INFO celery.app.trace Task connecthub.dispatcher.tick[9000a6f2-4e12-461e-8975-c26168bd8878] succeeded in 0.004994874998374144s: {'triggered': 0} +2026-01-05 08:44:48 INFO celery.app.trace Task connecthub.dispatcher.tick[9d246e97-8b93-42c0-96e7-87ba2dc4bc49] succeeded in 0.030297451001388254s: {'triggered': 0} +2026-01-05 08:45:48 INFO celery.app.trace Task connecthub.dispatcher.tick[0409f390-9901-4772-9a6d-f0e4ba2c6bb5] succeeded in 0.03012071999910404s: {'triggered': 0} +2026-01-05 08:46:48 INFO celery.app.trace Task connecthub.dispatcher.tick[733fc7ba-8d96-4b31-bb96-3b8a495a3456] succeeded in 0.0390545119989838s: {'triggered': 0} +2026-01-05 08:47:48 INFO celery.app.trace Task connecthub.dispatcher.tick[42b214b8-7daf-4be4-95bf-f0a2c53743b0] succeeded in 0.010055696999188513s: {'triggered': 0} +2026-01-05 08:48:48 INFO celery.app.trace Task connecthub.dispatcher.tick[d6f5246a-7a4e-489a-91c7-f90d7e0e2ed0] succeeded in 0.010429486002976773s: {'triggered': 0} +2026-01-05 08:49:48 INFO celery.app.trace Task connecthub.dispatcher.tick[ba1e7651-a380-4f0f-a5c7-1bfafeb7b99a] succeeded in 0.015170816997851944s: {'triggered': 0} +2026-01-05 08:50:48 INFO celery.app.trace Task connecthub.dispatcher.tick[b23d74b4-5858-476e-985f-9dd395568793] succeeded in 0.013874788000975968s: {'triggered': 0} +2026-01-05 08:51:48 INFO celery.app.trace Task connecthub.dispatcher.tick[9edb6834-48e5-4e23-be0e-ff959fa5f99a] succeeded in 0.03663521199996467s: {'triggered': 0} +2026-01-05 08:52:48 INFO celery.app.trace Task connecthub.dispatcher.tick[57e5aa15-9c0f-4909-87b8-43ce16924b26] succeeded in 0.017162136999104405s: {'triggered': 0} +2026-01-05 08:53:48 INFO celery.app.trace Task connecthub.dispatcher.tick[f2ab541d-29e0-400a-ad19-0f91faad7374] succeeded in 0.017423885001335293s: {'triggered': 0} +2026-01-05 08:54:48 INFO celery.app.trace Task connecthub.dispatcher.tick[342c37cb-3061-4c79-981b-11e82cb5d6e8] succeeded in 0.005632615997456014s: {'triggered': 0} +2026-01-05 08:55:48 INFO celery.app.trace Task connecthub.dispatcher.tick[265d9d44-f002-47bc-b44c-529f854d8d84] succeeded in 0.015303112999390578s: {'triggered': 0} +2026-01-05 08:56:48 INFO celery.app.trace Task connecthub.dispatcher.tick[63483f4d-7e5f-4a67-80f3-f6c2f35ad8ad] succeeded in 0.009836078002990689s: {'triggered': 0} +2026-01-05 08:57:48 INFO celery.app.trace Task connecthub.dispatcher.tick[456f25f7-fc48-4317-b147-99ec4370c5ca] succeeded in 0.0016178609985217918s: {'triggered': 0} +2026-01-05 08:58:48 INFO celery.app.trace Task connecthub.dispatcher.tick[e54a515a-8966-4b43-849e-4d4a8bedacc5] succeeded in 0.016467269000713713s: {'triggered': 0} +2026-01-05 08:59:48 INFO celery.app.trace Task connecthub.dispatcher.tick[71bfd1ed-1b1d-45d2-9e44-08b12cbaf810] succeeded in 0.014499245000479277s: {'triggered': 0} +2026-01-05 09:00:48 INFO celery.app.trace Task connecthub.dispatcher.tick[55377f57-2326-46cf-aee6-3639b9285453] succeeded in 0.027614209000603296s: {'triggered': 0} diff --git a/extensions/sync_oa_to_didi/__init__.py b/extensions/sync_oa_to_didi/__init__.py new file mode 100644 index 0000000..339bcee --- /dev/null +++ b/extensions/sync_oa_to_didi/__init__.py @@ -0,0 +1,3 @@ +# + +"""OA 到滴滴的同步任务包""" diff --git a/extensions/sync_oa_to_didi/__pycache__/job.cpython-312.pyc b/extensions/sync_oa_to_didi/__pycache__/job.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c421819795027483e7c0a972a60db21326899cd0 GIT binary patch literal 2749 zcmbUjZEPIHb@p~|@7u=qId|AbS)4+AR&tJ7Agz@+jfsgwBBSDf=$T|$-)@|9_I8ii zy~aMRD>p>+!bf66f{*ZJ1gJ&kBR`O8l~9o2AAioiNH!}+YL&ZS|B5S92}MZZ&F<|v zSP@k_($2g$Z{ED`dHZ!JarA|<0Vm7pjByJT0w z1uHGnX(qu~dyMQ(vkBIM-Lfa`O?WBfLXvBw(YbVe71 z5`B_)hOSbBW3E_Wg&;A?3Vlq96|Em|dap7DOK?Dv##H4&IVCAN;GSMVlO9%w za`z-vMUj&FvFuP>8r3C5Bb;$dTs);3$*bTC&wTA$hLIxV8G#A)J zjU&Bw&Ona#qU(4ILYNw(4dNH31-%Ad0oW}f0>42Ipcvb4qLm=rm!ms7J6BkMR(`m7 z^Cq!CmK0drx_TV6BLcV^cdx73wL#Uc5p^h@88ZWXTF{Pz0l0r7 z?!e@u6Mah_fBybwa+8nFdm@YV;l;Y9$==D|ntJw)2!I!WD<;I7+BWSq1AlSJmf-s}Fq?sXft*RuAsjI#!d@4Q8Njq-CIQXtQP zlff>-{S!}l94*09KF5CsZ7_!0U_bEAF!tWK$A=8hNWeLRNGCOI z4a4h59rx*XIJ+ZF4z#|CKEr3Q2JPrUA%Z)RGHB8NIy^>Oh;BbC`ve-Le~(U3u|U7+ zb!rHEK~Qe7r^Gk` zOEn=e28Tfz+)SoTWQq{oerq>@>RTctZH>4N@=!`*%--L9kH*y$A&6rIz%kkk+LdXu zSl3#jVt$j>bZpYYl5V;IN@Yy{>@9_NE`)c_g?E?2d-A@e zXlr5rTL)e}@YniM+pp)_dS-h|(LD>%zPV^$DY`!&T5N8c8hmMRy0z52J@5Z!NB6mb zLd(niLUZR_bLZveZC@~X|G(;^Z-*?ghf9q|ioql0hP$RmXMa;XBoxypE;o$k*;NzBRRMeg2cn5>B;x<;l`=-)2C+q)V?b)6FreaAGr z4^uV+ISVC0QUl*frkDf8LPUw9SWHRxTqD#Zr z{<(^4)*qFf*&#v&RW+;TF=I!PVit4VQO%=4`3c7ohMgG=&WWm=m=W8jp_Lh7PPOxh%L&d(U`vrBFD(w6Z IBCB=#k1l<{Y5)KL literal 0 HcmV?d00001 diff --git a/extensions/sync_oa_to_didi/job.py b/extensions/sync_oa_to_didi/job.py new file mode 100644 index 0000000..5f9d6bd --- /dev/null +++ b/extensions/sync_oa_to_didi/job.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import logging +from typing import Any + +from app.integrations.seeyon import SeeyonClient +from app.jobs.base import BaseJob + + +logger = logging.getLogger("connecthub.extensions.sync_oa_to_didi") + + +def _mask_token(token: str) -> str: + token = token or "" + if len(token) <= 12: + return "***" + return f"{token[:6]}***{token[-4:]}" + + +class SyncOAToDidiTokenJob(BaseJob): + """ + 示例 Job:演示致远 OA 的 token 获取与日志记录(不做任何业务同步)。 + + public_cfg: + - base_url: "https://oa.example.com" + + secret_cfg (解密后): + - rest_user + - rest_password + - loginName (可选) + """ + + job_id = "sync_oa_to_didi.token_demo" + + def run(self, params: dict[str, Any], secrets: dict[str, Any]) -> dict[str, Any]: + base_url = str(params.get("base_url") or "").strip() + if not base_url: + raise ValueError("public_cfg.base_url is required") + + rest_user = str(secrets.get("rest_user") or "").strip() + rest_password = str(secrets.get("rest_password") or "").strip() + login_name = secrets.get("loginName") + login_name = str(login_name).strip() if login_name else None + + if not rest_user or not rest_password: + raise ValueError("secret_cfg.rest_user and secret_cfg.rest_password are required") + + client = SeeyonClient(base_url=base_url, rest_user=rest_user, rest_password=rest_password, loginName=login_name) + try: + token = client.authenticate() + finally: + client.close() + + masked = _mask_token(token) + logger.info("Seeyon token acquired (masked) token=%s loginName=%s base_url=%s", masked, login_name, base_url) + return {"token_masked": masked, "loginName": login_name or "", "base_url": base_url} + +