From fbd5251033b020b98705e9807a1f6b32b2f9fd69 Mon Sep 17 00:00:00 2001 From: RUCYancy Date: Mon, 1 Jun 2026 11:40:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=BD=90=E7=AB=99=E5=86=85=E4=BF=A1?= =?UTF-8?q?=E4=B8=8EE+=E6=96=87=E6=A1=88=E5=88=B0PRD=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增审批待办站内信按菜单、频道、知识空间场景展示业务文案:src/backend/bisheng/approval/domain/services/approval_notification_service.py、src/frontend/client/src/components/NotificationsDialog.tsx 修复撤回、拒绝、异常取消等站内信文案与PRD不一致的问题:src/backend/bisheng/message/domain/services/notification_content.py、src/frontend/client/src/locales/zh-Hans/translation.json 新增转发中粮E+时携带审批原因和场景化正文的能力:src/backend/bisheng/notification/external/_payload.py、src/backend/bisheng/notification/forwarder.py 完善审批、频道、知识空间触发站内信时的业务场景标识传递:src/backend/bisheng/approval/domain/services/approval_center_service.py、src/backend/bisheng/channel/domain/services/channel_service.py、src/backend/bisheng/knowledge/domain/services/knowledge_space_service.py 新增站内信与E+文案回归测试:src/backend/test/notification/test_payload.py、src/backend/test/notification/test_forwarder.py、src/frontend/client/src/components/NotificationsDialog.test.tsx --- .../services/approval_center_service.py | 10 ++ .../services/approval_exception_service.py | 8 ++ .../services/approval_notification_service.py | 11 ++ .../domain/services/channel_service.py | 2 + .../services/knowledge_space_service.py | 2 + .../domain/services/message_service.py | 11 +- .../domain/services/notification_content.py | 5 + .../bisheng/notification/external/_payload.py | 130 +++++++++++------- src/backend/bisheng/notification/forwarder.py | 24 +++- .../test/notification/test_forwarder.py | 9 +- src/backend/test/notification/test_payload.py | 38 ++++- .../components/NotificationsDialog.test.tsx | 49 ++++++- .../src/components/NotificationsDialog.tsx | 26 +++- .../client/src/locales/en/translation.json | 42 +++--- .../client/src/locales/ja/translation.json | 44 +++--- .../src/locales/zh-Hans/translation.json | 50 +++---- 16 files changed, 323 insertions(+), 138 deletions(-) diff --git a/src/backend/bisheng/approval/domain/services/approval_center_service.py b/src/backend/bisheng/approval/domain/services/approval_center_service.py index a8cb42991..5ad62051e 100644 --- a/src/backend/bisheng/approval/domain/services/approval_center_service.py +++ b/src/backend/bisheng/approval/domain/services/approval_center_service.py @@ -453,6 +453,7 @@ async def withdraw_instance( action_code='approval_instance_withdrawn', business_name=instance.business_name, instance_id=instance.id, + scenario_code=instance.scenario_code, reason=reason, ) try: @@ -502,6 +503,7 @@ async def _send_menu_access_approval_messages( business_name=menu_name, button_action_code='request_menu_access', receiver_user_ids=approver_user_ids, + scenario_code='menu_access_request', ) @classmethod @@ -624,6 +626,7 @@ async def revoke_menu_grant( action_code='menu_grant_revoked', business_name=instance.business_name, instance_id=instance.id, + scenario_code=instance.scenario_code, reason=reason, ) return {'revoked_keys': [row.menu_key for row in rows], 'instance_id': instance_id} @@ -702,6 +705,7 @@ async def decide_task( action_code='approval_task_rejected', business_name=instance.business_name, instance_id=instance.id, + scenario_code=instance.scenario_code, reason=comment, ) try: @@ -807,6 +811,7 @@ async def _advance_after_node_approved( action_code='approval_instance_approved', business_name=instance.business_name or '', instance_id=instance.id, + scenario_code=instance.scenario_code, ) return @@ -841,6 +846,7 @@ async def _advance_after_node_approved( action_code='approval_instance_approved', business_name=instance.business_name or '', instance_id=instance.id, + scenario_code=instance.scenario_code, ) return @@ -890,6 +896,7 @@ async def _advance_after_node_approved( action_code='approval_exception_approver_empty', business_name=instance.business_name, instance_id=instance.id, + scenario_code=instance.scenario_code, ) return @@ -920,6 +927,7 @@ async def _advance_after_node_approved( action_code='approval_task_pending', business_name=instance.business_name, instance_id=instance.id, + scenario_code=instance.scenario_code, task_id=task.id, ) @@ -931,6 +939,7 @@ async def _send_approval_notify( action_code: str, business_name: str, instance_id: int, + scenario_code: str | None = None, reason: str | None = None, task_id: int | None = None, ) -> None: @@ -942,6 +951,7 @@ async def _send_approval_notify( action_code=action_code, business_name=business_name, instance_id=instance_id, + scenario_code=scenario_code, reason=reason, task_id=task_id, ) diff --git a/src/backend/bisheng/approval/domain/services/approval_exception_service.py b/src/backend/bisheng/approval/domain/services/approval_exception_service.py index f4fd5c5c0..8d5712a28 100644 --- a/src/backend/bisheng/approval/domain/services/approval_exception_service.py +++ b/src/backend/bisheng/approval/domain/services/approval_exception_service.py @@ -221,6 +221,7 @@ async def cancel_exception_api( action_code="approval_exception_cancelled", business_name=instance.business_name, instance_id=instance.id, + scenario_code=instance.scenario_code, reason=reason.strip(), ) return {"exception_id": exception_id, "instance_id": instance.id, "status": "cancelled"} @@ -254,6 +255,7 @@ async def assign_flow( sender=instance.applicant_user_id, business_name=instance.business_name, instance_id=instance.id, + scenario_code=instance.scenario_code, tasks=created_tasks, ) @@ -285,6 +287,7 @@ async def assign_approvers( sender=instance.applicant_user_id, business_name=instance.business_name, instance_id=instance.id, + scenario_code=instance.scenario_code, tasks=created_tasks, ) @@ -485,6 +488,7 @@ async def _advance_from_skipped_node(self, *, instance, current_node_order: int, action_code="approval_task_pending", business_name=instance.business_name or "", instance_id=instance.id, + scenario_code=instance.scenario_code, task_id=task.id, ) @@ -562,6 +566,7 @@ async def _notify_created_tasks( sender: int, business_name: str, instance_id: int, + scenario_code: str | None = None, tasks: list[ApprovalTask], ) -> None: for task in tasks: @@ -571,6 +576,7 @@ async def _notify_created_tasks( action_code="approval_task_pending", business_name=business_name or "", instance_id=instance_id, + scenario_code=scenario_code, task_id=task.id, ) @@ -755,6 +761,7 @@ async def _notify_user( action_code: str, business_name: str, instance_id: int, + scenario_code: str | None = None, reason: str | None = None, task_id: int | None = None, ) -> None: @@ -766,6 +773,7 @@ async def _notify_user( action_code=action_code, business_name=business_name, instance_id=instance_id, + scenario_code=scenario_code, reason=reason, task_id=task_id, ) diff --git a/src/backend/bisheng/approval/domain/services/approval_notification_service.py b/src/backend/bisheng/approval/domain/services/approval_notification_service.py index 1e32d5f67..02bc91251 100644 --- a/src/backend/bisheng/approval/domain/services/approval_notification_service.py +++ b/src/backend/bisheng/approval/domain/services/approval_notification_service.py @@ -19,6 +19,7 @@ async def notify_user( action_code: str, business_name: str, instance_id: int, + scenario_code: str | None = None, reason: str | None = None, task_id: int | None = None, ) -> None: @@ -28,6 +29,7 @@ async def notify_user( action_code=action_code, business_name=business_name, instance_id=instance_id, + scenario_code=scenario_code, reason=reason, task_id=task_id, ) @@ -40,6 +42,7 @@ async def notify_users( action_code: str, business_name: str, instance_id: int, + scenario_code: str | None = None, reason: str | None = None, task_id: int | None = None, ) -> None: @@ -53,6 +56,11 @@ async def notify_users( metadata = {} if task_id is not None: metadata = {"data": {"approval_task_id": str(task_id)}} + if scenario_code: + data = dict(metadata.get("data") or {}) + data.setdefault("scenario_code", scenario_code) + metadata["data"] = data + metadata["scenario_code"] = scenario_code actor_user_name = None try: from bisheng.user.domain.models.user import UserDao @@ -75,6 +83,7 @@ async def notify_users( business_id=instance_id, actor_user_id=sender, actor_user_name=actor_user_name, + scenario_code=scenario_code, reason=reason, metadata=metadata, ), @@ -95,6 +104,7 @@ async def notify_admins( action_code: str, business_name: str, instance_id: int, + scenario_code: str | None = None, ) -> None: admin_ids = await ApprovalNotificationService._get_admin_recipient_ids( tenant_id=tenant_id, @@ -106,6 +116,7 @@ async def notify_admins( action_code=action_code, business_name=business_name, instance_id=instance_id, + scenario_code=scenario_code, ) @staticmethod diff --git a/src/backend/bisheng/channel/domain/services/channel_service.py b/src/backend/bisheng/channel/domain/services/channel_service.py index a52ac22e8..aebaf0612 100644 --- a/src/backend/bisheng/channel/domain/services/channel_service.py +++ b/src/backend/bisheng/channel/domain/services/channel_service.py @@ -1104,6 +1104,7 @@ async def _send_channel_approval_notification( business_name=channel.name, button_action_code="request_channel", receiver_user_ids=approver_user_ids, + scenario_code="channel_subscribe_request", ) async def _send_subscribe_approval_notification( @@ -1147,6 +1148,7 @@ async def _send_subscribe_approval_notification( business_name=channel.name, button_action_code="request_channel", receiver_user_ids=receiver_user_ids, + scenario_code="channel_subscribe_request", ) async def update_channel(self, channel_id: str, req: UpdateChannelRequest, login_user: UserPayload): diff --git a/src/backend/bisheng/knowledge/domain/services/knowledge_space_service.py b/src/backend/bisheng/knowledge/domain/services/knowledge_space_service.py index 9077d803b..ed2c0428f 100644 --- a/src/backend/bisheng/knowledge/domain/services/knowledge_space_service.py +++ b/src/backend/bisheng/knowledge/domain/services/knowledge_space_service.py @@ -3561,6 +3561,7 @@ async def _send_space_approval_notification( business_name=space.name, button_action_code="request_knowledge_space", receiver_user_ids=approver_user_ids, + scenario_code="knowledge_space_subscribe_request", ) async def _send_subscription_notification(self, space: Knowledge): @@ -3579,6 +3580,7 @@ async def _send_subscription_notification(self, space: Knowledge): business_name=space.name, button_action_code="request_knowledge_space", receiver_user_ids=member_ids, + scenario_code="knowledge_space_subscribe_request", ) async def unsubscribe_space(self, space_id: int) -> bool: diff --git a/src/backend/bisheng/message/domain/services/message_service.py b/src/backend/bisheng/message/domain/services/message_service.py index 076a5ed57..59a582164 100644 --- a/src/backend/bisheng/message/domain/services/message_service.py +++ b/src/backend/bisheng/message/domain/services/message_service.py @@ -418,6 +418,7 @@ def build_generic_approval_content( business_name: str, button_action_code: str, approval_message_id: Optional[int] = None, + scenario_code: Optional[str] = None, ) -> List[Dict[str, Any]]: """ Build the generic message content structure for a business approval request. @@ -437,7 +438,13 @@ def build_generic_approval_content( "content": f"--{business_name}", "metadata": { "business_type": business_type, - "data": {business_type: business_id}, + "scenario_code": scenario_code, + "data": { + business_type: business_id, + "business_id": business_id, + "business_name": business_name, + **({"scenario_code": scenario_code} if scenario_code else {}), + }, }, }, { @@ -461,6 +468,7 @@ async def send_generic_approval( business_name: str, button_action_code: str, receiver_user_ids: List[int], + scenario_code: Optional[str] = None, ) -> InboxMessage: """ Send a generic approval notification to specific receivers. @@ -474,6 +482,7 @@ async def send_generic_approval( business_id=business_id, business_name=business_name, button_action_code=button_action_code, + scenario_code=scenario_code, ) # Create the message with action_code stored on model for reliable routing diff --git a/src/backend/bisheng/message/domain/services/notification_content.py b/src/backend/bisheng/message/domain/services/notification_content.py index a497e10c5..a4685afb2 100644 --- a/src/backend/bisheng/message/domain/services/notification_content.py +++ b/src/backend/bisheng/message/domain/services/notification_content.py @@ -22,6 +22,7 @@ def build_notify_content( business_id: str | int | None = None, actor_user_id: int | None = None, actor_user_name: str | None = None, + scenario_code: str | None = None, reason: str | None = None, navigable: bool = True, metadata: dict[str, Any] | None = None, @@ -41,12 +42,16 @@ def build_notify_content( content.append({"type": "system_text", "content": action_code}) target_metadata = dict(metadata or {}) + if scenario_code: + target_metadata.setdefault("scenario_code", scenario_code) if business_type and business_id is not None: target_metadata.setdefault("business_type", business_type) data = dict(target_metadata.get("data") or {}) data.setdefault(business_type, str(business_id)) data.setdefault("business_id", str(business_id)) data.setdefault("business_name", target_name) + if scenario_code: + data.setdefault("scenario_code", scenario_code) target_metadata["data"] = data if navigable and business_type and business_id is not None: diff --git a/src/backend/bisheng/notification/external/_payload.py b/src/backend/bisheng/notification/external/_payload.py index 237ac0053..e5ca8f356 100644 --- a/src/backend/bisheng/notification/external/_payload.py +++ b/src/backend/bisheng/notification/external/_payload.py @@ -1,4 +1,5 @@ """Build E+ textcard payloads from InboxMessage action_codes.""" +# ruff: noqa: RUF001 from bisheng.common.services.config_service import settings @@ -39,33 +40,33 @@ _TEMPLATES: dict[str, dict[str, str]] = { "request_channel": { "title": "[知源] 新的频道订阅申请", - "normal": "{applicant} 申请订阅频道「{resource_name}」", - "highlight": "需要你审批", + "normal": "{applicant}申请订阅频道「{resource_name}」", + "highlight": "", }, "approved_channel": { "title": "[知源] 频道订阅申请已通过", - "normal": "你订阅频道「{resource_name}」的申请", - "highlight": "已通过", + "normal": "{applicant}通过了你对「{resource_name}」的审批申请", + "highlight": "", }, "rejected_channel": { "title": "[知源] 频道订阅申请被拒绝", - "normal": "你订阅频道「{resource_name}」的申请", - "highlight": "被拒绝", + "normal": "{applicant}拒绝了你对「{resource_name}」的审批申请", + "highlight": "", }, "request_knowledge_space": { "title": "[知源] 新的知识空间加入申请", - "normal": "{applicant} 申请加入知识空间「{resource_name}」", - "highlight": "需要你审批", + "normal": "{applicant}申请加入知识空间「{resource_name}」", + "highlight": "", }, "approved_knowledge_space": { "title": "[知源] 知识空间加入申请已通过", - "normal": "你加入知识空间「{resource_name}」的申请", - "highlight": "已通过", + "normal": "{applicant}通过了你对「{resource_name}」的审批申请", + "highlight": "", }, "rejected_knowledge_space": { "title": "[知源] 知识空间加入申请被拒绝", - "normal": "你加入知识空间「{resource_name}」的申请", - "highlight": "被拒绝", + "normal": "{applicant}拒绝了你对「{resource_name}」的审批申请", + "highlight": "", }, "request_department_knowledge_space_upload": { "title": "[知源] 新的部门知识空间上传申请", @@ -89,107 +90,114 @@ }, "request_menu_access": { "title": "[知源] 新的菜单访问申请", - "normal": "{applicant} 申请访问菜单「{resource_name}」", - "highlight": "需要你审批", + "normal": "{applicant}申请访问菜单「{resource_name}」", + "highlight": "", }, "approval_task_pending": { "title": "[知源] 新的审批任务", "normal": "{applicant} 提交了「{resource_name}」审批申请", - "highlight": "需要你审批", + "highlight": "", }, "approval_task_rejected": { "title": "[知源] 审批申请被拒绝", - "normal": "你对「{resource_name}」的审批申请", - "highlight": "被拒绝", + "normal": "{applicant}拒绝了你对「{resource_name}」的审批申请", + "highlight": "", }, "approval_instance_approved": { "title": "[知源] 审批申请已通过", - "normal": "你对「{resource_name}」的审批申请", - "highlight": "已通过", + "normal": "{applicant}通过了你对「{resource_name}」的审批申请", + "highlight": "", }, "approval_instance_withdrawn": { "title": "[知源] 审批申请已撤回", - "normal": "{applicant} 撤回了「{resource_name}」的审批申请", - "highlight": "无需处理", + "normal": "{applicant}撤回了「{resource_name}」的审批申请", + "highlight": "", }, "approval_exception_cancelled": { "title": "[知源] 审批申请已取消", - "normal": "你对「{resource_name}」的审批申请", - "highlight": "已被取消", + "normal": "你的「{resource_name}」审批申请已被取消", + "highlight": "", }, "approval_exception_route_missing": { "title": "[知源] 审批异常", - "normal": "「{resource_name}」的审批申请未匹配到审批分支", - "highlight": "请处理", + "normal": "「{resource_name}」的审批申请未匹配到审批分支,请处理", + "highlight": "", }, "approval_exception_approver_empty": { "title": "[知源] 审批异常", - "normal": "「{resource_name}」的审批申请未解析到审批人", - "highlight": "请处理", + "normal": "「{resource_name}」的审批申请未解析到审批人,请处理", + "highlight": "", }, "approval_execute_failed": { "title": "[知源] 审批执行失败", - "normal": "「{resource_name}」审批已通过,但业务执行失败", # noqa: RUF001 - "highlight": "请处理", + "normal": "「{resource_name}」审批已通过,但业务执行失败,请处理", + "highlight": "", }, "menu_grant_revoked": { "title": "[知源] 菜单授权已撤回", - "normal": "你对菜单「{resource_name}」的访问权限", - "highlight": "已被撤回", + "normal": "你对菜单「{resource_name}」的访问权限已被{applicant}撤回", + "highlight": "", }, "assigned_channel_admin": { "title": "[知源] 频道管理员授权", "normal": "你已被设为频道「{resource_name}」的管理员", - "highlight": "权限已变更", + "highlight": "", }, "assigned_knowledge_space_admin": { "title": "[知源] 知识空间管理员授权", "normal": "你已被设为知识空间「{resource_name}」的管理员", - "highlight": "权限已变更", + "highlight": "", }, "revoked_channel_admin": { "title": "[知源] 频道管理员权限已取消", "normal": "你已不再是频道「{resource_name}」的管理员", - "highlight": "权限已变更", + "highlight": "", }, "revoked_knowledge_space_admin": { "title": "[知源] 知识空间管理员权限已取消", "normal": "你已不再是知识空间「{resource_name}」的管理员", - "highlight": "权限已变更", + "highlight": "", }, "removed_channel_member": { "title": "[知源] 已被移出频道", "normal": "你已被移出频道「{resource_name}」", - "highlight": "访问关系已变更", + "highlight": "", }, "removed_knowledge_space_member": { "title": "[知源] 已被移出知识空间", "normal": "你已被移出知识空间「{resource_name}」", - "highlight": "访问关系已变更", + "highlight": "", }, "channel_made_private": { "title": "[知源] 频道已转为私有", - "normal": "频道「{resource_name}」已转为私有", - "highlight": "你已无法访问", + "normal": "频道「{resource_name}」已转为私有,你已无法访问", + "highlight": "", }, "knowledge_space_made_private": { "title": "[知源] 知识空间已转为私有", - "normal": "知识空间「{resource_name}」已转为私有", - "highlight": "你已无法访问", + "normal": "知识空间「{resource_name}」已转为私有,你已无法访问", + "highlight": "", }, "channel_dismissed": { "title": "[知源] 频道已解散", - "normal": "频道「{resource_name}」", - "highlight": "已被解散", + "normal": "频道「{resource_name}」已被解散", + "highlight": "", }, "knowledge_space_deleted": { "title": "[知源] 知识空间已删除", - "normal": "知识空间「{resource_name}」", - "highlight": "已被删除", + "normal": "知识空间「{resource_name}」已被删除", + "highlight": "", }, } +_APPROVAL_TASK_SCENARIO_ACTION_CODES = { + "menu_access_request": "request_menu_access", + "channel_subscribe_request": "request_channel", + "knowledge_space_subscribe_request": "request_knowledge_space", +} + + def build_textcard_url(message_id: int) -> str: """Return the BiSheng callback URL for a textcard button.""" base = settings.get_cofco_forwarding_conf().bisheng_inbox_url.rstrip("/") @@ -212,6 +220,22 @@ def _truncate_bytes(text: str, max_bytes: int) -> str: return text[:lo] +def _normalize_reason(reason: str | None) -> str: + if not reason: + return "" + value = reason.strip() + for prefix in ("原因:", "原因:"): + if value.startswith(prefix): + return value[len(prefix):].strip() + return value + + +def _resolve_template_action_code(action_code: str, scenario_code: str | None = None) -> str: + if action_code == "approval_task_pending" and scenario_code: + return _APPROVAL_TASK_SCENARIO_ACTION_CODES.get(scenario_code, action_code) + return action_code + + def build_textcard( *, message_id: int, @@ -219,20 +243,24 @@ def build_textcard( applicant_name: str, resource_name: str, triggered_at: str, + reason: str | None = None, + scenario_code: str | None = None, ) -> dict: """Build the ``textcard`` dict for the E+ /v2/message/send body. Returns a dict with keys: title, description, url, btntxt. Raises KeyError when action_code is not in _TEMPLATES. """ - tpl = _TEMPLATES[action_code] # KeyError for unknown codes — intentional + resolved_action_code = _resolve_template_action_code(action_code, scenario_code) + tpl = _TEMPLATES[resolved_action_code] # KeyError for unknown codes — intentional title = _truncate_bytes(tpl["title"], 128) normal = tpl["normal"].format(applicant=applicant_name, resource_name=resource_name) - description = ( - f'
{triggered_at}
' - f'
{normal}
' - f'
{tpl["highlight"]}
' - ) + normalized_reason = _normalize_reason(reason) + if normalized_reason: + normal = f"{normal},原因:{normalized_reason}" + description = f'
{triggered_at}
{normal}
' + if tpl["highlight"]: + description += f'
{tpl["highlight"]}
' description = _truncate_bytes(description, 512) return { "title": title, diff --git a/src/backend/bisheng/notification/forwarder.py b/src/backend/bisheng/notification/forwarder.py index d5db1f704..6b98bfe00 100644 --- a/src/backend/bisheng/notification/forwarder.py +++ b/src/backend/bisheng/notification/forwarder.py @@ -1,12 +1,11 @@ """Synchronous hook: decide / resolve recipients / schedule HTTP task. Never blocks.""" import asyncio import logging -from typing import Optional, Tuple from bisheng.common.services.config_service import settings -from bisheng.user.domain.models.user import UserDao from bisheng.notification.external._payload import FORWARDABLE_ACTION_CODES, build_textcard from bisheng.notification.external.cofco_eplus_client import CofcoEPlusClient +from bisheng.user.domain.models.user import UserDao logger = logging.getLogger(__name__) @@ -22,7 +21,7 @@ def _fire_and_forget(coro) -> None: task.add_done_callback(_pending_tasks.discard) -def resolve_eplus_recipient(target_user_id: int) -> Tuple[Optional[str], str]: +def resolve_eplus_recipient(target_user_id: int) -> tuple[str | None, str]: """Return (e_plus_userid, skip_reason). skip_reason is empty string when resolved.""" user = UserDao.get_user(target_user_id) if not user: @@ -61,8 +60,8 @@ def _resolve_action_code(message) -> str: return "" -def _extract_payload_fields(message) -> Tuple[str, str]: - """Extract (applicant_name, resource_name) from an InboxMessage content list. +def _extract_payload_fields(message) -> tuple[str, str, str, str]: + """Extract (applicant_name, resource_name, reason, scenario_code) from content. Content block shapes (see message_schema.py): - UserContentItem → {"type": "user", "content": "@", ...} @@ -73,19 +72,28 @@ def _extract_payload_fields(message) -> Tuple[str, str]: """ applicant_name = "" resource_name = "" + reason = "" + scenario_code = "" try: for block in (message.content or []): btype = block.get("type") raw = block.get("content") or "" + meta = block.get("metadata") or {} + data = meta.get("data") or {} + if not scenario_code: + scenario_code = meta.get("scenario_code") or data.get("scenario_code") or "" if btype == "user" and not applicant_name and isinstance(raw, str): applicant_name = raw[1:] if raw.startswith("@") else raw elif btype == "business_url" and not resource_name and isinstance(raw, str): resource_name = raw[2:] if raw.startswith("--") else raw elif btype == "target" and not resource_name and isinstance(raw, str): resource_name = raw + elif btype == "tooltip_text" and not reason and isinstance(raw, str): + reason_prefix = f"原因{chr(0xFF1A)}" + reason = raw[len(reason_prefix):].strip() if raw.startswith(reason_prefix) else raw.strip() except Exception as exc: logger.debug("_extract_payload_fields parse error: %s", exc, exc_info=True) - return applicant_name, resource_name + return applicant_name, resource_name, reason, scenario_code def maybe_forward_external(message) -> None: @@ -132,7 +140,7 @@ def maybe_forward_external(message) -> None: if not resolved_eids: return - applicant_name, resource_name = _extract_payload_fields(message) + applicant_name, resource_name, reason, scenario_code = _extract_payload_fields(message) triggered_at = message.create_time.strftime("%Y-%m-%d %H:%M") textcard = build_textcard( @@ -141,6 +149,8 @@ def maybe_forward_external(message) -> None: applicant_name=applicant_name, resource_name=resource_name, triggered_at=triggered_at, + reason=reason, + scenario_code=scenario_code, ) client = CofcoEPlusClient() diff --git a/src/backend/test/notification/test_forwarder.py b/src/backend/test/notification/test_forwarder.py index 9aceea115..277ee6cf2 100644 --- a/src/backend/test/notification/test_forwarder.py +++ b/src/backend/test/notification/test_forwarder.py @@ -1,9 +1,8 @@ +from unittest.mock import MagicMock, patch + import pytest -from unittest.mock import AsyncMock, MagicMock, patch -from bisheng.notification.forwarder import ( - resolve_eplus_recipient, maybe_forward_external, -) +from bisheng.notification.forwarder import maybe_forward_external, resolve_eplus_recipient # ---- resolve_eplus_recipient ---- @@ -112,7 +111,7 @@ def test_forward_schedules_fire_and_forget( ): mock_settings.get_cofco_forwarding_conf.return_value.enabled = True mock_resolve.return_value = ("EMP001", "") - mock_extract.return_value = ("张三", "技术频道") + mock_extract.return_value = ("张三", "技术频道", "", "") mock_payload_settings.get_cofco_forwarding_conf.return_value.bisheng_inbox_url = "https://bisheng.cofco.com" maybe_forward_external(fake_msg) diff --git a/src/backend/test/notification/test_payload.py b/src/backend/test/notification/test_payload.py index e8a9c9bbb..a578146bf 100644 --- a/src/backend/test/notification/test_payload.py +++ b/src/backend/test/notification/test_payload.py @@ -1,3 +1,4 @@ +# ruff: noqa: RUF001 from unittest.mock import patch import pytest @@ -65,8 +66,8 @@ def test_build_textcard_request_channel(mock_settings): triggered_at="2026-05-13 10:30", ) assert card["title"] == "[知源] 新的频道订阅申请" - assert "张三 申请订阅频道「技术频道」" in card["description"] - assert "需要你审批" in card["description"] + assert "张三申请订阅频道「技术频道」" in card["description"] + assert "需要你审批" not in card["description"] assert "2026-05-13 10:30" in card["description"] assert card["btntxt"] == "去查看" assert "open-notifications=1" in card["url"] @@ -80,8 +81,37 @@ def test_build_textcard_approved_knowledge_space(mock_settings): applicant_name="李四", resource_name="研发知识空间", triggered_at="2026-05-13 11:00", ) assert card["title"] == "[知源] 知识空间加入申请已通过" - assert "你加入知识空间「研发知识空间」的申请" in card["description"] - assert "已通过" in card["description"] + assert "李四通过了你对「研发知识空间」的审批申请" in card["description"] + + +@patch("bisheng.notification.external._payload.settings") +def test_build_textcard_withdrawn_includes_reason_and_no_extra_no_action_needed(mock_settings): + mock_settings.get_cofco_forwarding_conf.return_value.bisheng_inbox_url = "https://bisheng.cofco.com" + card = build_textcard( + message_id=5, + action_code="approval_instance_withdrawn", + applicant_name="站内信", + resource_name="知识空间", + triggered_at="2026-06-01 10:05", + reason="不看了", + ) + assert "站内信撤回了「知识空间」的审批申请,原因:不看了" in card["description"] + assert "无需处理" not in card["description"] + + +@patch("bisheng.notification.external._payload.settings") +def test_build_textcard_pending_uses_scenario_specific_prd_copy(mock_settings): + mock_settings.get_cofco_forwarding_conf.return_value.bisheng_inbox_url = "https://bisheng.cofco.com" + card = build_textcard( + message_id=6, + action_code="approval_task_pending", + applicant_name="站内信", + resource_name="知识空间", + triggered_at="2026-06-01 10:06", + scenario_code="menu_access_request", + ) + assert "站内信申请访问菜单「知识空间」" in card["description"] + assert "提交了「知识空间」审批申请" not in card["description"] def test_build_textcard_unknown_action_code_raises(): diff --git a/src/frontend/client/src/components/NotificationsDialog.test.tsx b/src/frontend/client/src/components/NotificationsDialog.test.tsx index 4bbb1d9f2..386f267c5 100644 --- a/src/frontend/client/src/components/NotificationsDialog.test.tsx +++ b/src/frontend/client/src/components/NotificationsDialog.test.tsx @@ -12,7 +12,15 @@ jest.mock("react-i18next", () => ({ jest.mock("~/hooks/useLocalize", () => ({ __esModule: true, - default: () => (key: string) => key, + default: () => (key: string, vars?: Record) => { + const translations: Record = { + com_notifications_action_request_menu_access: "申请访问菜单「{{target}}」", + com_notifications_action_approval_task_pending: "提交了「{{target}}」审批申请", + }; + const template = translations[key]; + if (!template) return key; + return template.replace("{{target}}", vars?.target ?? ""); + }, })); jest.mock("~/Providers", () => ({ @@ -121,4 +129,43 @@ describe("NotificationsDialog approval jump", () => { }); }); }); + + it("uses scenario-specific PRD copy for later approval nodes", async () => { + jest.mocked(getMessageListApi).mockResolvedValue({ + total: 1, + data: [{ + id: 502, + sender: 7, + sender_name: "站内信", + message_type: "notify", + action_code: "approval_task_pending", + status: "pending", + is_read: false, + create_time: "2026-06-01T10:00:00Z", + update_time: "2026-06-01T10:00:00Z", + content: [ + { type: "system_text", content: "approval_task_pending" }, + { + type: "business_url", + content: "--知识空间", + metadata: { + business_type: "approval_instance_id", + scenario_code: "menu_access_request", + data: { + approval_instance_id: "99", + business_name: "知识空间", + scenario_code: "menu_access_request", + }, + }, + }, + ], + }], + }); + jest.mocked(markMessageReadApi).mockResolvedValue({}); + + render(); + + expect(await screen.findByText(/申请访问菜单/)).toBeInTheDocument(); + expect(screen.queryByText(/提交了/)).not.toBeInTheDocument(); + }); }); diff --git a/src/frontend/client/src/components/NotificationsDialog.tsx b/src/frontend/client/src/components/NotificationsDialog.tsx index 95e991a09..8f7b94610 100644 --- a/src/frontend/client/src/components/NotificationsDialog.tsx +++ b/src/frontend/client/src/components/NotificationsDialog.tsx @@ -92,6 +92,12 @@ const APPROVAL_NO_BUTTON_ACTION_CODES = new Set([ "approval_execute_failed", ]); +const APPROVAL_TASK_SCENARIO_TEXT_KEYS: Record = { + menu_access_request: "com_notifications_action_request_menu_access", + channel_subscribe_request: "com_notifications_action_request_channel", + knowledge_space_subscribe_request: "com_notifications_action_request_knowledge_space", +}; + export function NotificationsDialog({ open = false, onOpenChange, @@ -449,10 +455,28 @@ export function NotificationsDialog({ return getActionCode(notification); }; + const getScenarioCode = (notification: MessageItem): string => { + const parts = Array.isArray(notification.content) ? notification.content : []; + for (const part of parts) { + const metadata = (part as any)?.metadata ?? {}; + const data = metadata?.data ?? {}; + const value = metadata?.scenario_code ?? data?.scenario_code; + if (typeof value === "string" && value.trim()) return value.trim(); + } + return ""; + }; + const getNotificationText = (notification: MessageItem) => { const targetName = getTargetName(notification); const actionCode = getSystemTextCode(notification); - const actionTextKey = NOTIFICATION_ACTION_TEXT_KEYS[actionCode] || (actionCode ? `com_notifications_action_${actionCode}` : ""); + const scenarioTextKey = + actionCode === "approval_task_pending" + ? APPROVAL_TASK_SCENARIO_TEXT_KEYS[getScenarioCode(notification)] + : ""; + const actionTextKey = + scenarioTextKey || + NOTIFICATION_ACTION_TEXT_KEYS[actionCode] || + (actionCode ? `com_notifications_action_${actionCode}` : ""); const fallbackText = notification.content?.map((c) => c.content).filter(Boolean).join("") || ""; const safeLocalize = (key: string, vars?: Record) => { if (!key) return ""; diff --git a/src/frontend/client/src/locales/en/translation.json b/src/frontend/client/src/locales/en/translation.json index 997ea775e..18a0dba38 100644 --- a/src/frontend/client/src/locales/en/translation.json +++ b/src/frontend/client/src/locales/en/translation.json @@ -386,38 +386,38 @@ "com_notifications_accept": "Approve", "com_notifications_approved": "Approved", "com_notifications_rejected": "Rejected", - "com_notifications_action_approved_channel": "Approved your channel subscription request — {{target}}", - "com_notifications_action_approved_knowledge_space": "Approved your knowledge space join request — {{target}}", - "com_notifications_action_assigned_channel_admin": "Made you a channel administrator — {{target}}", - "com_notifications_action_assigned_knowledge_space_admin": "Made you a knowledge space administrator — {{target}}", + "com_notifications_action_approved_channel": "Approved your approval request for「{{target}}」", + "com_notifications_action_approved_knowledge_space": "Approved your approval request for「{{target}}」", + "com_notifications_action_assigned_channel_admin": "You have been set as an administrator of channel「{{target}}」", + "com_notifications_action_assigned_knowledge_space_admin": "You have been set as an administrator of knowledge space「{{target}}」", "com_notifications_action_request_department_knowledge_space_upload": "Requested to upload files to your department knowledge space — {{target}}", "com_notifications_action_approved_department_knowledge_space_upload": "Approved your department knowledge space upload request — {{target}}", "com_notifications_action_rejected_department_knowledge_space_upload": "Rejected your department knowledge space upload request — {{target}}", "com_notifications_action_sensitive_rejected_department_knowledge_space_upload": "Your upload failed the content safety check — {{target}}", - "com_notifications_action_rejected_channel": "Rejected your channel subscription request — {{target}}", - "com_notifications_action_rejected_knowledge_space": "Rejected your knowledge space join request — {{target}}", - "com_notifications_action_request_channel": "Requested to subscribe to your channel — {{target}}", - "com_notifications_action_request_knowledge_space": "Requested to join your knowledge space — {{target}}", + "com_notifications_action_rejected_channel": "Rejected your approval request for「{{target}}」", + "com_notifications_action_rejected_knowledge_space": "Rejected your approval request for「{{target}}」", + "com_notifications_action_request_channel": "Requested to subscribe to channel「{{target}}」", + "com_notifications_action_request_knowledge_space": "Requested to join knowledge space「{{target}}」", "com_notifications_action_generic": "Sent you a notification", "com_notifications_action_generic_with_target": "Sent you a notification — {{target}}", "com_notifications_action_request_menu_access": "Requested access to menu「{{target}}」", - "com_notifications_action_approval_task_pending": "Please review the approval request for「{{target}}」", + "com_notifications_action_approval_task_pending": "Submitted an approval request for「{{target}}」", "com_notifications_action_approval_task_rejected": "Rejected your approval request for「{{target}}」", "com_notifications_action_approval_instance_approved": "Approved your approval request for「{{target}}」", - "com_notifications_action_approval_instance_withdrawn": "Withdrew the approval request for「{{target}}」, no action needed", + "com_notifications_action_approval_instance_withdrawn": "Withdrew the approval request for「{{target}}」", "com_notifications_action_approval_exception_cancelled": "Your approval request for「{{target}}」has been cancelled", - "com_notifications_action_approval_exception_route_missing": "Approval request has a route-missing exception, action needed:「{{target}}」", - "com_notifications_action_approval_exception_approver_empty": "Approval request has an empty-approver exception, action needed:「{{target}}」", + "com_notifications_action_approval_exception_route_missing": "The approval request for「{{target}}」did not match an approval branch. Please handle it", + "com_notifications_action_approval_exception_approver_empty": "No approver was resolved for the approval request for「{{target}}」. Please handle it", "com_notifications_action_approval_execute_failed": "Approval for「{{target}}」passed, but the business action failed. Please handle it", - "com_notifications_action_menu_grant_revoked": "Revoked your access to menu「{{target}}」", - "com_notifications_action_revoked_channel_admin": "Removed your administrator permission for channel「{{target}}」", - "com_notifications_action_revoked_knowledge_space_admin": "Removed your administrator permission for knowledge space「{{target}}」", - "com_notifications_action_removed_channel_member": "Removed you from channel「{{target}}」", - "com_notifications_action_removed_knowledge_space_member": "Removed you from knowledge space「{{target}}」", - "com_notifications_action_channel_made_private": "Changed channel「{{target}}」to private. You can no longer access it", - "com_notifications_action_knowledge_space_made_private": "Changed knowledge space「{{target}}」to private. You can no longer access it", - "com_notifications_action_channel_dismissed": "Dismissed channel「{{target}}」", - "com_notifications_action_knowledge_space_deleted": "Deleted knowledge space「{{target}}」", + "com_notifications_action_menu_grant_revoked": "Your access to menu「{{target}}」has been revoked", + "com_notifications_action_revoked_channel_admin": "You are no longer an administrator of channel「{{target}}」", + "com_notifications_action_revoked_knowledge_space_admin": "You are no longer an administrator of knowledge space「{{target}}」", + "com_notifications_action_removed_channel_member": "You have been removed from channel「{{target}}」", + "com_notifications_action_removed_knowledge_space_member": "You have been removed from knowledge space「{{target}}」", + "com_notifications_action_channel_made_private": "Channel「{{target}}」has been changed to private. You can no longer access it", + "com_notifications_action_knowledge_space_made_private": "Knowledge space「{{target}}」has been changed to private. You can no longer access it", + "com_notifications_action_channel_dismissed": "Channel「{{target}}」has been dismissed", + "com_notifications_action_knowledge_space_deleted": "Knowledge space「{{target}}」has been deleted", "com_notifications_delete": "Delete message", "com_notifications_empty": "No messages yet", "com_notifications_empty_requests": "No requests yet", diff --git a/src/frontend/client/src/locales/ja/translation.json b/src/frontend/client/src/locales/ja/translation.json index de41dfc2e..587a5e23e 100644 --- a/src/frontend/client/src/locales/ja/translation.json +++ b/src/frontend/client/src/locales/ja/translation.json @@ -371,38 +371,38 @@ "com_notifications_accept": "承認", "com_notifications_approved": "承認済み", "com_notifications_rejected": "却下済み", - "com_notifications_action_approved_channel": "チャンネル購読申請を承認しました — {{target}}", - "com_notifications_action_approved_knowledge_space": "ナレッジスペース参加申請を承認しました — {{target}}", - "com_notifications_action_assigned_channel_admin": "チャンネル管理者に追加しました — {{target}}", - "com_notifications_action_assigned_knowledge_space_admin": "ナレッジスペース管理者に追加しました — {{target}}", + "com_notifications_action_approved_channel": "「{{target}}」の審査申請を承認しました", + "com_notifications_action_approved_knowledge_space": "「{{target}}」の審査申請を承認しました", + "com_notifications_action_assigned_channel_admin": "チャンネル「{{target}}」の管理者に設定されました", + "com_notifications_action_assigned_knowledge_space_admin": "ナレッジスペース「{{target}}」の管理者に設定されました", "com_notifications_action_request_department_knowledge_space_upload": "あなたの部門ナレッジスペースへのファイル追加を申請しました — {{target}}", "com_notifications_action_approved_department_knowledge_space_upload": "部門ナレッジスペースへのファイル追加申請を承認しました — {{target}}", "com_notifications_action_rejected_department_knowledge_space_upload": "部門ナレッジスペースへのファイル追加申請を却下しました — {{target}}", "com_notifications_action_sensitive_rejected_department_knowledge_space_upload": "アップロードがコンテンツ安全チェックで拒否されました — {{target}}", - "com_notifications_action_rejected_channel": "チャンネル購読申請を却下しました — {{target}}", - "com_notifications_action_rejected_knowledge_space": "ナレッジスペース参加申請を却下しました — {{target}}", - "com_notifications_action_request_channel": "あなたのチャンネル購読を申請しました — {{target}}", - "com_notifications_action_request_knowledge_space": "あなたのナレッジスペースへの参加を申請しました — {{target}}", + "com_notifications_action_rejected_channel": "「{{target}}」の審査申請を却下しました", + "com_notifications_action_rejected_knowledge_space": "「{{target}}」の審査申請を却下しました", + "com_notifications_action_request_channel": "チャンネル「{{target}}」の購読を申請しました", + "com_notifications_action_request_knowledge_space": "ナレッジスペース「{{target}}」への参加を申請しました", "com_notifications_action_generic": "通知を送信しました", "com_notifications_action_generic_with_target": "通知を送信しました — {{target}}", "com_notifications_action_request_menu_access": "メニュー「{{target}}」へのアクセスを申請しました", - "com_notifications_action_approval_task_pending": "「{{target}}」の審査申請を確認してください", + "com_notifications_action_approval_task_pending": "「{{target}}」の審査申請を提出しました", "com_notifications_action_approval_task_rejected": "「{{target}}」の審査申請を却下しました", "com_notifications_action_approval_instance_approved": "「{{target}}」の審査申請を承認しました", - "com_notifications_action_approval_instance_withdrawn": "「{{target}}」の審査申請を取り下げました。対応不要です", - "com_notifications_action_approval_exception_cancelled": "「{{target}}」の審査申請がキャンセルされました", - "com_notifications_action_approval_exception_route_missing": "の審査申請でルート未マッチの例外が発生しました。対応が必要です:「{{target}}」", - "com_notifications_action_approval_exception_approver_empty": "の審査申請で承認者が空の例外が発生しました。対応が必要です:「{{target}}」", + "com_notifications_action_approval_instance_withdrawn": "「{{target}}」の審査申請を取り下げました", + "com_notifications_action_approval_exception_cancelled": "あなたの「{{target}}」審査申請はキャンセルされました", + "com_notifications_action_approval_exception_route_missing": "「{{target}}」の審査申請は審査分岐に一致しませんでした。対応してください", + "com_notifications_action_approval_exception_approver_empty": "「{{target}}」の審査申請で承認者を解決できませんでした。対応してください", "com_notifications_action_approval_execute_failed": "「{{target}}」の審査は承認されましたが、業務処理に失敗しました。対応してください", - "com_notifications_action_menu_grant_revoked": "メニュー「{{target}}」へのアクセス権限を取り消しました", - "com_notifications_action_revoked_channel_admin": "チャンネル「{{target}}」の管理者権限を取り消しました", - "com_notifications_action_revoked_knowledge_space_admin": "ナレッジスペース「{{target}}」の管理者権限を取り消しました", - "com_notifications_action_removed_channel_member": "チャンネル「{{target}}」からあなたを削除しました", - "com_notifications_action_removed_knowledge_space_member": "ナレッジスペース「{{target}}」からあなたを削除しました", - "com_notifications_action_channel_made_private": "チャンネル「{{target}}」を非公開に変更しました。アクセスできなくなりました", - "com_notifications_action_knowledge_space_made_private": "ナレッジスペース「{{target}}」を非公開に変更しました。アクセスできなくなりました", - "com_notifications_action_channel_dismissed": "チャンネル「{{target}}」を解散しました", - "com_notifications_action_knowledge_space_deleted": "ナレッジスペース「{{target}}」を削除しました", + "com_notifications_action_menu_grant_revoked": "メニュー「{{target}}」へのアクセス権限は取り消されました", + "com_notifications_action_revoked_channel_admin": "チャンネル「{{target}}」の管理者ではなくなりました", + "com_notifications_action_revoked_knowledge_space_admin": "ナレッジスペース「{{target}}」の管理者ではなくなりました", + "com_notifications_action_removed_channel_member": "チャンネル「{{target}}」から削除されました", + "com_notifications_action_removed_knowledge_space_member": "ナレッジスペース「{{target}}」から削除されました", + "com_notifications_action_channel_made_private": "チャンネル「{{target}}」は非公開になり、アクセスできなくなりました", + "com_notifications_action_knowledge_space_made_private": "ナレッジスペース「{{target}}」は非公開になり、アクセスできなくなりました", + "com_notifications_action_channel_dismissed": "チャンネル「{{target}}」は解散されました", + "com_notifications_action_knowledge_space_deleted": "ナレッジスペース「{{target}}」は削除されました", "com_notifications_delete": "削除", "com_notifications_empty": "メッセージはありません", "com_notifications_empty_requests": "リクエストはありません", diff --git a/src/frontend/client/src/locales/zh-Hans/translation.json b/src/frontend/client/src/locales/zh-Hans/translation.json index f6c1f1677..60c2f11e0 100644 --- a/src/frontend/client/src/locales/zh-Hans/translation.json +++ b/src/frontend/client/src/locales/zh-Hans/translation.json @@ -374,38 +374,38 @@ "com_notifications_accept": "同意", "com_notifications_approved": "已同意", "com_notifications_rejected": "已拒绝", - "com_notifications_action_approved_channel": "同意了你订阅频道{{target}}的申请", - "com_notifications_action_approved_knowledge_space": "同意了你加入知识空间{{target}}的申请", - "com_notifications_action_assigned_channel_admin": "将你设为频道「{{target}}」的管理员", - "com_notifications_action_assigned_knowledge_space_admin": "将你设为知识空间「{{target}}」的管理员", + "com_notifications_action_approved_channel": "通过了你对「{{target}}」的审批申请", + "com_notifications_action_approved_knowledge_space": "通过了你对「{{target}}」的审批申请", + "com_notifications_action_assigned_channel_admin": "你已被设为频道「{{target}}」的管理员", + "com_notifications_action_assigned_knowledge_space_admin": "你已被设为知识空间「{{target}}」的管理员", "com_notifications_action_request_department_knowledge_space_upload": "申请上传文件到你{{target}}的部门知识空间", "com_notifications_action_approved_department_knowledge_space_upload": "同意了你上传部门知识空间文件{{target}}的申请", "com_notifications_action_rejected_department_knowledge_space_upload": "拒绝了你上传部门知识空间文件{{target}}的申请", "com_notifications_action_sensitive_rejected_department_knowledge_space_upload": "你的上传未通过内容安全检测", - "com_notifications_action_rejected_channel": "拒绝了你订阅频道{{target}}的申请", - "com_notifications_action_rejected_knowledge_space": "拒绝了你加入知识空间{{target}}的申请", - "com_notifications_action_request_channel": "申请订阅你{{target}}的频道", - "com_notifications_action_request_knowledge_space": "申请加入你{{target}}的知识空间", + "com_notifications_action_rejected_channel": "拒绝了你对「{{target}}」的审批申请", + "com_notifications_action_rejected_knowledge_space": "拒绝了你对「{{target}}」的审批申请", + "com_notifications_action_request_channel": "申请订阅频道「{{target}}」", + "com_notifications_action_request_knowledge_space": "申请加入知识空间「{{target}}」", "com_notifications_action_generic": "给你发送了一条通知", "com_notifications_action_generic_with_target": "给你发送了一条通知", "com_notifications_action_request_menu_access": "申请访问菜单「{{target}}」", - "com_notifications_action_approval_task_pending": "请审批「{{target}}」的申请", - "com_notifications_action_approval_task_rejected": "拒绝了你「{{target}}」的审批申请", - "com_notifications_action_approval_instance_approved": "通过了你「{{target}}」的审批申请", - "com_notifications_action_approval_instance_withdrawn": "撤回了「{{target}}」的审批申请,无需处理", - "com_notifications_action_approval_exception_cancelled": "已取消你「{{target}}」的审批申请", - "com_notifications_action_approval_exception_route_missing": "的审批申请出现分支未命中异常,需要处理「{{target}}」", - "com_notifications_action_approval_exception_approver_empty": "的审批申请出现审批人为空异常,需要处理「{{target}}」", - "com_notifications_action_approval_execute_failed": "的「{{target}}」审批已通过,但业务执行失败,请处理", - "com_notifications_action_menu_grant_revoked": "撤回了你对菜单「{{target}}」的访问权限", - "com_notifications_action_revoked_channel_admin": "取消了你频道「{{target}}」的管理员权限", - "com_notifications_action_revoked_knowledge_space_admin": "取消了你知识空间「{{target}}」的管理员权限", - "com_notifications_action_removed_channel_member": "将你移出频道「{{target}}」", - "com_notifications_action_removed_knowledge_space_member": "将你移出知识空间「{{target}}」", - "com_notifications_action_channel_made_private": "将频道「{{target}}」转为私有,你已无法访问", - "com_notifications_action_knowledge_space_made_private": "将知识空间「{{target}}」转为私有,你已无法访问", - "com_notifications_action_channel_dismissed": "解散了频道「{{target}}」", - "com_notifications_action_knowledge_space_deleted": "删除了知识空间「{{target}}」", + "com_notifications_action_approval_task_pending": "提交了「{{target}}」审批申请", + "com_notifications_action_approval_task_rejected": "拒绝了你对「{{target}}」的审批申请", + "com_notifications_action_approval_instance_approved": "通过了你对「{{target}}」的审批申请", + "com_notifications_action_approval_instance_withdrawn": "撤回了「{{target}}」的审批申请", + "com_notifications_action_approval_exception_cancelled": "你的「{{target}}」审批申请已被取消", + "com_notifications_action_approval_exception_route_missing": "「{{target}}」的审批申请未匹配到审批分支,请处理", + "com_notifications_action_approval_exception_approver_empty": "「{{target}}」的审批申请未解析到审批人,请处理", + "com_notifications_action_approval_execute_failed": "「{{target}}」审批已通过,但业务执行失败,请处理", + "com_notifications_action_menu_grant_revoked": "你对菜单「{{target}}」的访问权限已被撤回", + "com_notifications_action_revoked_channel_admin": "你已不再是频道「{{target}}」的管理员", + "com_notifications_action_revoked_knowledge_space_admin": "你已不再是知识空间「{{target}}」的管理员", + "com_notifications_action_removed_channel_member": "你已被移出频道「{{target}}」", + "com_notifications_action_removed_knowledge_space_member": "你已被移出知识空间「{{target}}」", + "com_notifications_action_channel_made_private": "频道「{{target}}」已转为私有,你已无法访问", + "com_notifications_action_knowledge_space_made_private": "知识空间「{{target}}」已转为私有,你已无法访问", + "com_notifications_action_channel_dismissed": "频道「{{target}}」已被解散", + "com_notifications_action_knowledge_space_deleted": "知识空间「{{target}}」已被删除", "com_notifications_delete": "删除消息", "com_notifications_empty": "暂无消息", "com_notifications_empty_requests": "暂无请求",