Przeglądaj źródła

单点待办skill

yuanzhi_kuang 1 tydzień temu
rodzic
commit
7dae3be3c2

+ 214 - 0
单点_待办skill/kdshrpmsg/SKILL.md

@@ -0,0 +1,214 @@
+---
+name: kdshrpmsg
+description: >-
+  Design and implement Kingdee EAS/SHR workflow todo and to-be-read message push
+  to external portals or Weaver/Fanwei via MessageWebServiceDao. Use for 待办, 待阅,
+  PORTAL-002/003/006/007/011/012/013, OAMessageWebServiceDao, WeaverMessageWebServiceDao,
+  BMCMessage, assignReadRecord, receiveOAConfig, BaoYPro, Weaver, ReceiveTodoRequestByJson,
+  weaverTodoConfig, pcurl entryPath, or ESB portal message integration on Kingdee SHR.
+  BEFORE coding: ask intake questions in this skill (channel, API, identity, deep link, config file).
+  When user says 上线/切生产/go-live: follow Go-live section with kdshrsso; update weaverTodoConfig host/credentials/shrServerName/sso.appId.
+---
+
+# Kingdee SHR Portal Message Push (待办 / 待阅)
+
+Reusable pattern from **YHPro** (玉湖,已测通) and **BaoYPro** (报业 / 泛微统一待办). Do not copy secrets; adapt per customer.
+
+Related SSO deep-link entry: skill **kdshrsso** (BaoYPro: Option B-qy `/qy/auth/weaver/app.do`).
+
+---
+
+## Agent intake — ask before coding (mandatory)
+
+When implementing **待办 / 待阅** for a new project or new channel, do **not** start coding until goals are confirmed. Ask in one batch. If SSO is in the same delivery, run **kdshrsso** intake first (or together).
+
+### Must confirm (block if unanswered)
+
+1. **渠道**: 泛微统一待办 / YH 门户 PORTAL-* / 钉钉 / 飞书 / 其他?**接口文档**是否已提供?
+2. **范围**: 只要待办,还是待办+待阅(BMC)?是否只要推送、SSO 已另做?
+3. **鉴权**: OpenAPI token / HTTP Basic / 其他?凭证放哪份 properties(勿与无关渠道混文件)?
+4. **人员**: 推送接收人字段 = 工号 / 手机 / 对方 userid?与 sHR 哪一列对齐?
+5. **幂等键**: 对方如何判重(如 Weaver:`syscode+flowid+workflow+receiver`;YH:`taskId`)?`flowid` 是否用 **procInstId**?
+6. **深链**: 绝对 URL 还是相对路径?SSO 入口(须与 kdshrsso 一致,如 `/qy/auth/weaver/app.do`)?审批页 path?
+7. **SPI**: 新建独立 `*MessageWebServiceDao`,还是 Router fan-out 多渠道?默认 **独立 Dao + enabled 开关**,少动已上线渠道。
+
+### Should confirm (sensible defaults)
+
+8. 配置命名:泛微继续 `weaverTodoConfig.properties`;钉钉/飞书用 `dingtalkTodoConfig` / `feishuTodoConfig`,**不要**改名合并成一个大文件。
+9. 状态机:待办/已办/删除/办结/待阅 与对方字段映射(Weaver `isremark` 等)——无文档则先按 weaver-baoy-plan 草案请用户改。
+10. CallLog / 失败补推是否要做;WF 延迟(如 `wf.delayMs`)经验值。
+11. 与 SSO 是否同期:深链入口必须已存在或同期交付(否则只推链接无法点开登录)。
+
+### Output before coding
+
+给出 **目标确认表**:渠道、配置文件名、SPI 策略、深链模板、人员字段、状态映射来源。用户确认后再改代码。用户说「按报业/按 BaoYPro」时可复述默认假设请一次确认。
+
+---
+
+## Go-live / 切生产 (with kdshrsso)
+
+When user says **上线 / 切生产**, coordinate with **kdshrsso** Go-live. For Weaver todo channel, update server file:
+
+`{EAS_HOME}/server/properties/baoye/weaverTodoConfig.properties`
+
+| Key | Set to production |
+|-----|-------------------|
+| `weaver.openapi.host` | 生产 OpenAPI/ecology |
+| `weaver.corpid` / `app_key` / `app_secret` | 生产开放平台 |
+| `weaver.syscode` | 生产已注册标识 |
+| `weaver.shrServerName` | 生产 SHR(redirect) |
+| `weaver.sso.appId` | 与 `integration-config-prod.json` 中 appId 一致 |
+| `weaver.sso.entryPath` | 通常仍 `/qy/auth/weaver/app.do` |
+
+Also: 泛微异构系统/IP 白名单;验收待办点击走生产 SSO。  
+Full list: project `docs/integration/go-live-prod.html`.
+
+---
+
+## When to use
+
+| Scenario | Fit |
+|----------|-----|
+| Push workflow **待办** to enterprise portal / OA | Yes |
+| Push BMC **通知/待阅** to portal | Yes |
+| Portal APIs match YH PORTAL-* REST + HTTP Basic + ESB envelope | Yes |
+| Weaver/Fanwei/统一待办中心 (BaoYPro) via ReceiveTodo API | Yes — see weaver-baoy-plan.md |
+| Only DingTalk/Feishu, no portal | Partial — separate channel + config file; skip PORTAL-* / Weaver receive |
+| Replace product message center entirely | No |
+
+## Quick decision
+
+```
+Need portal sync for SHR workflow/messages?
+  │
+  ├─ Implement MessageWebServiceDao (platform SPI) — NEVER invent a parallel push framework
+  │     ├─ Todo: addMessage / addCompletedMessage / removeMessage
+  │     └─ ToBeRead: updateMessages(id, MsgStatus, ctx)
+  │
+  ├─ Downstream?
+  │     ├─ YH-style ESB PORTAL-* → TodoMessageServer + receiveOAConfig
+  │     └─ Weaver E10 receiveRequestInfo → weaverTodo + weaverTodoConfig
+  │
+  ├─ Deep link back to SHR?
+  │     ├─ Weaver: relative /qy/auth/weaver/app.do?appId&redirect=approveAbs (kdshrsso B-qy)
+  │     ├─ Future DingTalk/Feishu: that platform SSO entry + same approveAbs
+  │     └─ Never rely on Unified /auth/app + UA for PC Weaver todos
+  │
+  └─ Config naming
+        ├─ Keep weaverTodoConfig.properties for Weaver only (do not mega-rename for BY)
+        └─ Later: dingtalkTodoConfig.properties / feishuTodoConfig.properties
+```
+
+---
+
+## Adapter targets (YH vs BaoY)
+
+| Project | Downstream | API style |
+|---------|------------|-----------|
+| YHPro | Enterprise portal ESB | Multiple PORTAL-* URLs |
+| BaoYPro | Weaver / Fanwei / eteams | One receiveRequestInfo (+ OpenAPI oauth) |
+
+Reuse YHPro SPI/hooks idea; replace HTTP payload and status fields. Detail: [weaver-baoy-plan.md](weaver-baoy-plan.md)
+
+---
+
+## Architecture (reference layout)
+
+### YHPro
+
+```
+platform workflow / BMC
+  → XxxMessageWebServiceDao
+  → Todo / ToBeRead servers → PORTAL-*
+  → MessageCommonUtil (Basic + JSON)
+```
+
+### BaoYPro (Weaver)
+
+```
+platform workflow / BMC
+  → WeaverMessageWebServiceDao
+  → WeaverTodoMessageServer
+  → WeaverReceiveTodoClient (OpenAPI token + receiveRequestInfo)
+  → pcurl from WeaverTodoSupport (entryPath + appId + redirect)
+```
+
+Config: `{EAS_HOME}/server/properties/baoye/weaverTodoConfig.properties`  
+Key: `weaver.sso.entryPath` (default `/qy/auth/weaver/app.do`) + `weaver.sso.appId`.
+
+---
+
+## Platform SPI map (MessageWebServiceDao)
+
+| Method | YH PORTAL | Weaver isremark |
+|--------|-----------|-----------------|
+| `addMessage` | 002 | 0 |
+| `addCompletedMessage` | 006 then 003 | 2; flow end → 4 |
+| `removeMessage` | 007 | -1 |
+| `updateMessages` | 011 / 012 | 8 (+ viewtype) |
+
+---
+
+## Implementation checklist (new project)
+
+```
+- [ ] 1. Downstream API doc + env + auth
+- [ ] 2. Person mapping field (工号 / cfidmusercode / …)
+- [ ] 3. MessageWebServiceDao facade + servers
+- [ ] 4. Per-channel properties (weaverTodoConfig / receiveOAConfig / …)
+- [ ] 5. assignReadRecord / CallLog
+- [ ] 6. Register SPI in product
+- [ ] 7. BMC hook if 待阅 needed
+- [ ] 8. PC URL: SSO wrap (kdshrsso) + approve page
+- [ ] 9. Mobile URL if required
+- [ ] 10. E2E create → approve → complete/delete
+```
+
+---
+
+## Thinking patterns
+
+1. **SPI first** — Extend `MessageWebServiceDao`.
+2. **Idempotency keys** — Decide before coding (YH taskId vs Weaver flowid+receiver).
+3. **Order** — Complete/delete tasks before terminal flow update.
+4. **Identity** — Same code across SSO + push + SHR person.
+5. **Deep link + SSO** — Always SSO entry then target (kdshrsso); platform-specific entry URL.
+6. **Latency** — WF DB lag; delay/retry.
+7. **Observability** — CallLog; empty requestparams often = exception before body write.
+8. **Channels** — Separate configs/clients per IdP; fan-out later; don’t block one channel on another.
+9. **Config externalization** — No secrets in git; hot-reload when possible.
+10. **Weaver links** — Relative path only; configurable `sso.entryPath`.
+
+---
+
+## Anti-patterns
+
+| Avoid | Why |
+|-------|-----|
+| Bypass MessageWebServiceDao | Missed callbacks |
+| Absolute pcurl to Weaver | API rejects / 外链规则不符 |
+| Unified `/auth/app` for Weaver PC todo | UA → unsupported_client |
+| Rename weaverTodoConfig to “todoConfig” mid-project | Risk / little gain |
+| One config file for Weaver OpenAPI + DingTalk push | Secrets and APIs differ |
+| Soft-fail empty person mapping | Wrong assignee |
+
+---
+
+## Search terms
+
+```
+MessageWebServiceDao WeaverMessageWebServiceDao
+WeaverTodoMessageServer WeaverTodoSupport WeaverTodoConfig
+weaver.sso.entryPath receiveRequestInfo syscode s-HRCloud
+assignReadRecord OAMsgCallLog receiveOAConfig PORTAL-002
+```
+
+## References
+
+| Doc | Content |
+|-----|---------|
+| [yhpro-reference.md](yhpro-reference.md) | YHPro package + field mapping |
+| [portal-api-map.md](portal-api-map.md) | PORTAL field quick map |
+| [weaver-baoy-plan.md](weaver-baoy-plan.md) | BaoYPro Weaver todo (implemented notes) |
+| [weaver-receive-todo-api.md](weaver-receive-todo-api.md) | Weaver E10 field extract |
+| kdshrsso Option B-qy | SSO entry / logout / AttackFilter |

+ 120 - 0
单点_待办skill/kdshrpmsg/portal-api-map.md

@@ -0,0 +1,120 @@
+# Portal PORTAL API Field Map
+
+Condensed from YH docs: 《推送待办消息接口说明》《消息通知推动接口说明》.
+Customer environments may change hosts; field semantics stay stable.
+
+## Common
+
+- Method: POST JSON
+- Auth: HTTP Basic
+- Envelope: `esbInfo` + `requestInfo`
+- Success: `esbInfo.returnStatus = "S"`
+- Volume: 1 record per call; SLA ~30s; failure → re-push
+
+Hosts (玉湖 example):
+
+| Env | Base |
+|-----|------|
+| Dev | `http://esb-dev.iyuhu.com:9191` |
+| UAT | `http://esb-test.iyuhu.com:9191` |
+| Prod | `http://esb.iyuhu.com:9191` |
+
+---
+
+## PORTAL-002 addFlow — 新增流程+待办
+
+Path: `/v1/portal/api/addFlow` · operatorType **1**
+
+| Field | Required | Notes |
+|-------|----------|-------|
+| instanceId | Y | Unique per systemCode |
+| systemCode | Y | Registered with portal |
+| owerId | Y | IDM userCode of initiator |
+| flowStartDate | Y | `yyyy-MM-dd HH:mm:ss` |
+| title | Y | |
+| flowStatus | Y | `approving` on create |
+| flowType / flowTypeName | Y | Process type code/name |
+| pcTaskUrl / mobileTaskUrl | Y | Header "我发起的" links |
+| taskList[].taskId | Y | Unique; SHR assignId |
+| taskList[].taskApproveId/Name | Y | IDM userCode + name |
+| taskList[].taskArriveDate | Y | |
+| taskList[].pcTaskUrl / mobileTaskUrl | Y | Todo card links |
+| todoType / positionCode | N | |
+
+---
+
+## PORTAL-006 updateAgency — 待办转已办
+
+Path: `/v1/portal/api/updateAgency` · operatorType **4**
+
+| Field | Required |
+|-------|----------|
+| taskId, instanceId, systemCode | Y |
+| taskApproveId, taskApproveName | Y |
+| taskArriveDate, taskDoneDate | Y |
+| pcTaskUrl, mobileTaskUrl | Y |
+
+---
+
+## PORTAL-007 deleteAgency — 删除待办
+
+Path: `/v1/portal/api/deleteAgency` · operatorType **3**
+
+| Field | Required | Notes |
+|-------|----------|-------|
+| instanceId, systemCode | Y | |
+| taskIdList | Y | Comma-separated; **cannot reuse** after delete |
+| operatorType | Y | 3 |
+
+---
+
+## PORTAL-003 updateFlow — 流程更新
+
+Path: `/v1/portal/api/updateFlow` · operatorType **2**
+
+| flowStatus | Portal meaning |
+|------------|----------------|
+| approving | In progress; may add new tasks |
+| complete | Done — terminal |
+| cancel | Void — terminal |
+| delete | Deleted — terminal |
+| refuse | Rejected |
+| revoke | Revoked |
+| draft | Draft |
+
+**Before terminal status:** clear todos via 006 or 007. Terminal instanceId must not receive further updates.
+
+Key fields: instanceId, systemCode, owerId, title, flowStatus, flowType, flowStartDate, pc/mobile URLs; taskList optional when approving.
+
+---
+
+## PORTAL-011 toBeRead — 新增待阅
+
+Path: `/v1/portal/api/toBeRead` · operationType **0**
+
+| Field | Required | Notes |
+|-------|----------|-------|
+| title, systemCode, systemName | Y | |
+| employeeCode/Name | Y | Receiver IDM userCode |
+| ownerCode/Name | Y | Display sender |
+| url, mobileUrl | Y | |
+| viewId | Y | Int; unique under systemCode |
+| type, typeName | Y | Module on portal UI |
+| sendDingFlag | N | 0/1 |
+| instanceId | N | Empty string OK |
+
+---
+
+## PORTAL-012 toBeReadTransferredToRead — 待阅转已阅
+
+Path: `/v1/portal/api/toBeReadTransferredToRead` · operationType **1**
+
+Required: systemCode, employeeCode, viewId, type, operationType=1
+
+---
+
+## PORTAL-013 deleteToBeRead — 删除待阅
+
+Path: `/v1/portal/api/deleteToBeRead` · operationType **2**
+
+Required: systemCode, employeeCode, viewId, type, operationType=2

