diff --git a/README.md b/README.md index 48ef451..71f1f35 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CMS 内容管理模块 -企业官网内容管理与钉钉审批工作流模块,基于Sage/bricks-framework开发。 +企业官网内容管理模块,基于Sage/bricks-framework开发。 ## 功能 @@ -9,7 +9,7 @@ - **栏目管理**: 官网页面栏目配置 - **商机线索**: 网站访客提交 + AI抽取 - **站点配置**: Hero标语、页脚等KV配置 -- **钉钉审批**: 内容发布审批工作流 +- **内容审批**: 发起发布审批(审批能力由 dingdingflow 独立模块提供) ## 数据库表 @@ -20,8 +20,6 @@ | cms_sections | 栏目管理 | | cms_leads | 商机线索 | | cms_site_config | 站点配置 | -| dd_approvals | 审批记录 | -| dd_approval_configs | 审批流程配置 | ## 安装 @@ -49,12 +47,17 @@ def init(): - cms_sections: 默认栏目配置 - dd_approval_configs: 默认审批配置 -## 环境变量 (钉钉审批) +## 审批依赖 -``` -DINGTALK_APP_KEY=xxx -DINGTALK_APP_SECRET=xxx -DINGTALK_AGENT_ID=xxx +内容审批走 **dingdingflow** 独立模块(软依赖): + +```python +from cms.init import load_cms +from dingdingflow.init import load_dingdingflow + +load_dingdingflow() # 必须先加载,cms 才能注册审批回调钩子 +load_cms() ``` -缺少环境变量时自动使用mock响应。 +未加载 dingdingflow 时,`submit_content_for_approval` 只把内容置为 pending +并返回提示,不报错。钉钉凭据/审批模板配置见 dingdingflow 的 README。 diff --git a/cms/__init__.py b/cms/__init__.py index c54009b..a1a08bf 100644 --- a/cms/__init__.py +++ b/cms/__init__.py @@ -1,6 +1,7 @@ -""" -CMS - 企业官网内容管理与审批工作流模块 -合并原 entcms + dingdingflow 两个子模块 +"""CMS - 企业官网内容管理模块 + +钉钉审批已独立为 dingdingflow 模块。本模块通过 ServerEnv 软依赖调用其 +submit_approval,并注册 content_publish 的审批结果回调钩子。 """ from .init import load_cms diff --git a/cms/dingtalk_client.py b/cms/dingtalk_client.py deleted file mode 100644 index d648d07..0000000 --- a/cms/dingtalk_client.py +++ /dev/null @@ -1,240 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -DingTalk API Client for approval workflow integration. -Reads credentials from environment variables (DINGTALK_APP_KEY, DINGTALK_APP_SECRET, DINGTALK_AGENT_ID). -Gracefully handles missing credentials by returning mock responses in dev mode. -""" - -import os -import json -import logging -import time -import urllib.request -import urllib.error - -logger = logging.getLogger(__name__) - - -class DingTalkClient: - """Client for DingTalk Open API - approval workflow operations.""" - - BASE_URL = "https://oapi.dingtalk.com" - - def __init__(self): - self.app_key = os.environ.get("DINGTALK_APP_KEY", "") - self.app_secret = os.environ.get("DINGTALK_APP_SECRET", "") - self.agent_id = os.environ.get("DINGTALK_AGENT_ID", "") - self.callback_token = os.environ.get("DINGTALK_CALLBACK_TOKEN", "") - self._access_token = None - self._token_expires_at = 0 - - if not self.app_key or not self.app_secret: - logger.warning( - "DingTalk credentials not configured (DINGTALK_APP_KEY / DINGTALK_APP_SECRET). " - "Running in dev/mock mode - API calls will return mock responses." - ) - - @property - def is_dev_mode(self): - """Return True if credentials are missing (dev/mock mode).""" - return not self.app_key or not self.app_secret - - def _http_post(self, url, data=None, params=None): - """Make HTTP POST request and return parsed JSON response.""" - if params: - query = "&".join(f"{k}={v}" for k, v in params.items()) - url = f"{url}?{query}" - - body = json.dumps(data).encode("utf-8") if data else b"" - req = urllib.request.Request( - url, - data=body, - headers={"Content-Type": "application/json"}, - method="POST", - ) - - try: - with urllib.request.urlopen(req, timeout=30) as resp: - resp_data = resp.read().decode("utf-8") - return json.loads(resp_data) - except urllib.error.HTTPError as e: - error_body = e.read().decode("utf-8", errors="replace") - logger.error("DingTalk API HTTP error %s: %s", e.code, error_body) - return {"errcode": e.code, "errmsg": f"HTTP {e.code}: {error_body}"} - except urllib.error.URLError as e: - logger.error("DingTalk API connection error: %s", str(e)) - return {"errcode": -1, "errmsg": f"Connection error: {str(e)}"} - except Exception as e: - logger.error("DingTalk API unexpected error: %s", str(e)) - return {"errcode": -1, "errmsg": str(e)} - - def _http_get(self, url, params=None): - """Make HTTP GET request and return parsed JSON response.""" - if params: - query = "&".join(f"{k}={v}" for k, v in params.items()) - url = f"{url}?{query}" - - req = urllib.request.Request(url, headers={"Content-Type": "application/json"}) - - try: - with urllib.request.urlopen(req, timeout=30) as resp: - resp_data = resp.read().decode("utf-8") - return json.loads(resp_data) - except urllib.error.HTTPError as e: - error_body = e.read().decode("utf-8", errors="replace") - logger.error("DingTalk API HTTP error %s: %s", e.code, error_body) - return {"errcode": e.code, "errmsg": f"HTTP {e.code}: {error_body}"} - except Exception as e: - logger.error("DingTalk API unexpected error: %s", str(e)) - return {"errcode": -1, "errmsg": str(e)} - - def get_access_token(self): - """ - Get DingTalk access token. Caches token until expiry. - Returns token string or empty string on failure. - """ - if self.is_dev_mode: - logger.info("Dev mode: returning mock access token") - return "mock_access_token_dev" - - # Return cached token if still valid (with 5-min buffer) - now = time.time() - if self._access_token and now < self._token_expires_at - 300: - return self._access_token - - url = f"{self.BASE_URL}/gettoken" - params = {"appkey": self.app_key, "appsecret": self.app_secret} - result = self._http_get(url, params=params) - - if result.get("errcode") == 0: - self._access_token = result.get("access_token", "") - expires_in = result.get("expires_in", 7200) - self._token_expires_at = now + expires_in - logger.info("DingTalk access token obtained, expires in %ss", expires_in) - return self._access_token - else: - logger.error( - "Failed to get DingTalk access token: %s", - result.get("errmsg", "unknown error"), - ) - return "" - - def create_approval_instance(self, process_code, form_data, originator_user_id): - """ - Create a DingTalk approval instance. - - Args: - process_code: DingTalk approval template code (from dd_approval_configs) - form_data: List of form component values, e.g.: - [{"name": "审批类型", "value": "内容发布"}, ...] - originator_user_id: DingTalk user ID of the applicant - - Returns: - dict with keys: - - success (bool) - - instance_id (str) - DingTalk process instance ID - - errmsg (str) - error message if failed - """ - if self.is_dev_mode: - mock_instance_id = f"mock_instance_{int(time.time())}" - logger.info( - "Dev mode: mock approval instance created: %s (process_code=%s)", - mock_instance_id, - process_code, - ) - return { - "success": True, - "instance_id": mock_instance_id, - "errmsg": "", - } - - token = self.get_access_token() - if not token: - return {"success": False, "instance_id": "", "errmsg": "Failed to get access token"} - - url = f"{self.BASE_URL}/topapi/processinstance/create" - payload = { - "agent_id": int(self.agent_id) if self.agent_id else 0, - "process_code": process_code, - "originator_user_id": originator_user_id, - "dept_id": -1, - "form_component_values": form_data, - } - - result = self._http_post(url, data=payload, params={"access_token": token}) - - if result.get("errcode") == 0: - instance_id = result.get("process_instance_id", "") - logger.info("DingTalk approval instance created: %s", instance_id) - return {"success": True, "instance_id": instance_id, "errmsg": ""} - else: - errmsg = result.get("errmsg", "unknown error") - logger.error("Failed to create DingTalk approval: %s", errmsg) - return {"success": False, "instance_id": "", "errmsg": errmsg} - - def get_approval_instance(self, instance_id): - """ - Get DingTalk approval instance details. - - Args: - instance_id: DingTalk process instance ID - - Returns: - dict with keys: - - success (bool) - - status (str) - NEW/RUNNING/COMPLETED/TERMINATED - - result (str) - agree/refuse (only when status=COMPLETED) - - data (dict) - full instance data - - errmsg (str) - error message if failed - """ - if self.is_dev_mode: - logger.info("Dev mode: mock get approval instance: %s", instance_id) - return { - "success": True, - "status": "COMPLETED", - "result": "agree", - "data": { - "process_instance_id": instance_id, - "status": "COMPLETED", - "result": "agree", - }, - "errmsg": "", - } - - token = self.get_access_token() - if not token: - return {"success": False, "status": "", "result": "", "data": {}, "errmsg": "Failed to get access token"} - - url = f"{self.BASE_URL}/topapi/processinstance/get" - payload = {"process_instance_id": instance_id} - - result = self._http_post(url, data=payload, params={"access_token": token}) - - if result.get("errcode") == 0: - pi = result.get("process_instance", {}) - status = pi.get("status", "") - pi_result = pi.get("result", "") - return { - "success": True, - "status": status, - "result": pi_result, - "data": pi, - "errmsg": "", - } - else: - errmsg = result.get("errmsg", "unknown error") - logger.error("Failed to get DingTalk approval instance: %s", errmsg) - return {"success": False, "status": "", "result": "", "data": {}, "errmsg": errmsg} - - -# Module-level singleton -_client_instance = None - - -def get_dingtalk_client(): - """Get or create the DingTalkClient singleton.""" - global _client_instance - if _client_instance is None: - _client_instance = DingTalkClient() - return _client_instance diff --git a/cms/init.py b/cms/init.py index 33a4d6b..c97dc01 100644 --- a/cms/init.py +++ b/cms/init.py @@ -9,8 +9,7 @@ cms - 企业CMS内容管理与钉钉审批工作流模块 - CMS Leads CRUD (cms_leads_*) - CMS Site Config CRUD (cms_site_config_*) - 公开API (get_published_content, get_latest_news, get_content_detail, submit_lead, get_visible_sections, get_site_config, get_category_options) - - 钉钉审批 CRUD (dd_approvals_*, dd_approval_configs_*) - - 审批业务逻辑 (submit_approval, get_approval_status, handle_dingtalk_callback) + - 内容审批发起(审批能力由 dingdingflow 独立模块提供,本模块只注册结果回调钩子) """ import json import logging @@ -19,7 +18,6 @@ import datetime from ahserver.serverenv import ServerEnv from appPublic.uniqueID import getID from sqlor.dbpools import DBPools -from .dingtalk_client import get_dingtalk_client logger = logging.getLogger(__name__) @@ -353,374 +351,47 @@ async def get_content_detail(content_id): # ═══════════════════════════════════════════════════════════════════════════════ async def submit_content_for_approval(content_id, title, applicant_id): - """提交内容审批(调用钉钉审批流程)""" + """提交内容审批。 + + 审批能力由 dingdingflow 独立模块提供,通过 ServerEnv 调用(软依赖): + 未安装/未加载该模块时只把内容置为 pending,不报错。 + """ dbname = _get_dbname() db = DBPools() async with db.sqlorContext(dbname) as sor: - # 更新内容状态为pending await sor.U('cms_content', {'id': content_id, 'status': 'pending'}) - # 调用审批流程 - result = await submit_approval('content_publish', content_id, title, applicant_id) - # 保存审批ID - if result and result.get('approval_id'): + + env = ServerEnv() + submit_approval = getattr(env, 'submit_approval', None) + if submit_approval is None: + logger.warning('submit_content_for_approval: dingdingflow 未加载,' + '内容已置 pending 但未发起审批') + return {'success': False, 'message': '审批模块(dingdingflow)未加载'} + + result = await submit_approval('content_publish', content_id, title, applicant_id) + if result and result.get('approval_id'): + async with db.sqlorContext(dbname) as sor: await sor.U('cms_content', {'id': content_id, 'approval_id': result['approval_id']}) - return result + return result -# ═══════════════════════════════════════════════════════════════════════════════ -# DD Approvals CRUD (原 dingdingflow) -# ═══════════════════════════════════════════════════════════════════════════════ +async def on_content_approval_done(biz_id, status, approval_id, comment): + """dingdingflow 审批结果回调钩子(biz_type='content_publish')。 -async def dd_approvals_create(data): - """创建审批记录""" - dbname = _get_dbname() - db = DBPools() - data['id'] = getID() - if 'org_id' not in data: - data['org_id'] = '0' - if 'status' not in data: - data['status'] = 'pending' - if 'created_at' not in data: - data['created_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - async with db.sqlorContext(dbname) as sor: - await sor.C('dd_approvals', data) - return {'id': data['id']} - - -async def dd_approvals_update(data): - """更新审批记录""" - dbname = _get_dbname() - db = DBPools() - record_id = data.get('id') - if not record_id: - raise ValueError('id is required for update') - async with db.sqlorContext(dbname) as sor: - await sor.U('dd_approvals', data) - return {'id': record_id} - - -async def dd_approvals_delete(data): - """删除审批记录""" - dbname = _get_dbname() - db = DBPools() - async with db.sqlorContext(dbname) as sor: - await sor.D('dd_approvals', data) - return data - - -async def dd_approvals_list(ns=None): - """查询审批记录列表""" - dbname = _get_dbname() - db = DBPools() - async with db.sqlorContext(dbname) as sor: - ns = ns or {} - ns.setdefault('sort', 'created_at desc') - rows = await sor.R('dd_approvals', ns) - return {'rows': rows, 'total': len(rows)} - - -# ═══════════════════════════════════════════════════════════════════════════════ -# DD Approval Configs CRUD (原 dingdingflow) -# ═══════════════════════════════════════════════════════════════════════════════ - -async def dd_approval_configs_create(data): - """创建审批配置""" - dbname = _get_dbname() - db = DBPools() - data['id'] = getID() - if 'org_id' not in data: - data['org_id'] = '0' - if 'is_active' not in data: - data['is_active'] = '1' - now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - if 'created_at' not in data: - data['created_at'] = now_str - data['updated_at'] = now_str - async with db.sqlorContext(dbname) as sor: - await sor.C('dd_approval_configs', data) - return {'id': data['id']} - - -async def dd_approval_configs_update(data): - """更新审批配置""" - dbname = _get_dbname() - db = DBPools() - record_id = data.get('id') - if not record_id: - raise ValueError('id is required for update') - data['updated_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - async with db.sqlorContext(dbname) as sor: - await sor.U('dd_approval_configs', data) - return {'id': record_id} - - -async def dd_approval_configs_delete(data): - """删除审批配置""" - dbname = _get_dbname() - db = DBPools() - async with db.sqlorContext(dbname) as sor: - await sor.D('dd_approval_configs', data) - return data - - -async def dd_approval_configs_list(ns=None): - """查询审批配置列表""" - dbname = _get_dbname() - db = DBPools() - async with db.sqlorContext(dbname) as sor: - ns = ns or {} - ns.setdefault('sort', 'biz_type') - rows = await sor.R('dd_approval_configs', ns) - return {'rows': rows, 'total': len(rows)} - - -async def get_approval_config_by_type(org_id, biz_type): - """根据org_id和biz_type获取审批配置""" - dbname = _get_dbname() - db = DBPools() - async with db.sqlorContext(dbname) as sor: - rows = await sor.R('dd_approval_configs', {'org_id': org_id, 'biz_type': biz_type}) - if rows: - return rows[0] - return None - - -# ═══════════════════════════════════════════════════════════════════════════════ -# Approval Workflow Business Logic (原 dingdingflow) -# ═══════════════════════════════════════════════════════════════════════════════ - -async def submit_approval(biz_type, biz_id, title, applicant_id, org_id='0'): + 由 load_cms() 注册:审批通过→published,驳回→draft。 + 这是原先硬编码在钉钉回调里的 cms_content 回写逻辑,现归位到 cms 自己。 """ - 提交审批请求: - 1. 查找审批配置 - 2. 创建审批记录 - 3. 调用钉钉API创建审批实例 - 4. 保存钉钉实例ID - """ - client = get_dingtalk_client() - - # 查找审批配置 - config = await get_approval_config_by_type(org_id, biz_type) - if not config: - logger.error('No approval config found for org_id=%s, biz_type=%s', org_id, biz_type) - return {'success': False, 'message': f'No approval config found for biz_type={biz_type}'} - - process_code = config.get('process_code', '') if isinstance(config, dict) else getattr(config, 'process_code', '') or '' - form_config_raw = config.get('form_config', '') if isinstance(config, dict) else getattr(config, 'form_config', '') or '' - - # 从form_config构建表单数据 - form_data = [] - if form_config_raw: - try: - form_config = json.loads(form_config_raw) if isinstance(form_config_raw, str) else form_config_raw - if isinstance(form_config, list): - form_data = form_config - except (json.JSONDecodeError, TypeError) as e: - logger.warning('Failed to parse form_config: %s', str(e)) - - # 无表单数据时创建最小表单 - if not form_data: - form_data = [ - {'name': '审批标题', 'value': title}, - {'name': '业务类型', 'value': biz_type}, - ] - - # 调用钉钉API - result = client.create_approval_instance(process_code, form_data, applicant_id) - - if not result['success']: - # API失败仍然创建记录 - approval_data = { - 'biz_type': biz_type, - 'biz_id': biz_id, - 'title': title, - 'applicant_id': applicant_id, - 'org_id': org_id, - 'status': 'pending', - 'dingtalk_instance_id': '', - 'comment': f"DingTalk API error: {result.get('errmsg', '')}", - } - approval = await dd_approvals_create(approval_data) - return { - 'success': False, - 'message': f"DingTalk API failed: {result.get('errmsg', '')}", - 'approval_id': approval['id'], - } - - # 创建审批记录 - approval_data = { - 'biz_type': biz_type, - 'biz_id': biz_id, - 'title': title, - 'applicant_id': applicant_id, - 'org_id': org_id, - 'status': 'pending', - 'dingtalk_instance_id': result['instance_id'], - } - approval = await dd_approvals_create(approval_data) - - logger.info( - 'Approval submitted: id=%s, instance=%s, biz=%s/%s', - approval['id'], result['instance_id'], biz_type, biz_id, - ) - - return { - 'success': True, - 'message': 'Approval submitted successfully', - 'approval_id': approval['id'], - 'instance_id': result['instance_id'], - } - - -async def get_approval_status(approval_id): - """查询钉钉审批最新状态并同步到本地""" + content_status = {'approved': 'published', 'rejected': 'draft'}.get(status, '') + if not content_status: + return + data = {'id': biz_id, 'status': content_status} + if content_status == 'published': + data['published_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') dbname = _get_dbname() - db = DBPools() + async with DBPools().sqlorContext(dbname) as sor: + await sor.U('cms_content', data) + logger.info('cms_content %s -> %s (approval %s)', biz_id, content_status, approval_id) - # 获取本地记录 - async with db.sqlorContext(dbname) as sor: - rows = await sor.R('dd_approvals', {'id': approval_id}) - if not rows: - return {'success': False, 'message': 'Approval record not found'} - - record = rows[0] - instance_id = record.get('dingtalk_instance_id', '') if isinstance(record, dict) else getattr(record, 'dingtalk_instance_id', '') - current_status = record.get('status', '') if isinstance(record, dict) else getattr(record, 'status', '') - - # 已完成无需再查 - if current_status in ('approved', 'rejected', 'cancelled'): - return { - 'success': True, - 'status': current_status, - 'approval_id': approval_id, - 'instance_id': instance_id, - } - - if not instance_id: - return { - 'success': True, - 'status': current_status, - 'approval_id': approval_id, - 'instance_id': '', - 'message': 'No DingTalk instance ID, cannot sync', - } - - # 查询钉钉 - client = get_dingtalk_client() - dt_result = client.get_approval_instance(instance_id) - - if not dt_result['success']: - return { - 'success': False, - 'message': f"DingTalk query failed: {dt_result.get('errmsg', '')}", - 'status': current_status, - } - - # 映射钉钉状态 - dt_status = dt_result.get('status', '') - dt_result_val = dt_result.get('result', '') - - new_status = current_status - if dt_status == 'COMPLETED': - if dt_result_val == 'agree': - new_status = 'approved' - elif dt_result_val == 'refuse': - new_status = 'rejected' - elif dt_status == 'TERMINATED': - new_status = 'cancelled' - - # 更新本地记录 - if new_status != current_status: - update_data = {'id': approval_id, 'status': new_status} - if new_status in ('approved', 'rejected', 'cancelled'): - update_data['completed_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - await dd_approvals_update(update_data) - logger.info('Approval %s status synced: %s -> %s', approval_id, current_status, new_status) - - return { - 'success': True, - 'status': new_status, - 'approval_id': approval_id, - 'instance_id': instance_id, - } - - -async def handle_dingtalk_callback(data): - """ - 处理钉钉webhook回调。 - 钉钉在审批状态变化时发送回调。 - """ - logger.info('DingTalk callback received: %s', json.dumps(data, ensure_ascii=False)) - - instance_id = data.get('processInstanceId', '') - if not instance_id: - return {'success': False, 'message': 'Missing processInstanceId'} - - callback_type = data.get('type', '') - if callback_type != 'bpms_instance_change': - logger.info('Ignoring callback type: %s', callback_type) - return {'success': True, 'message': f'Ignored callback type: {callback_type}'} - - # 查找本地审批记录 - dbname = _get_dbname() - db = DBPools() - async with db.sqlorContext(dbname) as sor: - rows = await sor.R('dd_approvals', {'dingtalk_instance_id': instance_id}) - - if not rows: - logger.warning('No local approval found for instance_id=%s', instance_id) - return {'success': False, 'message': f'No approval found for instance {instance_id}'} - - record = rows[0] - record_id = record.get('id', '') if isinstance(record, dict) else getattr(record, 'id', '') - current_status = record.get('status', '') if isinstance(record, dict) else getattr(record, 'status', '') - - # 映射回调状态 - dt_result = data.get('result', '') - new_status = current_status - if dt_result == 'agree': - new_status = 'approved' - elif dt_result == 'refuse': - new_status = 'rejected' - elif callback_type == 'terminate': - new_status = 'cancelled' - - # 更新记录 - if new_status != current_status: - update_data = { - 'id': record_id, - 'status': new_status, - 'comment': data.get('remark', ''), - } - if new_status in ('approved', 'rejected', 'cancelled'): - update_data['completed_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - await dd_approvals_update(update_data) - logger.info('Callback: approval %s updated to %s', record_id, new_status) - - # 通知CMS内容状态变更 - biz_type = record.get('biz_type', '') if isinstance(record, dict) else getattr(record, 'biz_type', '') - biz_id = record.get('biz_id', '') if isinstance(record, dict) else getattr(record, 'biz_id', '') - if biz_type == 'content_publish' and biz_id: - content_status = 'published' if new_status == 'approved' else 'draft' if new_status == 'rejected' else '' - if content_status: - content_update = {'id': biz_id, 'status': content_status} - if content_status == 'published': - content_update['published_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - async with db.sqlorContext(dbname) as sor: - await sor.U('cms_content', content_update) - logger.info('Callback: cms_content %s updated to %s', biz_id, content_status) - - return { - 'success': True, - 'message': f'Approval {record_id} updated to {new_status}', - 'approval_id': record_id, - 'status': new_status, - } - - -# ═══════════════════════════════════════════════════════════════════════════════ -# Module Loader -# ═══════════════════════════════════════════════════════════════════════════════ def load_cms(): """注册所有CMS模块函数到ServerEnv""" @@ -766,26 +437,12 @@ def load_cms(): env.get_content_detail = get_content_detail env.submit_content_for_approval = submit_content_for_approval - # DD Approvals CRUD - env.dd_approvals_create = dd_approvals_create - env.dd_approvals_update = dd_approvals_update - env.dd_approvals_delete = dd_approvals_delete - env.dd_approvals_list = dd_approvals_list - - # DD Approval Configs CRUD - env.dd_approval_configs_create = dd_approval_configs_create - env.dd_approval_configs_update = dd_approval_configs_update - env.dd_approval_configs_delete = dd_approval_configs_delete - env.dd_approval_configs_list = dd_approval_configs_list - env.get_approval_config_by_type = get_approval_config_by_type - - # Approval Business Logic - env.submit_approval = submit_approval - env.get_approval_status = get_approval_status - env.handle_dingtalk_callback = handle_dingtalk_callback - - # DingTalk Client - env.get_dingtalk_client = get_dingtalk_client + # 审批结果回调钩子(审批能力在 dingdingflow 独立模块) + register_biz_handler = getattr(env, 'register_biz_handler', None) + if register_biz_handler: + register_biz_handler('content_publish', on_content_approval_done) + else: + logger.info('dingdingflow 未加载,跳过 content_publish 审批钩子注册') logger.info('cms module loaded (v%s)', MODULE_VERSION) return True diff --git a/i18n/zh/msg.txt b/i18n/zh/msg.txt index 6d32599..a960b16 100644 --- a/i18n/zh/msg.txt +++ b/i18n/zh/msg.txt @@ -96,7 +96,4 @@ title is required: title is required 配置值: 配置值 配置组(hero/footer/contact/seo): 配置组(hero/footer/contact/seo) 配置键: 配置键 -钉钉审批实例ID: 钉钉审批实例ID -钉钉审批模板编码: 钉钉审批模板编码 -钉钉应用AgentId: 钉钉应用AgentId 静态内容(用于hero/cta等固定内容栏目): 静态内容(用于hero/cta等固定内容栏目) diff --git a/json/dd_approval_configs.json b/json/dd_approval_configs.json deleted file mode 100644 index 3f327f8..0000000 --- a/json/dd_approval_configs.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "tblname": "dd_approval_configs", - "alias": "dd_approval_configs", - "title": "审批流程配置", - "params": { - "sortby": ["biz_type"], - "browserfields": { - "exclouded": ["id", "form_config"], - "alters": { - "is_active": { - "uitype": "code", - "data": [ - {"value": "1", "text": "启用"}, - {"value": "0", "text": "停用"} - ] - } - } - }, - "editable": { - "new_data_url": "{{entire_url('../api/dingdingflow/dd_approval_configs_create.dspy')}}", - "update_data_url": "{{entire_url('../api/dingdingflow/dd_approval_configs_update.dspy')}}", - "delete_data_url": "{{entire_url('../api/dingdingflow/dd_approval_configs_delete.dspy')}}" - } - } -} diff --git a/json/dd_approvals.json b/json/dd_approvals.json deleted file mode 100644 index 6ea6fe9..0000000 --- a/json/dd_approvals.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "tblname": "dd_approvals", - "alias": "dd_approvals", - "title": "审批记录", - "params": { - "sortby": ["created_at desc"], - "data_filter": { - "AND": [ - {"field": "status", "op": "=", "var": "status_filter"}, - {"field": "biz_type", "op": "=", "var": "biz_type_filter"}, - {"field": "title", "op": "LIKE", "var": "title_filter"} - ] - }, - "browserfields": { - "exclouded": ["id"], - "alters": { - "status": { - "uitype": "code", - "data": [ - {"value": "pending", "text": "待审批"}, - {"value": "approved", "text": "已通过"}, - {"value": "rejected", "text": "已拒绝"}, - {"value": "cancelled", "text": "已取消"} - ] - }, - "biz_type": { - "uitype": "code", - "data": [ - {"value": "content_publish", "text": "内容发布"}, - {"value": "content_update", "text": "内容修改"}, - {"value": "content_delete", "text": "内容删除"} - ] - } - } - }, - "editable": { - "new_data_url": "{{entire_url('../api/dingdingflow/dd_approvals_create.dspy')}}", - "update_data_url": "{{entire_url('../api/dingdingflow/dd_approvals_update.dspy')}}", - "delete_data_url": "{{entire_url('../api/dingdingflow/dd_approvals_delete.dspy')}}" - } - } -} diff --git a/models/dd_approval_configs.json b/models/dd_approval_configs.json deleted file mode 100644 index 0549e58..0000000 --- a/models/dd_approval_configs.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "summary": [ - { - "name": "dd_approval_configs", - "title": "审批流程配置表", - "primary": [ - "id" - ] - } - ], - "fields": [ - { - "name": "id", - "title": "主键ID", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "org_id", - "title": "组织ID", - "type": "str", - "length": 32, - "default": "0" - }, - { - "name": "biz_type", - "title": "业务类型", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "biz_type_title", - "title": "业务类型名称", - "type": "str", - "length": 100 - }, - { - "name": "process_code", - "title": "钉钉审批模板编码", - "type": "str", - "length": 100 - }, - { - "name": "agent_id", - "title": "钉钉应用AgentId", - "type": "str", - "length": 100 - }, - { - "name": "form_config", - "title": "表单字段配置JSON", - "type": "text" - }, - { - "name": "is_active", - "title": "是否启用(1/0)", - "type": "str", - "length": 1, - "default": "1" - }, - { - "name": "created_at", - "title": "创建时间", - "type": "timestamp" - }, - { - "name": "updated_at", - "title": "更新时间", - "type": "timestamp" - } - ], - "indexes": [ - { - "name": "idx_apvcfg_org_type", - "idxtype": "unique", - "idxfields": [ - "org_id", - "biz_type" - ] - } - ], - "codes": [] -} diff --git a/models/dd_approvals.json b/models/dd_approvals.json deleted file mode 100644 index 5ad5763..0000000 --- a/models/dd_approvals.json +++ /dev/null @@ -1,114 +0,0 @@ -{ - "summary": [ - { - "name": "dd_approvals", - "title": "审批记录表", - "primary": [ - "id" - ] - } - ], - "fields": [ - { - "name": "id", - "title": "主键ID", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "org_id", - "title": "组织ID", - "type": "str", - "length": 32, - "default": "0" - }, - { - "name": "biz_type", - "title": "业务类型(content_publish等)", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "biz_id", - "title": "业务数据ID", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "title", - "title": "审批标题", - "type": "str", - "length": 255, - "nullable": "no" - }, - { - "name": "applicant_id", - "title": "申请人ID", - "type": "str", - "length": 32, - "nullable": "no" - }, - { - "name": "approver_id", - "title": "审批人ID", - "type": "str", - "length": 32 - }, - { - "name": "dingtalk_instance_id", - "title": "钉钉审批实例ID", - "type": "str", - "length": 100 - }, - { - "name": "status", - "title": "状态(pending/approved/rejected/cancelled)", - "type": "str", - "length": 32, - "default": "pending" - }, - { - "name": "comment", - "title": "审批意见", - "type": "text" - }, - { - "name": "created_at", - "title": "创建时间", - "type": "timestamp" - }, - { - "name": "completed_at", - "title": "完成时间", - "type": "datetime" - } - ], - "indexes": [ - { - "name": "idx_approval_biz", - "idxtype": "index", - "idxfields": [ - "biz_type", - "biz_id" - ] - }, - { - "name": "idx_approval_status", - "idxtype": "index", - "idxfields": [ - "org_id", - "status" - ] - }, - { - "name": "idx_approval_applicant", - "idxtype": "index", - "idxfields": [ - "applicant_id" - ] - } - ] -} diff --git a/scripts/load_path.py b/scripts/load_path.py index 16fe983..b56e4d7 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -78,20 +78,9 @@ PATHS = [ f"/{MOD}/api/cms_site_config_delete.dspy", f"/{MOD}/api/cms_site_config_list.dspy", # DD Approvals - f"/{MOD}/dd_approvals", f"/{MOD}/dd_approvals/%", - f"/{MOD}/api/dd_approvals_create.dspy", - f"/{MOD}/api/dd_approvals_update.dspy", - f"/{MOD}/api/dd_approvals_delete.dspy", - f"/{MOD}/api/dd_approvals_list.dspy", # DD Approval Configs - f"/{MOD}/dd_approval_configs", f"/{MOD}/dd_approval_configs/%", - f"/{MOD}/api/dd_approval_configs_create.dspy", - f"/{MOD}/api/dd_approval_configs_update.dspy", - f"/{MOD}/api/dd_approval_configs_delete.dspy", - f"/{MOD}/api/dd_approval_configs_list.dspy", # DingTalk f"/{MOD}/api/submit_approval.dspy", - f"/{MOD}/api/dingtalk_callback.dspy", # Export (in portal wwwroot/api/, not /cms/ prefix) f"/api/export_leads.dspy", f"/api/export_content.dspy", diff --git a/wwwroot/api/dd_approval_configs_create.dspy b/wwwroot/api/dd_approval_configs_create.dspy deleted file mode 100644 index 170f84d..0000000 --- a/wwwroot/api/dd_approval_configs_create.dspy +++ /dev/null @@ -1,37 +0,0 @@ -result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} - -try: - biz_type = params_kw.get('biz_type', '') - biz_type_title = params_kw.get('biz_type_title', '') - process_code = params_kw.get('process_code', '') - - if not biz_type: - result['options'] = {'title': 'Error', 'message': 'biz_type is required', 'type': 'error'} - else: - new_id = getID() - org_id = (await get_userorgid()) or '0' - now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - - dbname = get_module_dbname('dingdingflow') - async with DBPools().sqlorContext(dbname) as sor: - await sor.sqlExe( - "INSERT INTO dd_approval_configs (id, org_id, biz_type, biz_type_title, process_code, agent_id, form_config, is_active, created_at, updated_at) VALUES (${id}$, ${org_id}$, ${biz_type}$, ${biz_type_title}$, ${process_code}$, ${agent_id}$, ${form_config}$, ${is_active}$, ${created_at}$, ${updated_at}$)", - { - 'id': new_id, - 'org_id': org_id, - 'biz_type': biz_type, - 'biz_type_title': biz_type_title, - 'process_code': process_code, - 'agent_id': params_kw.get('agent_id', ''), - 'form_config': params_kw.get('form_config', ''), - 'is_active': params_kw.get('is_active', '1'), - 'created_at': now_str, - 'updated_at': now_str - } - ) - - result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批配置创建成功', 'type': 'success'}} -except Exception as e: - result['options'] = {'title': 'Error', 'message': f'创建失败: {str(e)}', 'type': 'error'} - -return result diff --git a/wwwroot/api/dd_approval_configs_delete.dspy b/wwwroot/api/dd_approval_configs_delete.dspy deleted file mode 100644 index c943f00..0000000 --- a/wwwroot/api/dd_approval_configs_delete.dspy +++ /dev/null @@ -1,16 +0,0 @@ -result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} - -try: - record_id = params_kw.get('id', '') - if not record_id: - result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'} - else: - dbname = get_module_dbname('dingdingflow') - async with DBPools().sqlorContext(dbname) as sor: - await sor.sqlExe("DELETE FROM dd_approval_configs WHERE id=${id}$", {'id': record_id}) - - result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批配置删除成功', 'type': 'success'}} -except Exception as e: - result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'} - -return result diff --git a/wwwroot/api/dd_approval_configs_list.dspy b/wwwroot/api/dd_approval_configs_list.dspy deleted file mode 100644 index e608a4d..0000000 --- a/wwwroot/api/dd_approval_configs_list.dspy +++ /dev/null @@ -1,54 +0,0 @@ -result = {'success': False, 'rows': [], 'total': 0} - -try: - dbname = get_module_dbname('dingdingflow') - async with DBPools().sqlorContext(dbname) as sor: - where_clauses = [] - where_ns = {} - - # Optional filtering - is_active = params_kw.get('is_active', '') - if is_active: - where_clauses.append("is_active=${is_active}$") - where_ns['is_active'] = is_active - - biz_type = params_kw.get('biz_type', '') - if biz_type: - where_clauses.append("biz_type=${biz_type}$") - where_ns['biz_type'] = biz_type - - where_sql = " AND ".join(where_clauses) - where_prefix = " WHERE " if where_clauses else "" - - # Count query - count_sql = "SELECT count(*) rcnt FROM dd_approval_configs" + where_prefix + where_sql - count_rows = await sor.sqlExe(count_sql, where_ns) - total = 0 - if count_rows and len(count_rows) > 0: - r = count_rows[0] - total = getattr(r, 'rcnt', 0) - - if total > 0: - ns = { - 'page': int(params_kw.get('page', 1)), - 'rows': int(params_kw.get('rows', 20)), - 'sort': params_kw.get('sort', 'biz_type') - } - sql = "SELECT id, org_id, biz_type, biz_type_title, process_code, agent_id, form_config, is_active, created_at, updated_at FROM dd_approval_configs" + where_prefix + where_sql - query_ns = dict(list(ns.items()) + list(where_ns.items())) - rows = await sor.sqlExe(sql, query_ns) - - if isinstance(rows, dict): - result['rows'] = rows.get('rows', []) - result['total'] = rows.get('total', total) - elif rows: - result['rows'] = [dict(r) if hasattr(r, 'keys') else r for r in rows] - result['total'] = total - else: - result['total'] = 0 - - result['success'] = True -except Exception as e: - result['error'] = str(e) - -return result diff --git a/wwwroot/api/dd_approval_configs_update.dspy b/wwwroot/api/dd_approval_configs_update.dspy deleted file mode 100644 index fe5eca8..0000000 --- a/wwwroot/api/dd_approval_configs_update.dspy +++ /dev/null @@ -1,32 +0,0 @@ -result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} - -try: - record_id = params_kw.get('id', '') - if not record_id: - result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'} - else: - dbname = get_module_dbname('dingdingflow') - async with DBPools().sqlorContext(dbname) as sor: - update_fields = [] - update_ns = {'id': record_id} - - for field in ['biz_type', 'biz_type_title', 'process_code', 'agent_id', 'form_config', 'is_active']: - val = params_kw.get(field) - if val is not None: - update_fields.append(f"{field}=${field}$") - update_ns[field] = val - - # Always update updated_at - now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - update_fields.append("updated_at=${updated_at}$") - update_ns['updated_at'] = now_str - - if update_fields: - set_clause = ", ".join(update_fields) - await sor.sqlExe(f"UPDATE dd_approval_configs SET {set_clause} WHERE id=${id}$", update_ns) - - result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批配置更新成功', 'type': 'success'}} -except Exception as e: - result['options'] = {'title': 'Error', 'message': f'更新失败: {str(e)}', 'type': 'error'} - -return result diff --git a/wwwroot/api/dd_approvals_create.dspy b/wwwroot/api/dd_approvals_create.dspy deleted file mode 100644 index cc6807b..0000000 --- a/wwwroot/api/dd_approvals_create.dspy +++ /dev/null @@ -1,39 +0,0 @@ -result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} - -try: - biz_type = params_kw.get('biz_type', '') - biz_id = params_kw.get('biz_id', '') - title = params_kw.get('title', '') - applicant_id = params_kw.get('applicant_id', '') - - if not biz_type or not title: - result['options'] = {'title': 'Error', 'message': 'biz_type and title are required', 'type': 'error'} - else: - new_id = getID() - org_id = (await get_userorgid()) or '0' - now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S') - - dbname = get_module_dbname('dingdingflow') - async with DBPools().sqlorContext(dbname) as sor: - await sor.sqlExe( - "INSERT INTO dd_approvals (id, org_id, biz_type, biz_id, title, applicant_id, approver_id, dingtalk_instance_id, status, comment, created_at) VALUES (${id}$, ${org_id}$, ${biz_type}$, ${biz_id}$, ${title}$, ${applicant_id}$, ${approver_id}$, ${dingtalk_instance_id}$, ${status}$, ${comment}$, ${created_at}$)", - { - 'id': new_id, - 'org_id': org_id, - 'biz_type': biz_type, - 'biz_id': biz_id, - 'title': title, - 'applicant_id': applicant_id, - 'approver_id': params_kw.get('approver_id', ''), - 'dingtalk_instance_id': params_kw.get('dingtalk_instance_id', ''), - 'status': params_kw.get('status', 'pending'), - 'comment': params_kw.get('comment', ''), - 'created_at': now_str - } - ) - - result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批记录创建成功', 'type': 'success'}} -except Exception as e: - result['options'] = {'title': 'Error', 'message': f'创建失败: {str(e)}', 'type': 'error'} - -return result diff --git a/wwwroot/api/dd_approvals_delete.dspy b/wwwroot/api/dd_approvals_delete.dspy deleted file mode 100644 index bbb198e..0000000 --- a/wwwroot/api/dd_approvals_delete.dspy +++ /dev/null @@ -1,16 +0,0 @@ -result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} - -try: - record_id = params_kw.get('id', '') - if not record_id: - result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'} - else: - dbname = get_module_dbname('dingdingflow') - async with DBPools().sqlorContext(dbname) as sor: - await sor.sqlExe("DELETE FROM dd_approvals WHERE id=${id}$", {'id': record_id}) - - result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批记录删除成功', 'type': 'success'}} -except Exception as e: - result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'} - -return result diff --git a/wwwroot/api/dd_approvals_list.dspy b/wwwroot/api/dd_approvals_list.dspy deleted file mode 100644 index 5c2eed7..0000000 --- a/wwwroot/api/dd_approvals_list.dspy +++ /dev/null @@ -1,72 +0,0 @@ -result = {'success': False, 'rows': [], 'total': 0} - -try: - dbname = get_module_dbname('dingdingflow') - async with DBPools().sqlorContext(dbname) as sor: - # Build WHERE clause from data_filter or direct params - where_clauses = [] - where_ns = {} - - # Support data_filter from CRUD search popup - data_filter_str = params_kw.get('data_filter', '') - if data_filter_str: - # Individual filter values passed alongside data_filter - status_filter = params_kw.get('status_filter', '') - biz_type_filter = params_kw.get('biz_type_filter', '') - title_filter = params_kw.get('title_filter', '') - - if status_filter: - where_clauses.append("status=${status_filter}$") - where_ns['status_filter'] = status_filter - if biz_type_filter: - where_clauses.append("biz_type=${biz_type_filter}$") - where_ns['biz_type_filter'] = biz_type_filter - if title_filter: - where_clauses.append("title LIKE ${title_filter}$") - where_ns['title_filter'] = f'%{title_filter}%' - else: - # Direct param filtering - status = params_kw.get('status', '') - if status: - where_clauses.append("status=${status}$") - where_ns['status'] = status - biz_type = params_kw.get('biz_type', '') - if biz_type: - where_clauses.append("biz_type=${biz_type}$") - where_ns['biz_type'] = biz_type - - where_sql = " AND ".join(where_clauses) - where_prefix = " WHERE " if where_clauses else "" - - # Count query - count_sql = "SELECT count(*) rcnt FROM dd_approvals" + where_prefix + where_sql - count_rows = await sor.sqlExe(count_sql, where_ns) - total = 0 - if count_rows and len(count_rows) > 0: - r = count_rows[0] - total = getattr(r, 'rcnt', 0) - - if total > 0: - ns = { - 'page': int(params_kw.get('page', 1)), - 'rows': int(params_kw.get('rows', 20)), - 'sort': params_kw.get('sort', 'created_at desc') - } - sql = "SELECT id, org_id, biz_type, biz_id, title, applicant_id, approver_id, dingtalk_instance_id, status, comment, created_at, completed_at FROM dd_approvals" + where_prefix + where_sql - query_ns = dict(list(ns.items()) + list(where_ns.items())) - rows = await sor.sqlExe(sql, query_ns) - - if isinstance(rows, dict): - result['rows'] = rows.get('rows', []) - result['total'] = rows.get('total', total) - elif rows: - result['rows'] = [dict(r) if hasattr(r, 'keys') else r for r in rows] - result['total'] = total - else: - result['total'] = 0 - - result['success'] = True -except Exception as e: - result['error'] = str(e) - -return result diff --git a/wwwroot/api/dd_approvals_update.dspy b/wwwroot/api/dd_approvals_update.dspy deleted file mode 100644 index bb0b27d..0000000 --- a/wwwroot/api/dd_approvals_update.dspy +++ /dev/null @@ -1,32 +0,0 @@ -result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}} - -try: - record_id = params_kw.get('id', '') - if not record_id: - result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'} - else: - dbname = get_module_dbname('dingdingflow') - async with DBPools().sqlorContext(dbname) as sor: - update_fields = [] - update_ns = {'id': record_id} - - for field in ['biz_type', 'biz_id', 'title', 'applicant_id', 'approver_id', 'dingtalk_instance_id', 'status', 'comment']: - val = params_kw.get(field) - if val is not None: - update_fields.append(f"{field}=${field}$") - update_ns[field] = val - - completed_at = params_kw.get('completed_at') - if completed_at: - update_fields.append("completed_at=${completed_at}$") - update_ns['completed_at'] = completed_at - - if update_fields: - set_clause = ", ".join(update_fields) - await sor.sqlExe(f"UPDATE dd_approvals SET {set_clause} WHERE id=${id}$", update_ns) - - result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批记录更新成功', 'type': 'success'}} -except Exception as e: - result['options'] = {'title': 'Error', 'message': f'更新失败: {str(e)}', 'type': 'error'} - -return result diff --git a/wwwroot/api/dingtalk_callback.dspy b/wwwroot/api/dingtalk_callback.dspy deleted file mode 100644 index 67e8559..0000000 --- a/wwwroot/api/dingtalk_callback.dspy +++ /dev/null @@ -1,45 +0,0 @@ -result = {'success': False, 'message': 'Invalid callback'} - -try: - # DingTalk callback data comes via params_kw (POST body auto-parsed) - callback_data = {} - - # params_kw contains the parsed POST body fields - process_instance_id = params_kw.get('processInstanceId', '') - callback_type = params_kw.get('type', '') - callback_result = params_kw.get('result', '') - staff_id = params_kw.get('staffId', '') - process_code = params_kw.get('processCode', '') - remark = params_kw.get('remark', '') - - # Also handle nested JSON body case where entire body is under a key - if not process_instance_id: - body = params_kw.get('body', None) - if isinstance(body, dict): - process_instance_id = body.get('processInstanceId', '') - callback_type = body.get('type', '') - callback_result = body.get('result', '') - staff_id = body.get('staffId', '') - process_code = body.get('processCode', '') - remark = body.get('remark', '') - - callback_data = { - 'processInstanceId': process_instance_id, - 'type': callback_type, - 'result': callback_result, - 'staffId': staff_id, - 'processCode': process_code, - 'remark': remark, - } - - if not process_instance_id: - result = {'success': False, 'message': 'Missing processInstanceId'} - else: - # Call the handle_dingtalk_callback function registered via load_dingdingflow() - callback_result_data = await handle_dingtalk_callback(callback_data) - result = callback_result_data - -except Exception as e: - result = {'success': False, 'message': f'Callback processing error: {str(e)}'} - -return result