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": "暂无请求",