+ 146 - 0
单点_待办skill/kdshrpmsg/weaver-baoy-plan.md

@@ -0,0 +1,146 @@
+# BaoYPro — Weaver E10 接收统一待办数据(已实现骨架)
+
+Source doc: 接收统一待办数据.docx  
+Field extract: [weaver-receive-todo-api.md](weaver-receive-todo-api.md)  
+SSO deep link: skill **kdshrsso** Option B-qy (`/qy/auth/weaver/app.do`).
+
+Target: Kingdee SHR → Weaver/Fanwei E10 统一待办中心(报业 BaoYPro)。
+
+**New project / new channel:** complete **Agent intake** in [SKILL.md](SKILL.md) before coding.
+
+## Endpoint (E10 OpenAPI)
+
+Private E10 often needs **`/papi/openapi`** prefix (root `/oauth2/authorize` may hit portal HTML):
+
+```
+POST {host}/papi/openapi/api/open_intunifytodo/server_engine/receiveRequestInfo?access_token={token}
+```
+
+Auth:
+
+1. corpid / app_key / app_secret (开放平台,与 SSO oauth apps **分开**)
+2. authorize → code → access_token (~2h)
+3. Success: `message.errcode == "200"` and `data.operResult == "1"`
+
+## Upsert / concurrency
+
+Same **syscode + flowid + workflowname(workflowcode) + receiver** → update; else insert.  
+Update compares **receivets** (ms): newer wins.
+
+## isremark / viewtype
+
+| isremark | Meaning | SHR SPI |
+|----------|---------|---------|
+| 0 | 待办 | `addMessage` |
+| 2 | 已办 | `addCompletedMessage` (task done) |
+| 4 | 办结 | Flow end (`requestStatus` often 3) |
+| 8 | 抄送/待阅 | `updateMessages` |
+| -1 | 删除 | `removeMessage` |
+
+| viewtype | Meaning |
+|----------|---------|
+| 0 | 未读 |
+| 1 | 已读 |
+
+## Field map (highlights)
+
+| Weaver | SHR |
+|--------|-----|
+| syscode | e.g. `s-HRCloud` (后台注册,<=10) |
+| flowid | **procInstId**(不要用 assignId) |
+| receiver | 工号 / PERSON_NUMBER 规则 |
+| pcurl / appurl | **相对路径 only**(禁止 http/https 开头) |
+| receivets | 毫秒时间戳,乱序必传 |
+
+## pcurl 拼法(BaoYPro 已落地)
+
+Weaver 禁止绝对 URL。SHR 推相对路径,由泛微外链前缀补全域名。
+
+```
+/{weaver.sso.entryPath}?appId={weaver.sso.appId}&redirect={encode(approveAbs)}
+```
+
+Defaults:
+
+- `weaver.sso.entryPath=/qy/auth/weaver/app.do`
+- `approveAbs = shrServerName + path.pc.approve + ?AssignmentId=&billID=`
+
+Code: `WeaverTodoSupport.buildRelativePcUrl` + `WeaverTodoConfig`.
+
+**Do not** use Unified `/qy/auth/app` + UA for todo links (PC browser from Weaver fails UA detect).
+
+## Config strategy (multi-platform later)
+
+| File | Scope |
+|------|-------|
+| `baoye/weaverTodoConfig.properties` | **Keep name** — Weaver OpenAPI push only |
+| `baoye/dingtalkTodoConfig.properties` | Future — separate credentials + `dingtalk.sso.entryPath` |
+| `baoye/feishuTodoConfig.properties` | Future |
+| `integration-config-*.json` | SSO OAuth only(kdshrsso),not OpenAPI push secrets |
+
+Do **not** rename weaverTodoConfig into a mega shared file for BY. Shared ideas: approve path builder + SSO entry template convention; each channel own config + push client.
+
+Hot-reload: `WeaverTodoConfig` reloads on file mtime (`WEAVER_TODO_CONFIG_PATH` override optional).
+
+### Template
+
+```properties
+weaver.todo.enabled=true
+weaver.openapi.host=http://{e10-host}:{port}
+weaver.openapi.authorizePath=/papi/openapi/oauth2/authorize
+weaver.openapi.accessTokenPath=/papi/openapi/oauth2/access_token
+weaver.openapi.receivePath=/papi/openapi/api/open_intunifytodo/server_engine/receiveRequestInfo
+weaver.corpid=
+weaver.app_key=
+weaver.app_secret=
+weaver.syscode=s-HRCloud
+weaver.shrServerName=http://{shr-host}:{port}
+weaver.sso.appId={integration oauth appId}
+weaver.sso.entryPath=/qy/auth/weaver/app.do
+weaver.path.pc.approve=/easweb/webviews/workflow/transferApprove.jsp
+weaver.wf.delayMs=3000
+```
+
+## SPI (BaoYPro)
+
+Package: `com.kingdee.eas.custom.weaverTodo`  
+Facade: `WeaverMessageWebServiceDao` → `WeaverTodoMessageServer` / ToBeRead path as implemented.
+
+```
+addMessage            → isremark=0
+addCompletedMessage   → isremark=2; flow end → 4
+removeMessage         → isremark=-1
+updateMessages        → isremark=8 (+ viewtype)
+```
+
+CallLog: empty `requestparams` often means exception path (e.g. authorize HTTP 502) — check server logs, not only CallLog.
+
+## Checklist BaoYPro
+
+```
+- [x] OpenAPI token + receiveRequestInfo
+- [x] WeaverMessageWebServiceDao + relative pcurl via weaver entryPath
+- [x] flowid=procInstId; receivets; sanitize title
+- [x] weaver.sso.entryPath configurable
+- [ ] 泛微后台:异构系统 / IP 白名单 / 流程类型开关
+- [ ] SPI 产品注册 + deploy jar/properties to EAS_HOME
+- [ ] E2E: 0→2→4; 待阅; delete -1
+- [ ] Compensation via CallLog (optional)
+- [ ] 切生产:见项目 docs/integration/go-live-prod.html + skill Go-live
+```
+
+## Diff vs YHPro PORTAL-*
+
+| Item | YHPro | BaoY Weaver E10 |
+|------|-------|-----------------|
+| APIs | Many PORTAL URLs | One receiveRequestInfo (+ oauth) |
+| Auth | HTTP Basic | OAuth2 → access_token |
+| Links | Absolute HTTPS OK | **Relative path only** |
+| SSO wrap | `/shr/api/oAToSHR?redirect=` | `/qy/auth/weaver/app.do?appId&redirect=` |
+| Config | receiveOAConfig | **weaverTodoConfig** (per platform later) |
+
+## Future DingTalk / Feishu todos
+
+1. New `*TodoConfig.properties` + push client (different API — not Weaver receive)
+2. Deep link = that platform SSO entry + same approve redirect
+3. Fan-out from SPI / router by `enabled` flags — do not overload weaverTodoConfig

+ 522 - 0
单点_待办skill/kdshrpmsg/weaver-receive-todo-api.md

@@ -0,0 +1,522 @@
+
+接入指引
+一、适用场景
+客户有业务系统要调用统一待办接口推送数据,本文档是用来快速指导业务系统开发人员快速开发。
+二、功能价值
+1.
+快速配置:指导用户在统一待办中心和开放平台快速做好相应配置。
+2.
+快速开发:指导业务系统开发人员快速调用接口。
+三、业务流程图
+
+接收统一待办数据
+
+一、功能背景
+泛微提供了统一待办接口,供三方系统开发人员调用,向泛微统一待办中心推送审批事项。
+二、功能价值
+供三方系统开发人员阅读,并指导其做统一待办推送开发。
+三、开发说明
+E9升级到E10的使用兼容接口: HYPERLINK "https://www.e-cology.com.cn/sp/doc/docDetail/2003694139603298098" \t "_blank" https://www.e-cology.com.cn/sp/doc/docDetail/2003694139603298098
+1.开发步骤
+1.1.第一步:完成开放平台认证
+公有云环境:
+HYPERLINK "https://weapp.eteams.cn/sp/openapi/base/doc/common/readme" \t "_blank" https://weapp.eteams.cn/sp/openapi/base/doc/common/readme
+
+私有云环境:
+HYPERLINK "https://weapp.eteams.cn/build/techdoc/wdoc/index.html" \l "/public/doc/535b5174-f827-423d-bfeb-673d252a6777" \t "_blank" https://weapp.eteams.cn/build/techdoc/wdoc/index.html#/public/doc/535b5174-f827-423d-bfeb-673d252a6777
+1、使用管理员账号进入私有化E10的后台管理中心 - 开放平台
+2、进入开放平台-首页-旁边有个文档中心
+3、点击文档中心
+4、会跳转到这个界面,对应开放平台的文档内容。相关的接口文档内容,点击右上角<示例>即可进入。
+按照以上文档完成开发者认证,获取“Corpid”:
+
+设置好通用型应用,获取“app_key”和“app_secret”:
+
+测试环境已设置的:
+Corpid
+76044c1d5e196b6d4951eae18210a7dc
+app_key
+8a30c568cb2540eea832e0b89ab007de
+app_secret
+65b6cf3ac6311518a91233e6d074f8
+标识 s-HRCloud
+简称 SHR
+全称 金蝶SHR
+
+1.2.第二步:授权接口调用
+获取code,请求地址:
+E10的OPENAPI域名+端口/oauth2/authorize?corpid=xxx&response_type=code&state=xxx
+
+请求方式:GET
+参数名
+必选
+类型
+说明
+corpid
+是
+String
+注册企业corpId
+response_type
+是
+String
+返回类型,此时固定为:code
+state
+是
+String
+重定向后会带上state参数,企业可以填写a-zA-Z0-9的参数
+值,长度不可超过128个字节
+
+返回示例:
+{
+"errcode": "0",
+"errmsg": "success",
+"code": "5b460013526833939ecbdc1e91250946"
+}
+
+参数名
+类型
+说明
+errcode
+String
+返回码
+errmsg
+String
+返回信息
+code
+String
+code信息,有效时间为10分钟且只能使用一次
+
+获取token请求地址:
+E10的OPENAPI域名+端口/oauth2/access_token
+
+请求方式:POST
+参数名
+必选
+类型
+说明
+app_key
+是
+String
+应用app_key
+app_secret
+是
+String
+应用app_secret
+grant_type
+是
+String
+授权类型,固定:authorization_code
+code
+是
+String
+获取的code
+
+注意,若返回显示系统繁忙,需要确认下“Content-Type”配置为:
+“application/x-www-form-urlencoded”
+
+返回示例:
+{
+"errcode": "0",
+"errmsg": "success",
+"accessToken": "e5cbfcb6c2c2839be5a3c18ea4895cdf",
+"acessToken": "e5cbfcb6c2c2839be5a3c18ea4895cdf",
+"refreshToken": "95b10953048c8f9b2aee66c3fe502da9",
+"expires_in": 7200
+}
+
+参数名
+类型
+说明
+errcode
+String
+返回码
+errmsg
+String
+返回信息
+accessToken
+String
+接口凭证有效时间为2小时
+refreshToken
+String
+刷新令牌
+expire_in
+Int
+有效时间
+
+1.3.第三步:统一待办中心配置
+开启总开关:
+
+做好异构系统注册:
+
+提供好测试账号的人员数据给推送方开发人员:
+
+1.4.第四步:统一待办接口调用
+1.4.1.接口地址
+E10的OPENAPI域名+端口
+/api/open_intunifytodo/server_engine/receiveRequestInfo?access_token=a44dba991ccfeaf9da592e8bc3cc55ee
+1.4.2.接口逻辑
+1. syscode 、flowid 、workflowname(workflowcode) 、receiver 相同更新,不同新增,更新时对比时间戳(receivets)字段,大于历史数据更新,小于等于只记录调用状态不更新。
+2. workflowname与workflowcode是一一对应的关系。
+注意:推送方系统开发的时候需要提供以下功能
+OA进行维护的时候,接口不通,推送方需要有补偿推送的功能
+如果需要将对接之前的数据推送,推送方需要提供历史数据补偿的功能
+1.4.3.参数说明
+参数名称
+参数说明
+必填
+类型
+注意事项
+access_token
+调用接口凭证
+是
+string
+获取accessToken信息,注意这个参数添加到url中
+syscode
+应用系统标识
+是
+string
+在泛微后台【统一待办中心集成】注册(需要购买该模块),然后分配给推送方(固定值)
+flowid
+第三方的流程id
+是
+string
+相同流程需要使用相同流程id,即:
+张三创建的流程提交给李四审批,这两个人的数据flowid是一致的
+requestname
+流程标题
+是
+string
+长度不超过2000,且不允许出现:
+单引号:'您有一个新的待审批事项,请及时处理'html编码字符(&nbsp;|&lt;|&gt;等)
+workflowcode
+流程类型ID
+是
+string
+流程类型ID,与workflowname(流程类型名称)是一一对应的关系。即相同的流程类型,流程类型ID也必须相同。可以使用workflowname的哈希码。
+workflowname
+流程类型名称
+是
+string
+正确示例:请假单
+错误示例:张三2021-12-12请假单
+还要注意:
+长度不允许超过2000,且不允许出现:单引号:'请假单'
+html编码字符(&nbsp;|\&lt;|&gt;等)
+nodename
+节点名称
+是
+string
+长度不允许超过2000,且不允许出现:单引号:'审批节点'
+html编码字符(&nbsp;|\\&lt;|\&gt;等)
+receivernodename
+接收人所属节点
+否
+string
+长度不允许超过2000,且不允许出现:单引号:'审批节点'
+html编码字符(&nbsp;\\&lt;\&gt;等)
+pcurl
+PC端地址
+否
+string
+示例:/testPCurl/111
+注意,提供的地址不允许http或者https开头,只要传相对路径即可
+appurl
+APP端地址
+否
+string
+示例:/testAppurl/222
+注意,提供的地址不允许http或者https开头,只要传相对路径即可
+creator
+创建人
+否
+string
+根据泛微后台配置(人员转换规则),可以支持:
+泛微OA的人员主键
+泛微OA的人员昵称
+泛微OA的人员工号
+泛微OA的人员身份证
+泛微OA的人员邮箱
+注意:没有创建人可以不传,创建人就会不显示数据
+createdatetime
+创建时间
+是
+string
+示例:”2018-12-04 17:56:00”
+receiver
+接收人
+是
+string
+根据泛微后台配置(人员转换规则),可以支持:
+泛微OA的人员主键
+泛微OA的人员账号
+泛微OA的人员工号
+泛微OA的人员身份证
+泛微OA的人员邮箱
+泛微OA的手机号
+注意:多个接收人可以“,”分隔,一次接收人不能超过20个
+receivedatetime
+接收时间
+是
+string
+示例:”2018-12-04 17:56:00”
+isremark
+待办处理状态
+是
+string
+流程处理状态
+0:待办
+2:已办
+4:办结
+8:抄送(待阅)
+-1:删除
+viewtype
+已读状态
+是
+string
+流程查看状态
+0:未读 (注:isremark = 0 且 viewtype = 0会给OA消息中心推送消息,对接钉钉、企业微信)
+1:已读;
+receivets
+时间戳
+否
+string
+时间戳字段:
+客户端使用异步线程调用接口的时候,根据此字段判断是否需要更新数据。(防止后发的请求数据被之前的覆盖)示例:1602817491990(毫秒级时间戳)
+custom
+自定义字段
+否
+string
+"custom":{
+"aa":"11",
+"bb":"22"
+}
+aa:自定义字段名称
+requestStatus
+流程状态
+否
+string
+-1  草稿(新建)
+1  审批中
+3  正常归档
+4  强制归档
+7  暂停
+当前参数未入参且isremark=4(办结)时,requestStauts默认设置为3(正常归档)
+10.0.2412.01版本更新
+apiOverTime
+归档时间
+否
+string
+示例:”2018-12-04 17:56:00”
+当前参数为入参且requestStauts=3(正常归档)时,当前参数自动赋值receivedatetime(接收时间);
+当参数requestStatus未空时(isremark=4时requestStatus自动赋值3,这种情况也不为空)当前字段入参无效
+10.0.2412.01版本更新
+1.4.4.返回值说明
+1.4.4.1.主体说明
+名称
+说明
+message:errcode
+状态码:
+200:正常
+500:系统内部错误
+3000:配置异常或者调用接口参数异常,导致无法接收数据
+message:errmsg
+接口执行说明
+data
+接口调用的详细说明
+1.4.4.2.data说明
+名称
+说明
+syscode
+调用接口时候传过来的系统标识
+flowid
+调用接口时候传过来的流程id
+receiver
+调用接口时候传过来的接收人
+operResult
+1:成功   0:失败
+message
+接口执行结果描述,示例:流程数据【测试流程001】检测接收人【fyj】存在,接收待办成功
+1.4.3.3.message说明:
+描述信息
+说明
+syscode不能为空
+syscode不能为空
+syscode长度不能超过10
+syscode长度不能超过10
+tenant_key不能为空
+tenant_key不能为空
+tenant_key长度不能超过10
+tenant_key长度不能超过10
+统一待办中心未启用
+请联系OA管理员,打开接口开关
+应用系统标识【test】未注册
+请联系OA管理员,注册指定应用系统信息
+应用系统标识【test】当前IP(127.0.0.1)未授权
+请联系OA管理员,添加当前IP到应用系统
+注册信息
+请求参数:receivets不合法,当前时间戳为【2021-12-12 12:12:12】
+receivets字段必须是长整型的毫秒值,示
+例:1602817491990(毫秒级时间戳)
+请求参数:requestlevel不合法,当前值为【" + requestlevel+"】
+requestlevel必须是数字内容的字符串,示
+例:"1"
+请求参数:receiver不能为空
+请求参数:receiver不能为空
+请求参数:createdatetime不合法,
+合法格式:yyyy-mm-dd hh:MM:ss,
+当前值为:【2021/12/12 12:12:12】
+createdatetime,必须符合格式:yyyymm-dd hh:MM:ss
+请求参数:receivedatetime不合法,
+合法格式:yyyy-mm-dd hh:MM:ss,
+当前值为:【2021/12/12 12:12:12】
+receivedatetime,必须符合格式:yyyymm-dd hh:MM:ss
+请求参数:flowid不能为空
+请求参数:flowid不能为空
+flowid长度不能超过200
+flowid长度不能超过200
+请求参数:isremark不合法,
+合法值为【0:待办|2:已办|4:办结|8:抄
+送|-1:删除】
+请求参数:isremark不合法,
+合法值为【0:待办|2:已办|4:办结|8:
+抄送|-1:删除】
+请求参数:viewtype不合法,
+合法值为【0:未读|1:已读】
+请求参数:viewtype不合法,
+合法值为【0:未读|1:已读】
+请求参数:requestname不能为空
+请求参数:requestname不能为空
+请求参数:requestname不能包含html编码字符,当前参数值为:[&nbsp;]
+请求参数:requestname不能包含如下
+html编码字符:
+&nbsp;
+&lt;
+&gt;
+&amp;
+&quot;
+&apos;
+&cent;
+&pound;
+&yen;
+&euro;
+&sect;
+&copy;
+&reg;
+&trade;
+&times;
+&divide;
+请求参数:requestname不允许超出2000长度
+请求参数:requestname不允许超出2000
+长度
+请求参数:workflowname不能为空
+请求参数:workflowname不能为空
+请求参数:workflowname不允许超出2000长度
+请求参数:workflowname不允许超出2000长度
+请求参数:pcurl不允许超出2000长度
+请求参数:pcurl不允许超出2000长度
+请求参数:pcurl不允许传绝对地址
+示例:/testPCurl/111
+提供的地址不允许http或者https开头,只
+要传相对路径即可
+请求参数:pcurl不允许包含html字符实体
+请求参数:pcurl不能包含如下html编码字
+符:
+&nbsp;
+&lt;
+&gt;
+&amp;
+&quot;
+&apos;
+&cent;
+&pound;
+&yen;
+&euro;
+&sect;
+&copy;
+&reg;
+&trade;
+&times;
+&divide;
+请求参数:appurl不允许超出2000长度
+请求参数:appurl不允许超出2000长度
+请求参数:appurl不允许传绝对地址
+示例:/testAppurl/222
+注意,提供的地址不允许http或者https开
+头,只要传相对路径即可
+请求参数:appurl不允许包含html字符实体
+请求参数:appurl不能包含如下html编码
+字符:
+&nbsp;
+&lt;
+&gt;
+&amp;
+&quot;
+&apos;
+&cent;
+&pound;
+&yen;
+&euro;
+&sect;
+&copy;
+&reg;
+&trade;
+&times;
+&divide;
+应用系统标识【test】不允许接收流程数据
+请联系OA管理员,打开当前注册的应用系
+统接收数据的开关
+流程类型【测试流程001】不允许接收流程数据
+请联系OA管理员,打开当前注册的应用系
+统流程类型接收数据的开关
+流程数据【测试流程标题001】检测接收人【fyj】
+不存在
+请联系OA管理员,确认当前接收人是否存
+在——当前接收人的OA人员主键ID|工号|
+身份证|邮箱是否对应
+请求参数:workflowcode不能为空
+请求参数:workflowcode不能为空
+tf1b4jq4ve _ test _ 359369_ 18019164087参数,调用接口失败;调用接口并发过高,存在重复的tf1b4jq4ve _ test _ 359369
+建议在pcurl、appurl多个接收人一致的情
+况下,receiver 添加多个人即可,降低并发
+量
+workflowcode长度不能超过200
+workflowcode长度不能超过200
+2.接口调用示例
+2.1.请求参数示例
+{
+"syscode": "wjn",
+"flowid": "020",
+"requestname": "已处理标签20(标题)",
+"workflowcode": "020",
+"workflowname": "已处理标签20(流程类型)",
+"receivernodename": "接收人节点名称",
+"nodename": "已处理标签20(节点名称)",
+"pcurl": "/s?ie=utf-8&f=3&rsv_bp=1&tn=baidu&wd=泛微",
+"appurl": "/s?ie=utf-8&f=3&rsv_bp=1&tn=baidu&wd=泛微",
+"creator": "18121431944",
+"createdatetime": "2022-03-21 11:23:20",
+"receiver": "18121431944",
+"receivedatetime": "2022-03-21 11:23:20",
+"isremark": "2",
+"viewtype": "1",
+"receivets": "1647846670662",
+"custom": {
+"aa": "11",
+"bb": "22"
+}
+}
+2.2.返回参数示例
+{
+"message": {
+"errcode": "200",
+"errmsg": "接口返回成功"
+},
+"data": {
+"syscode": "gj",
+"flowid": "003",
+"receiver": "18121431944",
+"operResult": "1",
+"message": "流程数据【测试自定义字段入库(标题)】检测接收人【18121431944】存在,
+接收已办成功"
+}
+}

+ 51 - 0
单点_待办skill/kdshrpmsg/yhpro-reference.md

@@ -0,0 +1,51 @@
+# YHPro Reference - Portal Message Push
+
+Concrete layout from 玉湖 YHPro (tested). Adapt package names and URLs for other projects.
+
+## Source layout
+
+```
+src/com/kingdee/eas/custom/messageWebService/
+|- OAMessageWebServiceDao.java
+|- config/MessageConfig.java
+|- server/TodoMessageServer.java
+|- server/ToBeReadMessageServer.java
+|- util/MessageCommonUtil.java, DingMessageUtil.java, DingTokenManager.java
+
+src/com/kingdee/eas/base/message/app/
+|- BMCMessageControllerBeanEx.java
+
+src/com/kingdee/eas/custom/synctask/
+|- MsgSyncIDMFacadeControllerBean.java
+```
+
+Config: `{EAS_HOME}/server/properties/yuhu/receiveOAConfig.properties`
+
+## Todo flow
+
+addMessage: sleep 8s -> assignId -> addFlow(op=1) -> assignReadRecord
+addCompletedMessage: sleep 8s -> if task terminal updateAgency(op=4) -> if flow terminal updateFlow(op=2)
+removeMessage: sleep 9s -> deleteAgency(op=3)
+
+## Status map
+
+COMPLETED->complete; REJECTED/ROLLBACKED->refuse; CANCELED/TERMINATED->cancel; ABORTED->revoke; else approving
+
+## ToBeRead
+
+bosType 41C9B39B; UNREADED->011; READED->012; BMCMessageControllerBeanEx hooks create
+
+## Deep links
+
+PC: /shr/api/oAToSHR?redirect=...transferApprove.jsp
+Mobile: MBOS -> /qy/auth/V2/sso?state=
+SSO: kdshrsso Option B
+
+## Tables
+
+assignReadRecord: MsgId, assignID, billId, workflowname, procInstId
+OAMsgCallLog: audit request/response
+
+## Person
+
+cfidmusercode via T_bd_person + T_PM_User; empty -> throw

+ 410 - 0
单点_待办skill/kdshrsso/SKILL.md

@@ -0,0 +1,410 @@
+---
+name: kdshrsso
+description: >-
+  Design and implement Kingdee EAS/SHR single sign-on. Patterns: (A) override
+  platform CAS/OAuth filters; (B) Servlet + LtpaToken (OAToSHR / IDM); (B-qy)
+  Weaver E10 or DingTalk OAuth in qy.war then POST index2sso. Covers
+  eassso/login_withForm.js, OAToSHR, CallbackSuffixCompatFilter, Apusic *.do
+  callback 404, LTPA Location multi-encode fix, index2sso vs OTP2sso,
+  logoutHR.js federated Weaver logout + service whitelist, AttackFilter CSRF/Referer,
+  adSsoConfig, OASSOConfig, KDPortalAuthenticationFilter, WeaverAuthController vs UnifiedAuth.
+  BEFORE coding: ask intake questions in this skill (IdP, pattern A/B/B-qy, env, mapping, logout).
+  When user says 上线/切生产/go-live: follow Go-live section; list files to change; confirm prod URLs before editing.
+---
+
+# Kingdee SSO — Implementation Patterns
+
+Pick **one primary pattern** per SHR SSO integration. Do not mix servlet Ltpa login with filter `_const_cas_assertion_` on the same entry without explicit design.
+
+---
+
+## Agent intake — ask before coding (mandatory)
+
+When the user asks to implement or port SSO for a **new project / new IdP**, do **not** jump straight into code. First confirm goals with short questions (batch them). Mark unknowns; block only on items that change architecture.
+
+### Must confirm (block if unanswered)
+
+1. **IdP type**: 泛微 E10 / 钉钉 / 飞书 / 企业 IDM / Azure AD / other?有无接口或对接文档?
+2. **Pattern**: A(全站 `/shr` CAS 过滤)/ B(`/shr/api/oAToSHR`)/ **B-qy**(`qy.war` `/qy/auth/{platform}`)?不确定则按 IdP 用本 skill Quick decision 推荐后让用户确认。
+3. **SHR / EAS 环境**: `shrServerName`(协议+主机+端口)、是否 Apusic、是否已有或可部署 **qy.war**?
+4. **人员映射**: IdP 用户字段 ↔ sHR 哪一列(`PERSON_NUMBER` / `cfidmusercode` / 手机号 / …)?
+5. **callbackUrl**: 拟注册的完整回调(**通常无 `.do`**);与现网 host 是否一致?
+6. **门户入口**: 是否改 `eassso` + `login_withForm.js`?`/shr/home.do` 是否保留本地登录?
+7. **联邦退出**: 退出后是否必须回 IdP 登录页再进 sHR?IdP 是否有 **service/redirect 白名单**?
+
+### Should confirm (default OK if user says「按 BaoYPro」)
+
+8. 桌面进 sHR:默认 **HTML POST `index2sso`**(禁止 LTPA 进 Location)。
+9. PC 深链:默认 **显式** `/qy/auth/{platform}/app`,不用 UA 版 Unified `/auth/app`。
+10. 配置:`INTEGRATION_CONFIG_PATH` + `integration-config-*.json`;密钥不进 git。
+11. 与待办是否同期做?若是,一并加载 **kdshrpmsg** 并做其 intake(深链入口与 SSO 一致)。
+
+### Output before coding
+
+Reply with a short **目标确认表**(模式、入口 URL、callback、映射、退出、是否动门户 JS),等用户点头或补答案后再改代码。若用户已贴齐文档+明确「按 B-qy / 按报业」,可只复述假设请一次确认。
+
+---
+
+## Go-live / 切生产 (mandatory when user says 上线)
+
+For **BaoYPro-style B-qy**, do not silently rewrite files. First ask for production SHR URL, ecology URL, appId/secret, dataCenter. Then list and edit:
+
+| # | File / config | Change |
+|---|---------------|--------|
+| 1 | `set-server-env.sh` (or equiv.) | `ENV=prod` → `INTEGRATION_CONFIG_PATH=.../integration-config-prod.json`;重启 |
+| 2 | `integration-config-prod.json` | `shrServerName`、`dataCenter`、`allowedPrefixes`、`appId`/`appSecret`、`callbackUrl`、`weaver.ecologyUrl` |
+| 3 | `login_withForm.js` | `ENV_CONFIG.prod.*` + `getCurrentEnv()` 识别生产 Host |
+| 4 | `logoutHR.js` | `prod.shrBaseUrl` / `weaverAppId`;`currentEnv='prod'`(或按 host) |
+| 5 | 泛微后台 | 生产 callback 注册;**退出 service 白名单** = 生产 SHR 根地址 |
+
+Also remind **kdshrpmsg** go-live for `weaverTodoConfig.properties`.  
+Full checklist: project `docs/integration/go-live-prod.html`.
+
+验收:生产门户登录、退出后再进 SHR、地址栏无测试 IP。
+
+---
+
+## Quick decision
+
+```
+Need SSO?
+  │
+  ├─ IdP is Azure AD / standard OIDC (POST token + userinfo)?
+  │     └─ Option A (Filter overlay) — see azure-oauth-reference.md
+  │
+  ├─ IdP is enterprise IDM, portal/OA deep-links with todo URLs?
+  │     ├─ Already on YHPro-style OAToSHR + index2sso? → stay Option B
+  │     └─ Want platform-native SHR session, all /shr URLs auto-OAuth?
+  │           └─ Option A with IDM TicketValidator — see idm-filter-reference.md
+  │
+  ├─ IdP is 泛微 E10 (or DingTalk) OAuth code flow into sHR?
+  │     └─ Option B-qy: qy.war /qy/auth/{platform}/app + POST index2sso
+  │           — prefer explicit /weaver or /dingtalk; avoid UA-only /auth/app for PC deep links
+  │           — see weaver-oauth-qywar-reference.md
+  │
+  └─ Entry from EAS portal eassso/login?service=... ?
+        ├─ Option A: portal links to SHR URL directly, or JS forwards service URL
+        ├─ Option B: JS/servlet bridge to /shr/api/oAToSHR
+        └─ Option B-qy: JS → /qy/auth/weaver/app.do?appId&redirect= (BaoYPro)
+```
+
+| | **Option A: Filter overlay** | **Option B: Servlet + LtpaToken** | **Option B-qy: qy.war + index2sso** |
+|--|------------------------------|-----------------------------------|-------------------------------------|
+| Entry | `/shr?adsso` or any SHR URL via CAS | `/shr/api/oAToSHR` (or portal JS) | `/qy/auth/weaver/app` (or `/dingtalk/...`) |
+| Callback | `{host}/shr?code=` | `{host}/shr/api/oAToSHR?code=` | `{host}/qy/auth/callback/{platform}/{appType}` |
+| Session | `_const_cas_assertion_` | `index2sso.jsp` + Ltpa | Same as B: `index2sso.jsp` + Ltpa |
+| Config | `adSsoConfig.properties` | `OASSOConfig.properties` | `integration-config*.json` + `INTEGRATION_CONFIG_PATH` |
+| Deploy | Overlay platform filters | Servlet in shr war | `lib/sp` jar + `qy.war` |
+| Best for | Seamless `/shr` browse; Azure/OIDC | IDM + OAToSHR | 泛微 E10 / 钉钉 via custom integration |
+
+**Not covered:** JSP-only redirect, `ActiveDirAuthHandler` (Kerberos), product default CAS ticket-only login.
+
+---
+
+## End-to-end flows (compare)
+
+### Option A
+
+```
+Browser → /shr/... (or /shr?adsso)
+  → KDPortalAuthenticationFilter → 302 IdP authorize
+  → callback /shr?code=
+  → KDPortalTicketValidationFilter → TicketValidator.validate()
+  → _const_cas_assertion_ → onSuccessfulValidation → target URL
+```
+
+### Option B (YHPro / IDM)
+
+```
+Portal → eassso/login?service=SHR_URL  [optional JS bridge]
+  → /shr/api/oAToSHR?redirect=|service=SHR_URL
+  → IDM authorize (state = encoded target)
+  → callback /shr/api/oAToSHR?code=&state=
+  → index2sso.jsp?username&password(Ltpa)&redirectTo
+  → SHR session → target URL
+```
+
+### Option B-qy (BaoYPro / Weaver E10)
+
+```
+Portal → eassso/login?service=SHR_URL
+  → login_withForm.js → /qy/auth/weaver/app.do?appId&redirect=
+  → 泛微 authorize → /qy/auth/callback/weaver/pa
+  → Filter forward → pa.do → token/profile → sHR user
+  → WeaverShrLanding HTML POST → /shr/index2sso.jsp
+  → SHR session → redirectTo
+```
+
+Logout (federated): `logoutHR.js` → `/qy/auth/weaver/logout.do?appId&redirect=` → E10 logout  
+`service` = SSO entry URL; **Weaver 退出白名单 must include SHR host**.
+
+Full detail: [weaver-oauth-qywar-reference.md](weaver-oauth-qywar-reference.md)
+
+---
+
+# Option A — Platform Class Overlay
+
+## When to use
+
+| Scenario | Fit |
+|----------|-----|
+| Azure AD / Office 365 OAuth2 | Yes — use `OAuth20CodeValidata` |
+| Enterprise IDM on all `/shr` URLs | Yes — custom `IdmCodeValidata` ([idm-filter-reference.md](idm-filter-reference.md)) |
+| EAS CAS username/password only | No |
+| Kerberos/AD domain | No — `ActiveDirAuthHandler` |
+
+## Architecture
+
+Platform: `SHRTicketValidationFilter extends KDPortalTicketValidationFilter`.
+
+Override hooks:
+
+- `retrieveTicketFromRequest()` — treat OAuth `code` as ticket
+- `getTicketValidator()` / `getTicketValidatorToSso()` — return custom `TicketValidator`
+- `onSuccessfulValidation()` — user check, deep-link redirect via `urlToMap`
+
+## Checklist
+
+```
+- [ ] IdP URLs; client_id; redirect_uri = https://{host}/shr (not servlet path)
+- [ ] adSsoConfig.properties + ConfigAddressUtil
+- [ ] KDPortalAuthenticationFilter — authorize redirect; whitelist local login paths
+- [ ] KDPortalTicketValidationFilter — code branch
+- [ ] TicketValidator (OAuth20CodeValidata or IdmCodeValidata)
+- [ ] onSuccessfulValidation — T_PM_USER check; urlToMap / state redirect
+- [ ] Portal: direct SHR links or login button → /shr?adsso ([portal-login-reference.md](portal-login-reference.md))
+- [ ] web.xml: Authentication Filter before Validation Filter
+- [ ] Overlay deploy; restart; verify _const_cas_assertion_ in session
+```
+
+## Config template (Azure/OIDC)
+
+Path: `{eas.properties.dir}/adSsoConfig.properties`
+
+```properties
+authUrl=https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize
+tokenUrl=https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token
+client_id=
+client_secret=
+shrUrl=https://{host}/shr
+userUrl=https://graph.microsoft.com/v1.0/me
+dataCenter=
+locale=L2
+mobileEid=
+```
+
+## Troubleshooting
+
+| Symptom | Check |
+|---------|--------|
+| No redirect to IdP | Filter deployed; `flag` whitelist; existing assertion |
+| Callback fails | Filter order; `code` in `retrieveTicketFromRequest` |
+| User not found | Mapping in TicketValidator; `T_PM_USER.FNUMBER` |
+| Portal still shows login form | eassso bridge not forwarding to SHR — see portal doc |
+| Deep link lost | `urlToMap` / `state` set before authorize redirect |
+
+Reference: [azure-oauth-reference.md](azure-oauth-reference.md), [idm-filter-reference.md](idm-filter-reference.md)
+
+---
+
+# Option B — Servlet + LtpaToken (Portal / IDM)
+
+## When to use
+
+| Scenario | Fit |
+|----------|-----|
+| Portal/OA todo deep links (`?redirect=`) | Yes |
+| IDM OAuth with servlet callback | Yes |
+| Federated logout JSON (`idmLogoutUrl`) | Yes |
+| All `/shr` browse should auto-OAuth without servlet | No — use Option A |
+| Azure AD on main `/shr` URL | No — use Option A |
+
+## Checklist
+
+```
+- [ ] OASSOConfig.properties (authorizePath, getAccessTokenPath, ssoUrl, idmBaseUrl, serverName)
+- [ ] OAToSHR: authorize() + callBack() + generateShrLoginUrl()
+- [ ] SsoUtil: config, getAccessToken (GET IDM), checkUserExist (cfidmusercode)
+- [ ] SHRToIDMLogout + logoutHR.js (if IDM federated logout required)
+- [ ] web.xml servlets; cas.client.UrlPattern excludes servlet + index2sso.jsp
+- [ ] IDM redirect_uri = ssoUrl (/shr/api/oAToSHR)
+- [ ] Portal link or login_withForm.js bridge
+- [ ] person.cfidmusercode populated for all SSO users
+```
+
+## Config template (IDM)
+
+Path: `{EAS_HOME}/server/properties/{customer}/OASSOConfig.properties`
+
+```properties
+authorizePath=https://idm.example.com/oauth2/authorize
+getAccessTokenPath=https://idm.example.com/token/codeInfo
+client_id=shr
+client_secret=
+response_type=code
+ssoUrl=https://shr.example.com/shr/api/oAToSHR
+serverName=https://shr.example.com
+idmBaseUrl=https://idm.example.com
+```
+
+## Dynamic redirect (`state`)
+
+`state` is **dynamic per request**, not fixed:
+
+1. `redirect` param (preferred) → URL-encoded into OAuth `state`
+2. fallback `service` param (CAS-style, used by eassso bridge)
+3. default homepage if both absent
+
+Callback decodes `state` → `index2sso.jsp` `redirectTo`.
+
+## Troubleshooting
+
+| Symptom | Check |
+|---------|--------|
+| Servlet 500 on init | OASSOConfig path; `ssoUrl` key present (not only redirect_uri1/2) |
+| Token exchange fails | GET vs POST API; redirect_uri match |
+| User not found | person.cfidmusercode vs IDM user_info.id |
+| Redirect loop | UrlPattern excludes /api/oAToSHR, /index2sso.jsp |
+| Logout incomplete | Frontend follows idmLogoutUrl |
+
+Reference: [idm-servlet-reference.md](idm-servlet-reference.md)
+
+---
+
+# Option B-qy — Weaver E10 / DingTalk via qy.war (summary)
+
+Session = Option B (`index2sso` + LTPA). OAuth runs in **qy.war** under `/qy/auth/*`.
+
+BaoYPro prefers **explicit** `/qy/auth/weaver/app(.do)` (not UA-based Unified `/auth/app`).
+
+## Checklist
+
+```
+- [ ] integration jar in lib/sp; qy.war in easweb.ear + Filter + Spring config
+- [ ] integration-config: callbackUrl == IdP register (no .do); ecologyUrl / appId / secret
+- [ ] CallbackSuffixCompatFilter + Controller *.do mappings + suffix-pattern=false
+- [ ] Landing: WeaverShrLanding / writeShrSsoLoginForm POST to index2sso (never LTPA in Location)
+- [ ] Portal login_withForm.js → /qy/auth/weaver/app.do?appId&redirect=
+- [ ] logoutHR.js → /qy/auth/weaver/logout.do?appId&redirect= + Weaver service whitelist
+- [ ] AttackFilter: allow /qy/ (or accept CSRF risk) if external Referer blocks /qy
+- [ ] Prefer index2sso.jsp over OTP2sso.jsp for OAuth LTPA
+```
+
+## What actually fixed “must copy-paste URL to login”
+
+| Problem | Root cause | Fix |
+|---------|------------|-----|
+| Callback `/pa` 404 | Apusic needs `*.do` for external GET | Filter forward to `.do`; map both paths |
+| Paste address bar then login works | LTPA in Location multi-encoded (`%2525252B`) | Callback HTML **POST** form to index2sso — not GET Location |
+| Logout then stay in Weaver | `service` not on Weaver whitelist / wrong service | Whitelist SHR host; service = SSO entry URL |
+| External Referer → `/qy` 404 | Apusic AttackFilter CSRF | Whitelist `/qy/` + Weaver host |
+
+Reference: [weaver-oauth-qywar-reference.md](weaver-oauth-qywar-reference.md)
+
+---
+
+# Portal bridge (eassso/login)
+
+EAS portal sends users to `eassso/login?service={target}`. Patterns differ:
+
+| Pattern | Portal JS behavior | Who does OAuth |
+|---------|-------------------|----------------|
+| **A — direct** | `location.replace(serviceParam)` → SHR URL | SHR CAS filters |
+| **A — adsso button** | Link to `/shr?adsso` | SHR CAS filters |
+| **B — servlet bridge** | `location.replace(oaSsoUrl + '?redirect=' + encodeURIComponent(target))` | OAToSHR servlet |
+| **B-qy** | `/qy/auth/weaver/app.do?appId=&redirect=` | qy WeaverAuthController |
+
+Common whitelist: keep `/shr/home.do` on native eassso login for emergency local accounts.
+
+**Anti-patterns:**
+
+- Hardcoding env URLs in JS — inject from server config/JSP when possible
+- Editing only `runtime/.../sso_web.war` without src overlay — lost on redeploy
+- `needRedirect` when `service` empty — may force SSO on bare eassso/login
+- Mixing Option A filters with Option B OAToSHR on same entry
+- Putting LTPA `password` in 302 Location on Apusic (multi-encode)
+
+Full detail: [portal-login-reference.md](portal-login-reference.md), [weaver-oauth-qywar-reference.md](weaver-oauth-qywar-reference.md)
+
+---
+
+# Logout
+
+| Pattern | Mechanism |
+|---------|-----------|
+| Option A | Platform `/eassso/logout` chain; extend for IdP if needed |
+| Option B (IDM) | `SHRToIDMLogout` → JSON `{ idmLogoutUrl }`; `logoutHR.js` redirects browser |
+| Option B-qy (Weaver / BaoYPro) | `logoutHR.js` → `/qy/auth/weaver/logout.do?appId&redirect=` → E10 `/sp/integration/oauth2.0/logout?service=` where **service = SHR SSO entry**; Weaver **退出白名单** must include SHR host |
+| IDM spec | `{idmBaseUrl}/token/code-logout?redirect_url={encoded authorize URL}` |
+
+Keep logout URL config in **one place** where possible. BaoYPro: JS holds test/prod `weaverAppId` + `shrBaseUrl`; server builds SSO entry as `service` and HTML-encodes once (`WeaverShrLanding.writeLogoutThenService`).
+
+If address bar after logout shows `service=http://{ecology}/`, whitelist failed or old package — user will stay in Weaver after login.
+
+---
+
+# Option A ↔ Option B migration
+
+| From → To | Key changes |
+|-----------|-------------|
+| **B → A** | IDM redirect_uri: `/api/oAToSHR` → `/shr`; implement IdmCodeValidata; remove OAToSHR from login path; portal JS forwards to SHR URL not servlet; replace index2sso with assertion session |
+| **A → B** | Register OAToSHR; exclude paths in UrlPattern; IDM redirect_uri → servlet; portal JS → OAToSHR; add SHRToIDMLogout |
+| **B ↔ B-qy** | Different OAuth host (IDM servlet vs qy.war); same index2sso landing; portal JS target and IdP redirect_uri must match chosen pattern |
+
+Can run **dual entry** during migration if IdP registers two redirect_uri values; avoid long-term dual login paths.
+
+Detail: [migration-guide.md](migration-guide.md)
+
+---
+
+# Config field mapping (IDM)
+
+| OASSOConfig (Option B) | adSsoConfig (Option A) | Notes |
+|------------------------|------------------------|-------|
+| authorizePath | authUrl | IDM authorize endpoint |
+| getAccessTokenPath | tokenUrl | B: GET query; A: may need IdmCodeValidata |
+| ssoUrl | shrUrl + redirect_uri | B uses servlet path; A uses `/shr` |
+| serverName | shrUrl host | SHR base URL |
+| idmBaseUrl | (logout only) | code-logout endpoint |
+| client_id / client_secret | same | |
+| — | userUrl | Not needed if token response includes user_info |
+
+B-qy uses `integration-config*.json` (`callbackUrl`, `ecologyUrl`, `appId`) instead of OASSOConfig — see weaver reference.
+
+---
+
+# Search terms
+
+```
+# Option A
+KDPortalAuthenticationFilter, KDPortalTicketValidationFilter, OAuth20CodeValidata
+ConfigAddressUtil, adSsoConfig, adsso, SHRTicketValidationFilter
+
+# Option B
+OAToSHR, SsoUtil, SHRToIDMLogout, OASSOConfig, index2sso.jsp
+LtpaTokenManager, cfidmusercode
+
+# Option B-qy
+WeaverAuthController, WeaverShrLanding, CallbackSuffixCompatFilter
+/qy/auth/weaver/app, /qy/auth/weaver/logout, /qy/auth/callback/weaver
+integration-config, ecologyUrl, AttackFilter, serverNameWhiteList
+logoutHR.js, writeLogoutThenService, index2sso.jsp
+OTP2sso.jsp (do not use for Weaver OAuth LTPA)
+
+# Portal
+login_withForm.js, login_withForm.jsp, eassso/login, service, logoutHR.js
+ssoParamemter.properties, cas.client.UrlPattern
+```
+
+---
+
+# References
+
+| Doc | Content |
+|-----|---------|
+| [azure-oauth-reference.md](azure-oauth-reference.md) | Option A — Azure AD / GYPro |
+| [idm-filter-reference.md](idm-filter-reference.md) | Option A — IDM TicketValidator |
+| [idm-servlet-reference.md](idm-servlet-reference.md) | Option B — OAToSHR / YHPro |
+| [weaver-oauth-qywar-reference.md](weaver-oauth-qywar-reference.md) | Option B-qy — Weaver E10 / qy.war + POST index2sso |
+| [portal-login-reference.md](portal-login-reference.md) | eassso/login_withForm.js bridge |
+| [migration-guide.md](migration-guide.md) | A ↔ B migration steps |

+ 107 - 0
单点_待办skill/kdshrsso/azure-oauth-reference.md

@@ -0,0 +1,107 @@
+# Azure AD OAuth2 � Overlay Deploy Reference (GYPro Example)
+
+Reference for `kdshrsso` skill **Option A** (Azure/OIDC). For IDM on Filter overlay see [idm-filter-reference.md](idm-filter-reference.md). For portal eassso bridge see [portal-login-reference.md](portal-login-reference.md).
+
+## Source layout (GYPro)
+
+```
+src/com/kingdee/bos/sso/client/
+������ filter/
+��   ������ ConfigAddressUtil.java
+��   ������ authentication/KDPortalAuthenticationFilter.java
+��   ������ validation/
+��       ������ KDPortalTicketValidationFilter.java
+��       ������ OAuth20CodeValidata.java
+
+src/com/kingdee/customer/Auth/handler/
+������ ConfigAddressServletUtil.java    # workflow portal only, not /shr main chain
+
+login_withForm.jsp                   # EAS login "Office 365" button -> /shr?adsso
+```
+
+## Call chain
+
+```
+login_withForm.jsp
+  onclick -> https://{host}/shr?adsso
+
+KDPortalAuthenticationFilter.doFilter()
+  ������ /adsso -> flag=false, force OAuth
+  ������ ConfigAddressUtil.getProperty("authUrl|client_id|url")
+  ������ session.isAdauth = true
+  ������ session.urlToMap = original URL (redirect after login)
+  ������ redirect -> login.microsoftonline.com/.../authorize?...
+
+Callback GET /shr?code=xxx
+
+KDPortalTicketValidationFilter
+  ������ retrieveTicketFromRequest() -> code
+  ������ getTicketValidatorToSso() -> new OAuth20CodeValidata()
+  ������ super.doFilter()  // AbstractTicketValidationFilter template
+
+OAuth20CodeValidata.validate(code, url)
+  ������ getToken()  -> POST tokenUrl
+  ������ getUser()   -> GET userUrl (Graph /me)
+  ������ user = part before @ in userPrincipalName
+  ������ return AssertionImpl(AttributePrincipalImpl)
+
+onSuccessfulValidation()
+  ������ check T_PM_USER exists
+  ������ mobile -> index2.do?eid=&appid=&param=
+  ������ PC -> session.urlToMap.urlTo
+```
+
+## KDPortalAuthenticationFilter highlights
+
+- Redirect to IdP (~252-268): when `!flag`, build authorize URL from `authUrl`
+- `retrieveTicketFromRequest` (~276-278): use `code` param as ticket
+- `flag` whitelist (~109-128): local login paths excluded; `/adsso` forces OAuth
+
+## KDPortalTicketValidationFilter highlights
+
+- `getTicketValidatorToSso` (~94-99): if `code` present -> `new OAuth20CodeValidata()`
+- `retrieveTicketFromRequest` (~51-71): call `setTicketValidator` before validate
+- Excluded paths still use CAS validator: viewProcessDiagram.jsp, dynamicPage.do, common/tree.do
+
+## OAuth20CodeValidata highlights
+
+- `validate()`: code -> access_token -> userPrincipalName -> Assertion
+- Assertion attrs: dataCenter, locale, userAuthPattern=aseAD, sso.user.mapping=true
+- Fix hardcoded client_id/secret in `refreshToken()` �� use config
+
+## Platform classes (in jars, do not patch)
+
+| Class | Jar | Relation |
+|-------|-----|----------|
+| SHRTicketValidationFilter | shr_syssetting_web.jar | extends KDPortalTicketValidationFilter |
+| AbstractTicketValidationFilter | cas-client | unchanged parent template |
+
+## Environment URLs (GTIIT example)
+
+| Env | redirect_uri | client_id |
+|-----|--------------|-----------|
+| Test | https://gtiit.test.kdeascloud.com/shr | test app registration |
+| Prod | https://gtiit.kdeascloud.com/shr | prod app registration |
+
+Tenant example: `28f285d8-5c78-4d60-9c7f-993071841cbd`
+
+## web.xml (on server)
+
+In `shr.war/WEB-INF/web.xml` find Authentication and Validation filters; Authentication must run before Validation.
+
+## ConfigAddressUtil vs ConfigAddressServletUtil
+
+| Class | Purpose |
+|-------|---------|
+| ConfigAddressUtil | SSO Filter chain (authUrl, token, OAuth) |
+| ConfigAddressServletUtil | UserEasAuthServlet workflow; only dataCenter/locale |
+
+Both read `adSsoConfig.properties`; different entry points.
+
+## Related docs
+
+| Doc | When |
+|-----|------|
+| [idm-filter-reference.md](idm-filter-reference.md) | Same Option A pattern for ?? IDM |
+| [portal-login-reference.md](portal-login-reference.md) | eassso/login_withForm.js bridge |
+| [migration-guide.md](migration-guide.md) | Switch between Option A and B |

+ 173 - 0
单点_待办skill/kdshrsso/idm-filter-reference.md

@@ -0,0 +1,173 @@
+# IDM OAuth �� Option A Filter Overlay Reference
+
+Reference for adapting **Option A** (Filter overlay) to enterprise IDM (���Ƶ�), as used in YHPro context. Not the same as Azure `OAuth20CodeValidata`.
+
+## When to choose this over Option B
+
+| Choose Option A (IDM Filter) | Stay Option B (OAToSHR servlet) |
+|-------------------------------|----------------------------------|
+| All `/shr/*` URLs should auto-trigger IDM OAuth | Only portal/OA external links need SSO |
+| Want `_const_cas_assertion_` (platform session) | Already working with index2sso + LtpaToken |
+| Will remove eassso �� servlet bridge | Need `SHRToIDMLogout` JSON pattern |
+| Can change IDM redirect_uri to `/shr` | IDM locked to servlet callback URL |
+
+---
+
+## IDM vs Azure API differences
+
+| Step | Azure (OAuth20CodeValidata) | IDM (YHPro / IdmCodeValidata) |
+|------|----------------------------|-------------------------------|
+| Authorize | GET authUrl + client_id + redirect_uri | GET authorizePath + response_type=code + scope=server + state + redirect_uri |
+| Token | POST tokenUrl (form body) | **GET** getAccessTokenPath?code&client_id&client_secret&redirect_uri |
+| User info | GET userUrl (separate) | **In token response** `data.user_info.id` |
+| Callback URL | `https://{host}/shr` | Must register `{host}/shr` (not `/api/oAToSHR`) |
+| User mapping | email prefix / loginName | `person.cfidmusercode = user_info.id` |
+
+Reuse logic from YHPro `SsoUtil.getAccessToken()` and `checkUserExist()` inside `TicketValidator.validate()`.
+
+---
+
+## Source layout (target)
+
+```
+src/com/kingdee/bos/sso/client/
+������ filter/
+��   ������ ConfigAddressUtil.java          # read adSsoConfig.properties
+��   ������ authentication/
+��   ��   ������ KDPortalAuthenticationFilter.java
+��   ������ validation/
+��       ������ KDPortalTicketValidationFilter.java
+��       ������ IdmCodeValidata.java        # NOT OAuth20CodeValidata
+
+src/org/jasig/cas/client/Protocol.java   # optional: YH("code","redirect_uri")
+```
+
+YHPro WIP classes (`com.kingdee.eas.custom.sso.filter.*`) are **migration reference only** �� production path was servlet. Do not wire both.
+
+---
+
+## Call chain
+
+```
+GET /shr/dynamic.do?uipk=...&AssignmentId=...
+  �� KDPortalAuthenticationFilter
+       no assertion �� save original URL (urlToMap or state)
+       �� 302 IDM authorizePath?...&state=encode(originalUrl)&redirect_uri=https://host/shr
+  �� IDM login
+  �� GET /shr?code=xxx&state=...
+  �� KDPortalTicketValidationFilter
+       retrieveTicketFromRequest() �� code
+       getTicketValidatorToSso() �� new IdmCodeValidata()
+  �� IdmCodeValidata.validate(code, service)
+       GET getAccessTokenPath?code&client_id&client_secret&redirect_uri
+       parse data.user_info.id
+       SQL: person.cfidmusercode �� T_PM_USER.FNumber
+       return AssertionImpl(AttributePrincipalImpl(FNumber))
+  �� onSuccessfulValidation
+       redirect to decoded state / urlToMap
+  �� session._const_cas_assertion_
+```
+
+---
+
+## IdmCodeValidata sketch
+
+Adapt from YHPro `SsoUtil` �� do not copy Azure POST token flow.
+
+```java
+public Assertion validate(String code, String service) throws TicketValidationException {
+    // 1. GET getAccessTokenPath with code, client_id, client_secret, redirect_uri=shrUrl
+    // 2. Parse JSON: code=="0" �� data.user_info.id
+    // 3. checkUserExist(id) via cfidmusercode SQL
+    // 4. return new AssertionImpl(new AttributePrincipalImpl(userNumber), attrs);
+}
+```
+
+---
+
+## KDPortalAuthenticationFilter (IDM-specific)
+
+Build authorize URL like OAToSHR.authorize():
+
+```java
+params: response_type=code, scope=server, client_id, state=URLEncoder.encode(originalUrl), redirect_uri=shrUrl
+redirect �� authUrl + "?" + params
+```
+
+Store `originalUrl` from request before redirect:
+
+- Full request URL with query string (todo deep link), or
+- `session.urlToMap` pattern from Azure reference
+
+Whitelist in `flag` / `FilterUtil.isUnFilter`:
+
+- `/shr/home.do` �� local login (matches YHPro JS keepLoginPath)
+- Product paths already in `cas.client.UrlPattern`
+
+---
+
+## adSsoConfig.properties (IDM)
+
+```properties
+authUrl=https://idm.example.com/oauth2/authorize
+tokenUrl=https://idm.example.com/token/codeInfo
+client_id=shr
+client_secret=
+shrUrl=https://shr.example.com/shr
+dataCenter=
+locale=L2
+# userUrl not required �� user in token response
+idmBaseUrl=https://idm.example.com
+```
+
+Map from OASSOConfig: `authorizePath��authUrl`, `getAccessTokenPath��tokenUrl`, `serverName/shr��shrUrl`.
+
+---
+
+## Portal integration after Option A
+
+Remove OAToSHR from login path. Change `login_withForm.js`:
+
+```javascript
+// Instead of oaSsoUrl redirect:
+window.top.location.replace(serviceParam);  // SHR filters handle OAuth
+```
+
+Or portal menus link directly to SHR URLs.
+
+---
+
+## Logout with Option A + IDM
+
+Platform logout chain clears SHR/EAS sessions. IDM federated logout still needed:
+
+- Keep `SHRToIDMLogout` servlet as logout-only endpoint, or
+- Extend logout filter to return idmLogoutUrl, or
+- Frontend `logoutHR.js` calls servlet (not hardcoded IDM URLs)
+
+---
+
+## Migration from YHPro Option B
+
+```
+1. Implement IdmCodeValidata (port SsoUtil token + user SQL)
+2. Overlay KDPortal* filters
+3. IDM admin: add redirect_uri https://host/shr (keep servlet URI during transition if needed)
+4. Change login_withForm.js: service �� SHR direct (remove OAToSHR)
+5. Test /shr?code= callback through validation filter
+6. Remove OAToSHR from web.xml when stable (or keep for backward compat temporarily)
+7. Remove /api/oAToSHR from UrlPattern if servlet removed
+8. Verify logout end-to-end
+```
+
+---
+
+## Troubleshooting
+
+| Symptom | Check |
+|---------|--------|
+| /shr?code= not validated | Validation filter order; code in retrieveTicketFromRequest |
+| Token GET fails | IdmCodeValidata uses GET not POST; redirect_uri = /shr not servlet |
+| User not found | cfidmusercode mapping; same SQL as Option B |
+| Deep link lost | state / urlToMap saved before authorize |
+| Dual login paths | Disable OAToSHR when Filter path active |

+ 226 - 0
单点_待办skill/kdshrsso/idm-servlet-reference.md

@@ -0,0 +1,226 @@
+# IDM Portal SSO �� Servlet + LtpaToken Reference (YHPro)
+
+Reference for `kdshrsso` skill **Option B**. Adapt paths, IdP URLs, and user-mapping field per project.
+
+Related: [portal-login-reference.md](portal-login-reference.md), [migration-guide.md](migration-guide.md)
+
+## Source layout (YHPro)
+
+```
+src/com/kingdee/eas/custom/sso/
+������ OAToSHR.java              # SHR portal SSO entry servlet
+������ SHRToIDMLogout.java       # Federated logout (returns idmLogoutUrl JSON)
+������ OAToMBos.java             # Mobile MBOS SSO (separate config)
+������ SsoUtil.java              # Config, HTTP, user lookup, bill validation
+������ utils/ExpiringMapCache.java
+������ filter/authentication/PortalAuthenticationFilter.java   # WIP �� not in web.xml
+������ filter/validate/TicketValidationFilter.java             # WIP �� not in web.xml
+������ validation/PortalServiceTicketValidator.java            # WIP validator
+
+src/org/jasig/cas/client/Protocol.java   # Protocol.YH("code","redirect_uri")
+
+runtime/server/properties/yuhu/OASSOConfig.properties
+runtime/server/deploy/easweb.ear/shr_web.war/WEB-INF/web.xml
+runtime/server/deploy/easweb.ear/shr_web.war/WEB-INF/sso/ssoParamemter.properties
+runtime/server/deploy/eas.ear/sso_web.war/common/js/login_withForm.js   # portal bridge
+runtime/server/deploy/easweb.ear/shr_web.war/scripts/gui/logoutHR.js    # SHR logout
+```
+
+---
+
+## web.xml (SHR servlets)
+
+```xml
+<!-- SSO ��ʼ -->
+<servlet>
+    <servlet-name>OAToSHR</servlet-name>
+    <servlet-class>com.kingdee.eas.custom.sso.OAToSHR</servlet-class>
+</servlet>
+<servlet-mapping>
+    <servlet-name>OAToSHR</servlet-name>
+    <url-pattern>/api/oAToSHR</url-pattern>
+</servlet-mapping>
+
+<servlet>
+    <servlet-name>SHRToIDMLogout</servlet-name>
+    <servlet-class>com.kingdee.eas.custom.sso.SHRToIDMLogout</servlet-class>
+</servlet-mapping>
+<servlet-mapping>
+    <servlet-name>SHRToIDMLogout</servlet-name>
+    <url-pattern>/api/SHRToIDMLogout</url-pattern>
+</servlet-mapping>
+```
+
+CAS filters unchanged (`casAuthenticationFilter` / `casTicketValidationFilter`). Exclude in `ssoParamemter.properties`:
+
+```
+cas.client.UrlPattern=...;/index2sso.jsp;...;/api/oAToSHR;/api/SHRToIDMLogout;/api/oatombos;
+```
+
+---
+
+## Full call chain (with portal)
+
+```
+Portal CAS
+  �� eassso/login?service=https://shr.../shr/dynamic.do?...&AssignmentId=...
+  �� login_withForm.js intercept
+  �� /shr/api/oAToSHR?service=encodeURIComponent(service)
+       (portal may also use ?redirect= for direct links)
+
+OAToSHR.doPost()
+  ������ no code �� authorize()
+  ��     dynamicRedirect = redirect | service | default homepage
+  ��     state = URLEncoder.encode(dynamicRedirect)   �� dynamic, not fixed
+  ��     302 �� IDM authorizePath?response_type=code&scope=server&client_id&state&redirect_uri=ssoUrl
+  ������ code �� callBack()
+        decode state �� redirectUrl
+        GET getAccessTokenPath?code&client_id&client_secret&redirect_uri
+        user_info.id �� checkUserExist(cfidmusercode) �� T_PM_USER.FNumber
+        302 �� /shr/index2sso.jsp?username&password(Ltpa)&redirectTo
+
+index2sso.jsp �� SHR session �� redirectUrl (todo / homepage)
+```
+
+---
+
+## Dynamic redirect (`state`)
+
+| Stage | Behavior |
+|-------|----------|
+| Input | `redirect` (1st) or `service` (2nd) on OAToSHR request |
+| Authorize | `state = URLEncoder.encode(targetUrl)` �� **changes every request** |
+| Callback | `URLDecoder.decode(state)` �� final redirectTo |
+| Default | `{serverName}/shr/dynamic.do?uipk=shr.perself.homepage` |
+
+---
+
+## Logout chain
+
+```
+logoutHR.js
+  �� platform logout AJAX chain (/eassso/logout, shr_loginout, easweb/logout, ...)
+  �� clear cookies / storage
+  �� redirect to idmLogoutUrl (built in JS or from servlet �� prefer servlet)
+
+SHRToIDMLogout (POST /shr/api/SHRToIDMLogout)
+  �� build IDM authorize URL with state = portal home
+  �� idmLogoutUrl = {idmBaseUrl}/token/code-logout?redirect_url={encoded authorize URL}
+  �� session.invalidate()
+  �� JSON { code:"1", idmLogoutUrl:"..." }
+```
+
+**Improvement:** have `logoutHR.js` call `SHRToIDMLogout` instead of duplicating IDM URL in JS.
+
+---
+
+## Config template
+
+Path: `{EAS_HOME}/server/properties/yuhu/OASSOConfig.properties`
+
+```properties
+authorizePath=https://idm-test.example.com/oauth2/authorize
+getAccessTokenPath=https://idm-test.example.com/token/codeInfo
+client_id=shr
+client_secret=
+response_type=code
+ssoUrl=https://shr-test.example.com/shr/api/oAToSHR
+serverName=https://shr-test.example.com
+idmBaseUrl=https://idm-test.example.com
+lastRedirectUrl=/shr/home.do
+```
+
+**Production pitfall:** sample file had `redirect_uri1/2` for MBOS but `OAToSHR` requires **`ssoUrl`** (strict). Missing `ssoUrl` causes init/runtime errors.
+
+---
+
+## User mapping SQL
+
+```sql
+SELECT uinfo.FNumber AS userNum
+FROM T_bd_person person
+LEFT JOIN T_PM_USER uinfo ON person.fid = uinfo.FPERSONID
+WHERE uinfo.FIsDelete = '0'
+  AND uinfo.FForbidden = '0'
+  AND person.cfidmusercode = ?
+```
+
+IDM field: `data.user_info.id` from token response.
+
+---
+
+## IDM token API
+
+GET `{getAccessTokenPath}?code=&client_id=&client_secret=&redirect_uri=`
+
+```json
+{ "code": "0", "data": { "access_token": "...", "user_info": { "id": "...", "username": "..." } } }
+```
+
+---
+
+## Portal login_withForm.js (YHPro bridge)
+
+See [portal-login-reference.md](portal-login-reference.md) for full analysis.
+
+Summary:
+
+- IIFE at top of `login_withForm.js` runs before login form renders
+- Whitelist `/shr/home.do` for native eassso login
+- Other SHR paths �� `oaSsoUrl + "?service=" + encodeURIComponent(serviceParam)`
+- **Anti-patterns found:** hardcoded ENV_CONFIG, runtime-only edit, prod URL placeholder, loose whitelist `includes()`
+
+---
+
+## OAToMBos (mobile, separate)
+
+Uses `ExpiringMapCache` �� state carries cache key not full URL:
+
+```
+state = ssoUrl + "?redirect=" + cacheKey
+```
+
+Do not confuse with OAToSHR direct URL-in-state pattern.
+
+---
+
+## Deploy checklist
+
+```
+- [ ] Compile src/com/kingdee/eas/custom/sso/**
+- [ ] Deploy to shr_web.war/WEB-INF/classes
+- [ ] web.xml servlets + ssoParamemter.properties UrlPattern
+- [ ] OASSOConfig.properties on server (include ssoUrl, idmBaseUrl)
+- [ ] IDM redirect_uri = ssoUrl
+- [ ] person.cfidmusercode for all SSO users
+- [ ] login_withForm.js overlay in sso_web.war (src tracked, not runtime-only)
+- [ ] logoutHR.js calls SHRToIDMLogout or shares config
+- [ ] Test: portal todo, home.do whitelist, logout
+```
+
+---
+
+## Troubleshooting
+
+| Symptom | Check |
+|---------|--------|
+| 500 on servlet init | OASSOConfig missing; ssoUrl empty |
+| authorize fails | authorizePath, client_id, IDM app registration |
+| Token exchange fails | GET API; redirect_uri must match authorize |
+| User not found | cfidmusercode vs user_info.id; duplicate rows |
+| Login page flash | mask + replace(); onbeforeunload cleared |
+| index2sso loop | UrlPattern excludes servlet + index2sso.jsp |
+| Deep link wrong | state decode; redirect vs service param |
+| Logout 401 IDM | idmLogoutUrl spec; redirect_url encoding |
+
+---
+
+## WIP filter code (not production)
+
+`PortalAuthenticationFilter` + `TicketValidationFilter` + `PortalServiceTicketValidator`:
+
+- Mirror Option A structure but **not wired in web.xml**
+- Hardcoded test URLs; POST token vs GET in OAToSHR
+- Use only as reference for [idm-filter-reference.md](idm-filter-reference.md) migration
+
+Production login path: **OAToSHR servlet only**.

+ 140 - 0
单点_待办skill/kdshrsso/migration-guide.md

@@ -0,0 +1,140 @@
+# Option A ? Option B Migration Guide
+
+When to migrate, what breaks, and minimal steps. Based on YHPro analysis.
+
+## Comparison at a glance
+
+| Item | Option B (current YHPro) | Option A (target) |
+|------|--------------------------|-------------------|
+| IDM redirect_uri | `.../shr/api/oAToSHR` | `.../shr` |
+| OAuth callback | Servlet `callBack()` | `/shr?code=` �� ValidationFilter |
+| Session | LtpaToken + index2sso.jsp | `_const_cas_assertion_` |
+| Portal JS | �� OAToSHR?service= | �� SHR URL direct or /shr?adsso |
+| Deep link | OAuth state (dynamic) | state or urlToMap (dynamic) |
+| Config file | OASSOConfig.properties | adSsoConfig.properties |
+| Logout | SHRToIDMLogout + logoutHR.js | Platform logout + IDM extension |
+| CAS UrlPattern | Excludes servlet, index2sso | Standard filter chain |
+
+---
+
+## B �� A migration
+
+### Prerequisites
+
+- IDM admin can register new redirect_uri (`https://{host}/shr`)
+- Dev/test environment for parallel validation
+- User mapping unchanged (`cfidmusercode`)
+
+### Steps
+
+```
+Phase 1 �� Build Option A
+  [ ] IdmCodeValidata (port SsoUtil.getAccessToken + checkUserExist)
+  [ ] KDPortalAuthenticationFilter (IDM authorize + state/urlToMap)
+  [ ] KDPortalTicketValidationFilter (code �� IdmCodeValidata)
+  [ ] ConfigAddressUtil + adSsoConfig.properties
+  [ ] Overlay deploy to shr_web.war/WEB-INF/classes
+  [ ] Protocol.YH patch if needed (code, redirect_uri)
+
+Phase 2 �� IDM
+  [ ] Register redirect_uri = https://{host}/shr
+  [ ] Keep servlet redirect_uri during transition (optional)
+
+Phase 3 �� Portal
+  [ ] login_withForm.js: replace OAToSHR redirect with location.replace(serviceParam)
+  [ ] Or portal menus �� direct SHR links
+  [ ] Remove hardcoded ENV_CONFIG from JS; inject from server
+  [ ] Keep /shr/home.do whitelist (move to Filter flag)
+
+Phase 4 �� Validate
+  [ ] Portal todo deep link
+  [ ] Direct /shr access without session
+  [ ] /shr/home.do local login
+  [ ] Session has _const_cas_assertion_
+  [ ] Logout clears SHR + IDM
+
+Phase 5 �� Decommission B
+  [ ] Remove OAToSHR servlet from web.xml (or deprecate)
+  [ ] Remove servlet paths from UrlPattern if unused
+  [ ] Remove IDM servlet redirect_uri
+  [ ] Archive OASSOConfig or merge into adSsoConfig
+```
+
+### Rollback
+
+Keep OAToSHR servlet and dual redirect_uri in IDM until Option A verified. Portal JS switch is one flag:
+
+```javascript
+mode: "servlet"  // �� OAToSHR
+mode: "direct"   // �� Option A
+```
+
+---
+
+## A �� B migration
+
+Use when servlet/Ltpa approach required (e.g. IDM mandates servlet callback, or federated logout JSON already built).
+
+```
+  [ ] Implement OAToSHR + SsoUtil + SHRToIDMLogout
+  [ ] OASSOConfig.properties
+  [ ] web.xml servlet mappings
+  [ ] ssoParamemter.properties: exclude /api/oAToSHR, /index2sso.jsp
+  [ ] IDM redirect_uri �� servlet URL
+  [ ] login_withForm.js �� OAToSHR bridge
+  [ ] logoutHR.js �� SHRToIDMLogout JSON
+  [ ] Revert or disable Filter overlay classes
+```
+
+---
+
+## What not to do
+
+| Mistake | Why |
+|---------|-----|
+| Run OAToSHR + Filter overlay on same user journey | Two session models (Ltpa vs assertion) |
+| Same redirect_uri for /shr and /api/oAToSHR | IDM allows multiple URIs but each OAuth flow must be consistent |
+| Keep login_withForm �� OAToSHR after Option A active | Double redirect; wrong callback |
+| Hardcode URLs in JS during migration | Use server-injected config + mode flag |
+| Delete OASSOConfig before adSsoConfig validated | Config field names differ |
+
+---
+
+## Config migration table
+
+Copy values when moving B �� A:
+
+| OASSOConfig | adSsoConfig |
+|-------------|-------------|
+| authorizePath | authUrl |
+| getAccessTokenPath | tokenUrl |
+| client_id | client_id |
+| client_secret | client_secret |
+| serverName + `/shr` | shrUrl |
+| idmBaseUrl | idmBaseUrl (logout) |
+| ssoUrl | **do not copy** �� A uses shrUrl as redirect_uri |
+
+---
+
+## YHPro WIP filter code
+
+`com.kingdee.eas.custom.sso.filter.PortalAuthenticationFilter` was an abandoned Option A attempt:
+
+- Hardcoded `shr-test.iyuhu.com/api/oAToSHR`
+- Not in web.xml
+- POST token API unlike production OAToSHR GET
+
+**Do not enable as-is.** Port useful ideas into proper `com.kingdee.bos.sso.client` overlay per [idm-filter-reference.md](idm-filter-reference.md).
+
+---
+
+## Decision: migrate or stay?
+
+| Stay Option B | Migrate to Option A |
+|---------------|---------------------|
+| Production stable | Want platform-native SHR session |
+| IDM redirect_uri fixed on servlet | Can change IDM to /shr callback |
+| Team familiar with OAToSHR | Reducing eassso JS hack long-term |
+| Short timeline | Willing to invest Filter overlay + regression |
+
+Option B with **proper engineering** (src overlay, server config injection, unified logout servlet) is valid long-term if Option A cost outweighs benefit.

+ 201 - 0
单点_待办skill/kdshrsso/portal-login-reference.md

@@ -0,0 +1,201 @@
+# EAS Portal Login Bridge �� login_withForm.js Reference
+
+Reference for `kdshrsso` skill. Describes how `eassso/login` interacts with SHR SSO (Option A or B).
+
+## Context
+
+EAS portal CAS flow sends unauthenticated users to:
+
+```
+/eassso/login?service=https://{shr-host}/shr/{path}?...
+```
+
+Loaded by `login_withForm.jsp`:
+
+```jsp
+<script src="<%= EASWebContextUtil.render(request,"/common/js/login_withForm.js") %>"></script>
+```
+
+Default behavior: show username/password form. Customizations intercept before `initLoginFormPattern()` runs.
+
+---
+
+## Pattern 1 �� Option B servlet bridge (YHPro)
+
+**Goal:** Hide eassso login for SHR; redirect to `OAToSHR` with target URL in `service` or `redirect`.
+
+```
+eassso/login?service=SHR_TODO_URL
+  �� login_withForm.js IIFE (top of file)
+  �� /shr/api/oAToSHR?service=encodeURIComponent(SHR_TODO_URL)
+  �� OAToSHR puts target into OAuth state
+  �� IDM �� index2sso.jsp �� SHR_TODO_URL
+```
+
+### YHPro logic (simplified)
+
+```javascript
+var ENV_CONFIG = {
+    keepLoginPath: "/shr/home.do",   // whitelist: show native login
+    shrRootPath: "/shr/",
+    test: { oaSsoUrl: "https://shr-test.example.com/shr/api/oAToSHR" }
+};
+
+var serviceParam = getUrlParam("service");
+var originalPath = getOriginalPathFromService(serviceParam);
+var isKeepLogin = originalPath === ENV_CONFIG.keepLoginPath
+               || (serviceParam && serviceParam.includes(ENV_CONFIG.keepLoginPath));
+var needRedirect = !isKeepLogin
+    && (originalPath.includes("/shr/") || originalPath === ""
+        || location.pathname.includes("/eassso/login"));
+
+if (needRedirect) {
+    var callbackUrl = serviceParam || (shrBaseUrl + "/shr/");
+    location.replace(oaSsoUrl + "?service=" + encodeURIComponent(callbackUrl));
+}
+```
+
+### Parameter choice
+
+| Param to OAToSHR | OAToSHR reads | Recommendation |
+|------------------|---------------|----------------|
+| `redirect` | 1st priority | Preferred for portal direct links |
+| `service` | 2nd priority | Used by eassso CAS bridge |
+
+`state` in OAuth is **dynamic**: encoded value of redirect/service URL.
+
+### UX additions (YHPro)
+
+- Full-screen mask `#sso-jump-mask` before redirect (avoid login page flash)
+- `window.onbeforeunload = null` �� prevent EAS blocking redirect
+- `location.replace()` �� no back-button to login page
+- 3s mask timeout �� fallback if redirect fails
+
+---
+
+## Pattern 2 �� Option A direct forward
+
+**Goal:** Let SHR CAS filters handle OAuth; no servlet.
+
+```javascript
+if (isShrService(serviceParam) && !isKeepLogin) {
+    window.top.location.replace(serviceParam);  // go to SHR URL directly
+}
+```
+
+Flow:
+
+```
+eassso/login?service=https://shr/.../dynamic.do?...
+  �� JS replace �� SHR URL
+  �� KDPortalAuthenticationFilter (no assertion) �� IDM
+  �� /shr?code= �� validation �� target page
+```
+
+Portal menus can also link **directly** to SHR URLs, skipping eassso/login entirely.
+
+---
+
+## Pattern 3 �� Option A adsso button (GYPro / Azure)
+
+`login_withForm.jsp` adds Office 365 button:
+
+```javascript
+onclick="window.location.href='https://{host}/shr?adsso'"
+```
+
+Filter treats `/adsso` as force-OAuth entry; stores original URL in session before IdP redirect.
+
+---
+
+## Whitelist local login
+
+Keep emergency / admin path on native eassso form:
+
+| Path | Typical purpose |
+|------|-----------------|
+| `/shr/home.do` | Local DB login bypass (YHPro) |
+| Product default | Specific JSP paths in Filter `flag` (Option A) |
+
+Implement whitelist by **exact pathname match**, not loose `includes()`.
+
+---
+
+## Recommended implementation (either pattern)
+
+### Do
+
+1. Put custom logic in **separate** `login_sso_redirect.js`; include it in JSP **before** `login_withForm.js`
+2. Inject config from server:
+
+```jsp
+<script>
+window.SSO_PORTAL_CONFIG = {
+    oaSsoUrl: "<%= ssoUrl from properties %>",
+    keepLoginPath: "/shr/home.do",
+    mode: "servlet"  // or "direct"
+};
+</script>
+<script src="/common/js/login_sso_redirect.js"></script>
+<script src="/common/js/login_withForm.js"></script>
+```
+
+3. Keep source under project `src/` or overlay build �� not only `runtime/server/deploy/...`
+4. Use one config file shared with Java (`OASSOConfig` / `adSsoConfig`)
+
+### Do not
+
+| Anti-pattern | Risk |
+|--------------|------|
+| Hardcode dev/test/prod URLs in JS | Prod misconfig; triple maintenance with Java |
+| Edit 1500-line platform file in runtime war only | Lost on redeploy / upgrade |
+| `needRedirect` when `service` is empty | Bare `/eassso/login` forced to SHR SSO |
+| `serviceParam.includes(keepLoginPath)` | Loose match bypass |
+| Prod placeholder URL (`��������OA�����¼��ַ`) | Production outage |
+| Duplicate logout URLs in JS + Java + servlet | Inconsistent federated logout |
+
+---
+
+## logoutHR.js (Option B companion)
+
+SHR logout extended for IDM federated logout:
+
+```
+logoutHR()
+  �� /eassso/logout + shr_loginout + easweb/logout (platform chain)
+  �� clear cookies / storage
+  �� redirect to idmLogoutUrl (or portal home)
+```
+
+IDM logout URL built per spec:
+
+```
+{idmBaseUrl}/token/code-logout?redirect_url={encodeURIComponent(authorizeUrlWithParams)}
+```
+
+**Prefer:** call `/shr/api/SHRToIDMLogout` servlet (server builds URL from properties) instead of rebuilding URL in JS.
+
+YHPro `logoutHR.js` currently duplicates IDM URL logic in frontend �� refactor to servlet JSON response.
+
+---
+
+## File locations (typical)
+
+| File | War | Role |
+|------|-----|------|
+| `common/js/login_withForm.js` | `sso_web.war` (eas.ear) | Portal login page logic |
+| `login_withForm.jsp` | `sso_web.war` | Loads JS |
+| `scripts/gui/logoutHR.js` | `shr_web.war` | SHR logout |
+| `sso/ssoParamemter.properties` | `shr_web.war` | CAS UrlPattern exclusions |
+
+---
+
+## Test checklist
+
+```
+- [ ] Portal menu �� SHR todo: lands on todo after IDM login
+- [ ] /shr/home.do (whitelist): shows eassso login form
+- [ ] Direct /shr URL without session: OAuth (A) or needs portal link (B)
+- [ ] Logout: SHR session cleared + IDM session cleared
+- [ ] Upgrade sso_web.war: custom JS preserved via overlay
+```

+ 202 - 0
单点_待办skill/kdshrsso/weaver-oauth-qywar-reference.md

@@ -0,0 +1,202 @@
+# Option B variant: Weaver E10 OAuth via qy.war (BaoYPro)
+
+Session model stays **Option B** (`index2sso.jsp` + `LtpaTokenManager`). OAuth lives in custom **qy.war** (`/qy/auth/...`) under Kingdee `lib/sp` + `easweb.ear/qy.war`.
+
+Primary reference project: **BaoYPro** (报业 / 泛微 E10). Prefer **explicit** `/qy/auth/weaver/...` over Unified `/qy/auth/app` (UA detection).
+
+**New project:** complete **Agent intake** in [SKILL.md](SKILL.md) before coding.
+
+---
+
+## When to use
+
+| Scenario | Fit |
+|----------|-----|
+| 泛微 E10 OAuth2 门户 / 待办进 sHR | Yes |
+| 钉钉 OAuth 进 sHR(显式 `/auth/dingtalk`) | Yes — same POST index2sso landing |
+| IdP redirect_uri must stay without `.do` | Yes (Filter forwards internally) |
+| Rely on Unified `/auth/app` + User-Agent | No for PC deep links / Weaver todos |
+| All `/shr` URLs auto OAuth without click entry | No — use Option A |
+| OTP / WeChart OTP auto login | No — use `OTP2sso.jsp`, not this |
+
+---
+
+## BaoYPro end-to-end (current)
+
+```
+Portal eassso/login?service=SHR_URL
+  → sso_web login_withForm.js (keep /shr/home.do native)
+  → /qy/auth/weaver/app.do?appId=&redirect=
+  → state=Base64Url(redirect|appType) → 泛微 authorize
+  → callback /qy/auth/callback/weaver/pa?code&state
+  → CallbackSuffixCompatFilter → forward pa.do
+  → code → accessToken → profile → map sHR user (PERSON_NUMBER)
+  → WeaverShrLanding.writePostForm → HTML POST /shr/index2sso.jsp
+  → SHR session → redirectTo
+```
+
+Todo / logout deep links use the **same** `/qy/auth/weaver/app.do` entry (not Unified `/auth/app`).
+
+### Config
+
+| Source | Role |
+|--------|------|
+| `INTEGRATION_CONFIG_PATH` → `integration-config-*.json` | `common.*`, `oauth.apps[]`, `weaver.ecologyUrl` + OAuth paths |
+| `weaver.ecologyUrl` | authorize / accessToken / profile / **logout** (IdP) |
+| `oauth.apps[].callbackUrl` | IdP redirect_uri (**no** `.do`) |
+| `oauth.apps[].appId` / `appSecret` | E10 client_id / secret |
+| `desktopRedirectType=TO_SHR` | PC → index2sso POST |
+
+Do **not** put Weaver OpenAPI push secrets in this JSON — those stay in `weaverTodoConfig.properties` (kdshrpmsg).
+
+Register with 泛微: **exact** `callbackUrl` string (protocol/host/port/path).
+
+---
+
+## UnifiedAuth `/auth/app` vs WeaverAuthController
+
+| | `UnifiedAuthController` `/auth/app` | `WeaverAuthController` `/auth/weaver/app` |
+|--|--|--|
+| Platform select | **User-Agent** detectors | Explicit weaver + `appId` |
+| PC browser from Weaver todo | Often `unsupported_client` | Works |
+| Landing | Base `doCallback` often **302 Location + LTPA** | **POST** form (`WeaverShrLanding`) |
+| Minimize base edits | Needs UA / landing hooks in base | Keep changes in weaver package |
+
+**Rule:** PC 深链 / 待办 / 门户桥 → `/qy/auth/weaver/app(.do)`.  
+App WebView + reliable UA → Unified `/auth/app` OK for DingTalk-in-app.
+
+Future DingTalk todo/SSO: add `/qy/auth/dingtalk/...` (explicit), reuse POST index2sso; do not force everything through UA-based `/auth/app`.
+
+---
+
+## Two hard Apusic problems (must fix both)
+
+### 1) Callback 404 without `.do`
+
+| Piece | Role |
+|-------|------|
+| `CallbackSuffixCompatFilter` on `/auth/callback/*` | forward to `.do`, query preserved |
+| Controller | Map both `/callback/.../pa` and `/pa.do` |
+| `suffix-pattern="false"` | `pa` is path var |
+| `web.xml` | `*.do` and `/` |
+
+### 2) Copy-paste required to login (`%2525252B`)
+
+LTPA in **302 Location** → Apusic multi-encode.  
+**Fix:** HTML form **POST** to `/shr/index2sso.jsp` (`WeaverShrLanding.writePostForm` / base `writeShrSsoLoginForm`). Never put LTPA password in Location.
+
+---
+
+## Apusic AttackFilter (Referer / “跨域” 404)
+
+Not classic CORS. Global filter:
+
+`{EAS_HOME}/apusic/domains/server1/config/web.xml` → `com.apusic.util.AttackFilter`
+
+| Param | Role |
+|-------|------|
+| `enableCSRFCheck` | Master switch for Referer check |
+| `enableCSRFCheck_URL` | Allow URI via `allow_URLS` when CSRF on |
+| `serverNameWhiteList` | Referer host allow list |
+| `allow_URLS` | Path prefixes (include `/qy/` if CSRF on) |
+
+`shr_web` `enableSHRRequestParamWrapFilter` is **not** cross-origin control (param wrap on `/shr` only).
+
+Symptom: external Referer (Weaver) → `/qy/...` 404/403; no Referer / same-host → OK.  
+Prefer whitelist `/qy/` + Weaver host over disabling CSRF globally.
+
+---
+
+## Federated logout (BaoYPro)
+
+### Desired UX
+
+1. Clear SHR/EAS local sessions (`logoutHR.js` product chain)
+2. Redirect to `/qy/auth/weaver/logout.do?appId=&redirect=`
+3. Server sets Weaver logout `service` = **SHR SSO entry**  
+   `http://{shr}/qy/auth/weaver/app.do?appId=&redirect=`
+4. Browser `encodeURIComponent(service)` then jump to  
+   `{ecology}/sp/integration/oauth2.0/logout?service=...`
+5. User sees Weaver login; after password → SSO entry → OAuth → SHR
+
+### Critical ops
+
+Weaver docs: **`service` domain must be on 退出重定向白名单**.  
+Otherwise Weaver rewrites to ecology home (`service=http://ecology/`), login stays in Weaver (e.g. workflow center).
+
+Whitelist at least: `http://{shrHost}:{port}` (e.g. `http://172.16.38.1:6888`).
+
+### Code pieces
+
+| Piece | Role |
+|-------|------|
+| `logoutHR.js` 二开块 | After local logout → `/qy/auth/weaver/logout.do?appId&redirect` |
+| `WeaverAuthController.logout` | Build SSO entry as `service` |
+| `WeaverShrLanding.writeLogoutThenService` | HTML + JS encode (avoid double-encode) |
+
+Do **not** set `service` to ecology homepage. Do **not** expect external `service` to work without whitelist.
+
+---
+
+## Portal JS (login_withForm.js)
+
+```
+oaSsoUrl = http://{host}/qy/auth/weaver/app.do
++ appId=
++ redirect=service/target
+```
+
+Whitelist: `/shr/home.do` native. Prefer src overlay for sso_web.
+
+---
+
+## Deploy checklist
+
+```
+- [ ] integration jar → {EAS_HOME}/server/lib/sp/
+- [ ] qy.war in easweb.ear application.xml + deployed
+- [ ] INTEGRATION_CONFIG_PATH; callbackUrl == 泛微注册
+- [ ] login_withForm.js → /qy/auth/weaver/app.do
+- [ ] logoutHR.js 二开 + 泛微 service 白名单
+- [ ] AttackFilter: /qy/ allowed or CSRF off only if accepted
+- [ ] ssoParamemter UrlPattern includes /index2sso.jsp
+- [ ] Restart; private window for OAuth code
+```
+
+---
+
+## Troubleshooting
+
+| Symptom | Check |
+|---------|--------|
+| Callback 404 `/callback/weaver/pa` | Filter + `.do` mapping + suffix-pattern |
+| `%2525252B` / must paste URL | Still GET Location — deploy POST form landing |
+| External Referer → `/qy` 404 | AttackFilter CSRF / whitelist `/qy/` |
+| Logout then stay in Weaver | `service` not whitelisted; address bar still `service=ecology/` |
+| Logout login page `service=ecology/` | White list OR old JS/jar not deployed |
+| `unsupported_client` on `/auth/app` | UA miss — use `/auth/weaver/app` |
+| 80013 redirect_uri | callbackUrl vs IdP register mismatch |
+| qy 404 always | qy.war missing from live `easweb.ear` |
+
+---
+
+## Key classes / paths (BaoYPro)
+
+```
+WeaverAuthController (/auth/weaver/app, callback, logout)
+WeaverOAuthStrategy, WeaverApiUtils, WeaverConfigService
+WeaverShrLanding (writePostForm, writeLogoutThenService, redirectLocation)
+CallbackSuffixCompatFilter
+login_withForm.js → /qy/auth/weaver/app.do
+logoutHR.js → /qy/auth/weaver/logout.do
+integration-config-*.json (weaver + oauth)
+apusic/.../config/web.xml AttackFilter
+```
+
+Search:
+
+```
+WeaverShrLanding, writeLogoutThenService, /auth/weaver/app
+CallbackSuffixCompatFilter, AttackFilter, serverNameWhiteList
+logoutHR.js, ecologyUrl, index2sso.jsp
+```

+ 266 - 0
单点_待办skill/金蝶SHR-Skills使用说明.md

@@ -0,0 +1,266 @@
+# 金蝶 EAS/sHR 对接 Skills 使用说明
+
+本压缩包包含两个 Agent Skill(`SKILL.md` 标准目录),可用于 **Cursor** 与 **Claude Code**,指导 AI 在金蝶 EAS/sHR 上做 **单点登录(SSO)** 与 **待办/待阅推送**。
+
+| 目录 | Skill 名 | 用途 |
+|------|----------|------|
+| `kdshrsso/` | **kdshrsso** | 单点登录:泛微 E10 / 钉钉 / IDM / Azure 等对接 sHR |
+| `kdshrpmsg/` | **kdshrpmsg** | 待办、待阅推送:泛微统一待办 / 门户 PORTAL-* 等 |
+
+两者常一起用:待办深链要能登录,入口须与 SSO 一致。  
+**同一套文件夹内容**可分别拷到 Cursor 或 Claude Code 的 skills 目录(路径不同,见下)。
+
+---
+
+## 一、安装(解压后)
+
+两套工具的目录名都叫 `skills/`,但父目录不同,**不要混放**。
+
+### 1.1 Cursor
+
+| 范围 | Windows | macOS / Linux |
+|------|---------|----------------|
+| 个人全局 | `C:\Users\<用户名>\.cursor\skills\` | `~/.cursor/skills/` |
+| 仅本项目 | `<项目>\.cursor\skills\` | `<项目>/.cursor/skills/` |
+
+注意:不要放到 `.cursor/skills-cursor/`(Cursor 内置技能目录)。
+
+### 1.2 Claude Code
+
+| 范围 | Windows | macOS / Linux |
+|------|---------|----------------|
+| 个人全局 | `C:\Users\<用户名>\.claude\skills\` | `~/.claude/skills/` |
+| 仅本项目(推荐团队共享) | `<项目>\.claude\skills\` | `<项目>/.claude/skills/` |
+
+官方说明:[Extend Claude with skills](https://code.claude.com/docs/en/skills)
+
+### 1.3 解压后的目录结构(两种工具相同)
+
+```
+skills/                    ← 上面表里对应的 .cursor 或 .claude 下的 skills
+├── kdshrsso/
+│   ├── SKILL.md           ← 必需
+│   └── *.md               ← 参考文档,随目录一起拷贝
+└── kdshrpmsg/
+    ├── SKILL.md
+    └── *.md
+```
+
+同时用 Cursor 和 Claude Code 时:把 `kdshrsso`、`kdshrpmsg` **各拷一份**到 `.cursor/skills/` 与 `.claude/skills/`,或项目内两套路径各放一份。
+
+### 1.4 注意
+
+- **必须保留文件夹结构**(目录名 = 技能名),不要只拷贝单个 `SKILL.md` 改名。
+- Cursor:安装后重启 Cursor / 重新打开项目。
+- Claude Code:拷到已有 skills 目录后,一般当前会话会很快生效;若是新建了顶层 `skills` 目录,需 **重启一次 Claude Code**。
+- 项目技能进 Git 时:Cursor 用 `.cursor/skills/`,Claude Code 用 `.claude/skills/`;团队若两边都用,可两个目录都提交(内容相同)。
+
+---
+
+## 二、在 Cursor 里怎么用
+
+在 Agent / Chat 中**点名技能**或描述业务即可,例如:
+
+**SSO:**
+
+- 「按 **kdshrsso**,给新项目接泛微 E10 SSO」
+- 「按报业 / BaoYPro 的 B-qy 模式做单点」
+- 「退出后又自动登录了,按 kdshrsso 查联邦退出」
+
+**待办:**
+
+- 「按 **kdshrpmsg**,对接泛微统一待办」
+- 「按报业 weaverTodo 推送待办,深链走 SSO」
+- 「只要待办推送,SSO 已做好」
+
+**上线:**
+
+- 「按 skill 做 **切生产 / 上线**」
+
+Agent 应按 skill 要求:**先提问确认(intake),再写代码**;说「上线」时先列待改配置、确认生产地址,再改文件。
+
+---
+
+## 三、在 Claude Code 里怎么用
+
+本包格式与 Claude Code Skills 兼容(目录 + `SKILL.md` + 同级参考文档)。安装到 `~/.claude/skills/` 或项目 `.claude/skills/` 后即可。
+
+### 3.1 推荐:斜杠命令显式调用
+
+在 Claude Code 输入框输入 `/`,应能看到(目录名即命令名):
+
+- `/kdshrsso` — 加载 SSO 技能  
+- `/kdshrpmsg` — 加载待办/待阅技能  
+
+示例:
+
+```
+/kdshrsso
+按 B-qy 对接泛微 E10,测试 SHR 地址是 ……,请先做 intake 提问。
+```
+
+```
+/kdshrpmsg
+对接泛微统一待办,深链走 /qy/auth/weaver/app.do,请先确认渠道与配置文件名。
+```
+
+两个都要时,可先 `/kdshrsso` 再 `/kdshrpmsg`,或开场说明「先 SSO 后待办,两个 skill 一起按」。
+
+### 3.2 也可自然语言触发
+
+不打 `/` 时,若描述命中 skill 的 `description`(如「金蝶 SSO」「泛微待办」「weaverTodo」),Claude 可能自动加载。重要流程建议仍用 **`/kdshrsso` / `/kdshrpmsg`**,避免漏载。
+
+### 3.3 行为约定(与 Cursor 相同)
+
+- 新项目 / 新 IdP:须先做 **intake 提问**,确认后再改代码。  
+- 用户说「上线 / 切生产」:先列待改文件与生产 URL,确认后再改。  
+- 同目录下的 `weaver-oauth-qywar-reference.md` 等参考文档,Claude 会在需要时按 `SKILL.md` 指引读取,请勿删掉。
+
+### 3.4 Claude Code 侧常见问题
+
+| 现象 | 处理 |
+|------|------|
+| 输入 `/` 看不到 kdshrsso | 确认路径是 `.claude/skills/kdshrsso/SKILL.md`(不是 `.cursor/...`);重启 Claude Code |
+| 只有个人机生效、云端/同事没有 | 把技能放进项目 `.claude/skills/` 并提交 Git(云端会话不读你本机 `~/.claude/skills/`) |
+| 想限制只能手动触发 | 可在 `SKILL.md` 的 YAML 里加 `disable-model-invocation: true`(加后需用 `/技能名` 才会加载) |
+
+---
+
+## 四、选型速查
+
+### kdshrsso — SSO 模式
+
+| 模式 | 适用 | 典型入口 |
+|------|------|----------|
+| **A** Filter 覆盖 | Azure / 标准 OIDC,希望全站 `/shr` 自动鉴权 | `/shr?adsso` 等 |
+| **B** Servlet + Ltpa | 企业 IDM、OAToSHR 深链 | `/shr/api/oAToSHR` |
+| **B-qy** qy.war + index2sso | **泛微 E10 / 钉钉**(报业类) | `/qy/auth/weaver/app.do` |
+
+不确定时让 Agent 按 skill 里的 Quick decision 推荐,再由你确认。
+
+### kdshrpmsg — 消息推送
+
+| 场景 | 是否适合 |
+|------|----------|
+| 流程待办推到门户 / 泛微统一待办 | 适合 |
+| BMC 待阅 / 通知 | 适合 |
+| 玉湖式 PORTAL-* + ESB | 适合(见包内 yhpro / portal 参考) |
+| 只做钉钉/飞书、无门户 | 部分:单独渠道 + 独立配置文件 |
+| 替换产品消息中心整套 | 不适合 |
+
+---
+
+## 五、开工前 Agent 会问什么(请提前准备)
+
+### SSO(kdshrsso)
+
+1. IdP 类型(泛微 / 钉钉 / 飞书 / IDM / Azure…)及接口文档  
+2. 模式 A / B / B-qy  
+3. sHR 地址(协议+主机+端口)、是否 Apusic、能否部署 `qy.war`  
+4. 人员映射(IdP 字段 ↔ sHR 工号/自定义字段等)  
+5. 回调 URL(通常无 `.do`)  
+6. 是否改门户 `login_withForm.js`;本地 `/shr/home.do` 是否保留  
+7. 是否要联邦退出;IdP 退出重定向白名单  
+
+### 待办(kdshrpmsg)
+
+1. 渠道与接口文档  
+2. 只要待办,还是待办+待阅;SSO 是否同期做  
+3. 鉴权方式与配置文件名(勿混渠道)  
+4. 接收人字段(工号/手机/userid)  
+5. 幂等键规则  
+6. 深链 SSO 入口(须与 kdshrsso 一致)  
+7. 新建独立 Dao,还是多渠道 Router  
+
+你说「按报业 / 按 BaoYPro」时,Agent 可按默认假设复述一次,你确认后再改代码。
+
+---
+
+## 六、压缩包内容清单
+
+### kdshrsso/
+
+| 文件 | 说明 |
+|------|------|
+| `SKILL.md` | 主流程:intake、模式选型、切生产、排错要点 |
+| `weaver-oauth-qywar-reference.md` | 泛微 E10 / qy.war(B-qy)详细参考 |
+| `portal-login-reference.md` | 门户 eassso / login_withForm |
+| `idm-servlet-reference.md` | Option B(OAToSHR) |
+| `idm-filter-reference.md` | Option A(IDM Filter) |
+| `azure-oauth-reference.md` | Azure / OIDC Filter |
+| `migration-guide.md` | 迁移注意 |
+
+### kdshrpmsg/
+
+| 文件 | 说明 |
+|------|------|
+| `SKILL.md` | 主流程:intake、SPI、切生产、与 SSO 协同 |
+| `weaver-baoy-plan.md` | 报业 / 泛微统一待办落地计划与约定 |
+| `weaver-receive-todo-api.md` | 泛微 ReceiveTodo / OpenAPI 说明 |
+| `yhpro-reference.md` | 玉湖门户待办参考实现 |
+| `portal-api-map.md` | PORTAL-* 接口对照 |
+
+---
+
+## 七、使用建议与注意事项
+
+1. **密钥与生产地址不要写进 skill、不要提交 Git**;只放服务器 properties / 本地私有配置。  
+2. SSO 与待办同期交付时,**先定深链入口**(如 `/qy/auth/weaver/app.do`),两边保持一致。  
+3. 桌面进 sHR 优先 **HTML POST `index2sso`**,避免 LTPA 放进 Location 导致多重编码。  
+4. PC 待办深链用**显式平台路径**(`/qy/auth/weaver/...`),不要依赖仅靠 UA 的 Unified `/auth/app`。  
+5. Skill 内若出现示例主机、路径,请改成你们环境;报业项目还可对照仓库内 `docs/integration/*.html`(若有)。  
+6. 说「上线 / 切生产」时,Agent 应先列文件清单并确认生产 URL,再改配置。
+
+---
+
+## 八、推荐对话开场白(可复制)
+
+### Cursor
+
+```
+请加载 kdshrsso 和 kdshrpmsg。
+我们要对接:泛微 E10(或钉钉/飞书/IDM)。
+环境:测试 SHR 地址是 ……;是否有 qy.war:……
+人员映射:……
+请先按 skill 做 intake 提问,确认后再写代码。不要直接改生产配置。
+```
+
+### Claude Code
+
+```
+/kdshrsso
+我们要对接:泛微 E10(或钉钉/飞书/IDM)。
+环境:测试 SHR 地址是 ……;是否有 qy.war:……
+人员映射:……
+请先做 intake 提问,确认后再写代码。不要直接改生产配置。
+```
+
+需要待办时再执行:
+
+```
+/kdshrpmsg
+深链 SSO 入口与上面一致;请先确认渠道、配置文件名与人员字段。
+```
+
+### 切生产(两种工具通用)
+
+Cursor 可直接说;Claude Code 建议先 `/kdshrsso`(及需要时 `/kdshrpmsg`)再发:
+
+```
+按 kdshrsso / kdshrpmsg 做切生产。
+生产 SHR:……
+生产 ecology / OpenAPI:……
+请先列出要改的文件和项,我确认后再改。
+```
+
+---
+
+## 九、版本与反馈
+
+- 适用产品:金蝶 EAS / sHR(含 Apusic 等常见部署)  
+- 适用工具:**Cursor**、**Claude Code**(同一套 `kdshrsso` / `kdshrpmsg` 目录)  
+- 参考落地:YHPro(门户 PORTAL)、BaoYPro(泛微 B-qy + 统一待办)  
+- 问题反馈:把对话中 Agent 的 intake 结论与报错日志一并反馈给技能维护人,便于更新 `SKILL.md` / 参考文档  
+
+**Cursor**:对话中点名 **kdshrsso** / **kdshrpmsg**。  
+**Claude Code**:输入 **`/kdshrsso`** / **`/kdshrpmsg`**。

BIN
单点_待办sso_skills.zip