收藏
回答

调用 tcb-api.tencentcloudapi.com/web/functions/ 不成功?

requestId: 9a7c9ab6-909c-4344-ba98-77719633a351

请求在到达腾讯云的 API 接入层 (tcb-api.tencentcloudapi.com/web) 时,由于参数校验失败(INVALID_PARAM),未能成功路由到您的云函数内部执行。


尝试调用 tcb-api.tencentcloudapi.com/web/functions/{function_name} 的过程。

tcb_adapter.py

# flask_admin_panel/app/adapters/tcb_adapter.py


import requests
import json
import base64
import hmac
import hashlib
import time
import datetime
from urllib.parse import quote_plus
from flask import current_app
import logging


logger = logging.getLogger(__name__)


class TCBAdapter:
    def __init__(self, env_id=None, secret_id=None, secret_key=None):
        # ... (属性定义与之前相同) ...
        self._env_id = env_id
        self._secret_id = secret_id
        self._secret_key = secret_key
        self._base_url = "https://tcb-api.tencentcloudapi.com/web"


    @property
    def env_id(self):
        if self._env_id is None:
            if not current_app:
                raise RuntimeError("TCBAdapter: Flask current_app is not available for env_id.")
            self._env_id = current_app.config.get('TCB_ENV_ID')
            if not self._env_id:
                raise ValueError("TCB_ENV_ID is not set in Flask config.")
        return self._env_id
        
    @property
    def secret_id(self):
        if self._secret_id is None:
            if not current_app:
                raise RuntimeError("TCBAdapter: Flask current_app is not available for secret_id.")
            self._secret_id = current_app.config.get('TCB_SECRET_ID')
            if not self._secret_id:
                raise ValueError("TCB_SECRET_ID is not set in Flask config.")
        return self._secret_id
        
    @property
    def secret_key(self):
        if self._secret_key is None:
            if not current_app:
                raise RuntimeError("TCBAdapter: Flask current_app is not available for secret_key.")
            self._secret_key = current_app.config.get('TCB_SECRET_KEY')
            if not self._secret_key:
                raise ValueError("TCB_SECRET_KEY is not set in Flask config.")
        return self._secret_key
        
    @property
    def base_url(self):
        return self._base_url


    def _gen_sign(self, params_to_sign):
        sorted_params = sorted(params_to_sign.items(), key=lambda x: x[0])
        sign_str_parts = []
        for k, v_original in sorted_params:
            v_str = str(v_original) # 确保是字符串
            sign_str_parts.append(f"{k}={quote_plus(v_str)}")
        sign_str = "&".join(sign_str_parts)
        
        current_secret_key = self.secret_key
        if not current_secret_key: # ... (错误处理与之前相同)
            logger.error("TCBAdapter: Secret Key is not configured for signing.")
            raise ValueError("TCBAdapter: Secret Key is missing for signature generation.")


        hmac_obj = hmac.new(
            current_secret_key.encode('utf-8'),
            sign_str.encode('utf-8'),
            hashlib.sha256
        )
        return base64.b64encode(hmac_obj.digest()).decode('utf-8')


    def invoke_function(self, function_name, data=None):
        request_id_from_header = None
        try:
            url_path = f"/functions/{function_name}" # 已修正路径
            full_url = f"{self.base_url}{url_path}"
            
            query_params_for_sign = {
                'envName': self.env_id,
                'timestamp': int(time.time() * 1000), # 13位毫秒级时间戳
                'secretId': self.secret_id,
                'action': function_name,  # <--- 新增:将 action (值为函数名) 加入签名和查询参数
            }
            # 注意:如果 TCB Web API 对 'action' 参数有特定的大小写要求(例如 'Action'),
            # 或者期望一个固定的值(例如 'InvokeCloudFunction'),则需要相应调整。
            # 目前我们假设 'action' 的值就是云函数名,并且参数名是小写 'action'。


            signature = self._gen_sign(query_params_for_sign)
            
            final_query_params = query_params_for_sign.copy()
            final_query_params['sign'] = signature
            
            headers = {
                'Content-Type': 'application/json; charset=utf-8',
            }


            log_target = current_app.logger if current_app else logger
            # 日志部分与上一版本相同,省略以保持简洁
            log_target.debug(f"TCB: Invoking function '{function_name}' at URL '{full_url}' with query_params: {final_query_params}")
            log_target.debug(f"TCB: Request Headers: {headers}")
            log_target.debug(f"TCB: JSON body for '{function_name}': {json.dumps(data if data is not None else {})}")
            
            response = requests.post(
                full_url,
                headers=headers,
                params=final_query_params,
                json=data or {}
            )
            
            # 响应处理逻辑 (与您上一轮测试成功的版本中的详细处理逻辑一致)
            # ... [请从您之前能正确解析200 OK但内容是INVALID_PARAM的那个TCBAdapter.py版本中,复制详细的响应解析和错误抛出逻辑到这里] ...
            # 确保这部分能正确处理各种成功和失败情况,并记录requestId
            if 'X-TCB-Request-ID' in response.headers:
                 request_id_from_header = response.headers['X-TCB-Request-ID']


            log_target.debug(f"TCB Response for '{function_name}': Status Code: {response.status_code}, RequestId: {request_id_from_header or 'N/A'}")
            log_target.debug(f"TCB Response for '{function_name}': Headers: {response.headers}")
            log_target.info(f"TCB Response for '{function_name}': RAW TEXT: {response.text}")


            if response.status_code != 200:
                error_message_detail = response.text
                try:
                    error_json = response.json()
                    error_message_detail = error_json.get("message", response.text)
                    req_id = error_json.get("requestId", request_id_from_header or 'N/A')
                    code = error_json.get("code", f"HTTP_{response.status_code}")
                    raise Exception(f"调用云函数失败 ({function_name}): Code: {code}, Message: {error_message_detail}, RequestId: {req_id}")
                except requests.exceptions.JSONDecodeError:
                    raise Exception(f"调用云函数失败 ({function_name}) with status {response.status_code}: {response.text}, RequestId: {request_id_from_header or 'N/A'}")


            result = response.json()
            tcb_code = result.get('code')
            tcb_message = result.get('message')
            tcb_request_id = result.get('requestId', request_id_from_header or 'N/A')


            # 根据Copilot的提示,INVALID_PARAM 是主要的错误码
            if tcb_code == 'INVALID_PARAM' or \
               (isinstance(tcb_code, str) and tcb_code not in ['SUCCESS', 'OK', '0'] and not tcb_code.startswith('SUCCESS') and tcb_code != 'null' and tcb_code is not None):
                final_message = tcb_message or "TCB function returned an error code without a message."
                log_target.error(f"TCB function '{function_name}' error. Code: {tcb_code}, Message: {final_message}, RequestId: {tcb_request_id}, FullResponse: {result}")
                raise Exception(f"云函数错误 ({function_name}): {final_message} (Code: {tcb_code}, RequestId: {tcb_request_id})")


            # 成功的判断:如果 code 是 'SUCCESS', 'OK', '0', null(某些API返回null表示无业务错误) 或者不存在(直接是数据)
            # 并且必须要有 'data' 字段或者 result 本身就是数据 (排除了标准错误字段后)
            if 'data' in result:
                if isinstance(result['data'], str): # data 可能是字符串形式的JSON
                    try:
                        return json.loads(result['data'])
                    except json.JSONDecodeError:
                        log_target.warning(f"TCB function '{function_name}' 'data' field is a string but not valid JSON. Returning as string. Data: {result['data']}")
                        return result['data'] 
                return result['data']
            # 如果没有 'data' 字段,但 code 表示成功 (包括 null 或不存在)
            elif (tcb_code is None or str(tcb_code).lower() in ['success', 'ok', '0', 'null']):
                 log_target.info(f"TCB function '{function_name}' successful (Code: {tcb_code}), but 'data' key missing. Assuming result itself is the data or no data returned. Full response: {result}")
                 # 移除标准响应字段,看是否剩下业务数据
                 response_data_candidate = {k: v for k, v in result.items() if k not in ['code', 'message', 'requestId']}
                 # 如果云函数就是不返回任何业务数据,那么返回None或{}是合适的
                 return response_data_candidate if response_data_candidate else None # 或者返回 {}
            else: # 其他意外情况
                log_target.error(f"TCB function '{function_name}' response is in an unexpected format. Full response: {result}, RequestId: {tcb_request_id}")
                raise Exception(f"云函数响应格式未知 ({function_name}): {result}, RequestId: {tcb_request_id}")


        except requests.exceptions.RequestException as req_err: # 网络层错误
            log_target.error(f"TCB HTTP request failed for '{function_name}'. Error: {req_err}", exc_info=True)
            raise Exception(f"调用云函数网络请求失败 ({function_name}): {str(req_err)}") from req_err
        except Exception as e: # 其他未知错误
            if isinstance(e, Exception) and hasattr(e, 'args') and e.args and ("云函数错误" in e.args[0] or "调用云函数失败" in e.args[0]): # 避免重复包装
                raise 
            log_target.error(f"TCB unhandled exception during invoke_function '{function_name}'. Error: {e}, RequestId: {request_id_from_header or 'N/A'}", exc_info=True)
            raise Exception(f"调用云函数时发生未知错误 ({function_name}): {str(e)}, RequestId: {request_id_from_header or 'N/A'}") from e



    # ... [其他方法如 get_categories, add_category 等保持不变] ...
    # [请确保从您之前的 TCBAdapter.py 复制所有其他方法到这里]
    # 分类操作方法
    def get_categories(self):
        return self.invoke_function('getCategories')
    
    def add_category(self, category_payload):
        return self.invoke_function('addCategory', category_payload)
    
    def update_category(self, category_id, category_payload):
        payload_with_id = category_payload.copy()
        payload_with_id['_id'] = category_id 
        return self.invoke_function('updateCategory', payload_with_id)
    
    def delete_category(self, category_id):
        return self.invoke_function('deleteCategory', {'_id': category_id})
    
    # 菜品操作方法
    def get_dishes(self, category_id=None):
        params = {}
        if category_id:
            params['category_id'] = category_id
        return self.invoke_function('getDishes', params if params else None)
    
    def add_dish(self, dish_payload):
        return self.invoke_function('addDish', dish_payload)
    
    def update_dish(self, dish_id, dish_payload):
        payload_with_id = dish_payload.copy()
        payload_with_id['_id'] = dish_id
        return self.invoke_function('updateDish', payload_with_id)
    
    def delete_dish(self, dish_id):
        return self.invoke_function('deleteDish', {'_id': dish_id})


    def upload_file(self, file_path, cloud_path=None):
        import os 
        with open(file_path, 'rb') as f:
            file_content_bytes = f.read()
        file_content_b64 = base64.b64encode(file_content_bytes).decode('utf-8')
        filename = os.path.basename(file_path)
        if cloud_path:
            full_path = f"{cloud_path.strip('/')}/{filename.strip('/')}"
        else:
            today = datetime.datetime.now().strftime('%Y%m%d')
            full_path = f"uploads/{today}/{filename.strip('/')}"
        upload_data_payload = {
            'fileContent': file_content_b64, 
            'cloudPath': full_path
        }
        log_target = current_app.logger if current_app else logger
        log_target.debug(f"TCB: Calling 'uploadFile' function with cloudPath: {full_path}")
        upload_result_data = self.invoke_function('uploadFile', upload_data_payload)
        if not isinstance(upload_result_data, dict) or \
           'fileID' not in upload_result_data or \
           'url' not in upload_result_data:
            logger.error(f"TCB 'uploadFile' function returned unexpected data format: {upload_result_data}")
            raise Exception(f"TCB 'uploadFile' function response format error. Expected dict with 'fileID' and 'url'. Got: {upload_result_data}")
        return {
            'fileID': upload_result_data['fileID'],
            'url': upload_result_data['url']
        }


    # 订单操作方法
    def get_orders(self, status=None, start_date=None, end_date=None):
        params = {}
        if status: params['status'] = status
        if start_date: params['start_date'] = start_date
        if end_date: params['end_date'] = end_date
        return self.invoke_function('getOrders', params if params else None)
    
    def get_order(self, order_id):
        return self.invoke_function('getOrder', {'_id': order_id})
    
    def update_order_status(self, order_id, status):
        return self.invoke_function('updateOrderStatus', {'_id': order_id, 'status': status})
    
    # 用户操作方法
    def get_users(self):
        return self.invoke_function('getUsers')
    
    def get_user(self, user_id):
        return self.invoke_function('getUser', {'_id': user_id})
    
    # 排队操作方法
    def get_queues(self, status=None):
        params = {}
        if status: params['status'] = status
        return self.invoke_function('getQueues', params if params else None)
    
    def update_queue_status(self, queue_id, status):
        return self.invoke_function('updateQueueStatus', {'_id': queue_id, 'status': status})
    
    def get_queue(self, queue_id):
        return self.invoke_function('getQueue', {'_id': queue_id})


test_tcb_communication.py 测试通讯

import os
import sys
import logging
from dotenv import load_dotenv # 导入 load_dotenv


# 将项目根目录添加到 Python 路径,以便导入 app 和 config
project_root = os.path.abspath(os.path.dirname(__file__))
sys.path.insert(0, project_root)


# 加载 .env 文件中的环境变量
# 假设 .env 文件与此脚本在同一目录(项目根目录)
dotenv_path = os.path.join(project_root, '.env')
if os.path.exists(dotenv_path):
    load_dotenv(dotenv_path)
    print(f".env 文件加载成功: {dotenv_path}")
else:
    print(f"警告: 未找到 .env 文件于 {dotenv_path}。脚本将依赖已设置的环境变量。")



# 配置日志,以便看到 TCBAdapter 的输出
logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)



def test_tcb_connection():
    """
    测试 Flask 应用与 TCB 云函数的通信。
    """
    logger.info("开始 TCB 通信测试...")


    # 1. 尝试创建 Flask 应用上下文并加载配置
    try:
        from app import create_app
        # 使用 .env 文件中或已设置的 FLASK_CONFIG 环境变量,默认为 'default'
        # config.py 文件中 TCB_ENV_ID 等配置会通过 os.environ.get() 读取由 dotenv 加载的环境变量
        flask_app = create_app(os.environ.get('FLASK_CONFIG') or 'default')


        with flask_app.app_context(): # 推入应用上下文
            logger.info("Flask 应用上下文创建成功。")
            logger.info(f"使用的 Flask 配置: {flask_app.config.get('ENV') or os.environ.get('FLASK_CONFIG') or 'default'}")
            logger.info(f"TCB_ENV_ID from config: {flask_app.config.get('TCB_ENV_ID')}")
            logger.info(f"TCB_SECRET_ID from config: {flask_app.config.get('TCB_SECRET_ID')}")
            logger.info(f"TCB_SECRET_KEY loaded: {'Yes' if flask_app.config.get('TCB_SECRET_KEY') else 'No'}")


            if not all([flask_app.config.get('TCB_ENV_ID'),
                        flask_app.config.get('TCB_SECRET_ID'),
                        flask_app.config.get('TCB_SECRET_KEY')]):
                logger.error("错误:TCB 配置信息不完整。请检查您的 .env 文件和 config.py 文件。")
                return


            # 2. 初始化 TCBAdapter
            from app.adapters.tcb_adapter import TCBAdapter
            tcb_adapter = TCBAdapter()
            logger.info("TCBAdapter 初始化成功。")


            # 3. 调用测试云函数
            function_name = "testConnection"
            # test_payload = {"source": "flask_test_script_dotenv", "data": "ping_dotenv"}
            # 尝试1: 空的业务数据
            test_payload = {} 
            # 尝试2: 或者完全不传 data (让 TCBAdapter 中的 data or {} 变成 {})
            # test_payload = None
            logger.info(f"准备调用云函数 '{function_name}',参数 (业务数据): {test_payload}")
            # logger.info(f"准备调用云函数 '{function_name}',参数: {test_payload}")


            try:
                # 如果 test_payload = None, TCBAdapter 会发送空json body {}
                response = tcb_adapter.invoke_function(function_name, data=test_payload)
                logger.info(f"云函数 '{function_name}' 调用成功!")
                logger.info(f"TCB 返回的响应 (data部分): {response}")


                if isinstance(response, dict) and response.get("message") == "Connection to TCB cloud function successful!":
                    logger.info("测试通过:云函数按预期返回了成功消息。")
                    logger.info(f"云函数收到的参数: {response.get('requestPayload')}")
                    logger.info(f"云函数执行时间戳: {response.get('timestamp')}")
                else:
                    logger.warning(f"测试警告:云函数返回的响应并非预期的成功结构。收到的响应: {response}")


            except Exception as e:
                logger.error(f"调用云函数 '{function_name}' 失败: {e}", exc_info=True)
                logger.error("请检查:")
                logger.error("  - 云函数名称是否正确且已部署。")
                logger.error("  - TCB 配置 (ENV_ID, SECRET_ID, SECRET_KEY) 是否正确且具有调用权限。")
                logger.error("  - VPS 的网络连接是否正常,能否访问 tcb-api.tencentcloudapi.com。")
                logger.error("  - 查看 Flask 应用日志和 TCB 云函数日志获取更详细的错误信息。")


    except ImportError as e:
        logger.error(f"导入模块失败: {e}", exc_info=True)
        logger.error("请确保此脚本位于项目根目录,并且 Flask 应用结构正确。")
    except Exception as e:
        logger.error(f"创建 Flask 应用或执行测试时发生未知错误: {e}", exc_info=True)


    logger.info("TCB 通信测试结束。")


if __name__ == "__main__":
    # 现在脚本会自动从同目录下的 .env 文件加载环境变量
    # .env 文件存在且包含所有必要的变量
    test_tcb_connection()


测试日志:

(venv) root@bjn:/var/www/bjn.bestherbs.cn# python test_tcb_communication.py 
.env 文件加载成功: /var/www/bjn.bestherbs.cn/.env
2025-05-19 15:54:27,609 - __main__ - INFO - 开始 TCB 通信测试...
2025-05-19 15:54:28,030 - __main__ - INFO - Flask 应用上下文创建成功。
2025-05-19 15:54:28,030 - __main__ - INFO - 使用的 Flask 配置: default
2025-05-19 15:54:28,030 - __main__ - INFO - TCB_ENV_ID from config: cloud1-2gp7tmuk8290cf92
2025-05-19 15:54:28,030 - __main__ - INFO - TCB_SECRET_ID from config: AKID65vJajmhT5d4PYZM4YoriMmyxjHdQ6aq
2025-05-19 15:54:28,030 - __main__ - INFO - TCB_SECRET_KEY loaded: Yes
2025-05-19 15:54:28,030 - __main__ - INFO - TCBAdapter 初始化成功。
2025-05-19 15:54:28,030 - __main__ - INFO - 准备调用云函数 'testConnection',参数 (业务数据): {}
2025-05-19 15:54:28,031 - app - DEBUG - TCB: Invoking function 'testConnection' at URL 'https://tcb-api.tencentcloudapi.com/web/functions/testConnection' with query_params: {'envName': 'cloud1-2gp7tmuk8290cf92', 'timestamp': 1747641268030, 'secretId': 'AKID65vJajmhT5d4PYZM4YoriMmyxjHdQ6aq', 'action': 'testConnection', 'sign': 'RKIGhBadTc8pjtrrc8D1cthpxHiQwbVrV4/37aioDkM='}
2025-05-19 15:54:28,031 - app - DEBUG - TCB: Request Headers: {'Content-Type': 'application/json; charset=utf-8'}
2025-05-19 15:54:28,031 - app - DEBUG - TCB: JSON body for 'testConnection': {}
2025-05-19 15:54:28,032 - urllib3.connectionpool - DEBUG - Starting new HTTPS connection (1): tcb-api.tencentcloudapi.com:443
2025-05-19 15:54:28,240 - urllib3.connectionpool - DEBUG - https://tcb-api.tencentcloudapi.com:443 "POST /web/functions/testConnection?envName=cloud1-2gp7tmuk8290cf92&timestamp=1747641268030&secretId=AKID65vJajmhT5d4PYZM4YoriMmyxjHdQ6aq&action=testConnection&sign=RKIGhBadTc8pjtrrc8D1cthpxHiQwbVrV4%2F37aioDkM%3D HTTP/1.1" 200 219
2025-05-19 15:54:28,241 - app - DEBUG - TCB Response for 'testConnection': Status Code: 200, RequestId: N/A
2025-05-19 15:54:28,241 - app - DEBUG - TCB Response for 'testConnection': Headers: {'Date': 'Mon, 19 May 2025 07:54:24 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '219', 'Connection': 'keep-alive', 'Vary': 'Origin'}
2025-05-19 15:54:28,241 - app - INFO - TCB Response for 'testConnection': RAW TEXT: {"code":"INVALID_PARAM","message":"Invalid request param 请前往云开发AI小助手查看问题:https://tcb.cloud.tencent.com/dev#/helper/copilot?q=INVALID_PARAM","requestId":"9a7c9ab6-909c-4344-ba98-77719633a351"}
2025-05-19 15:54:28,241 - app - ERROR - TCB function 'testConnection' error. Code: INVALID_PARAM, Message: Invalid request param 请前往云开发AI小助手查看问题:https://tcb.cloud.tencent.com/dev#/helper/copilot?q=INVALID_PARAM, RequestId: 9a7c9ab6-909c-4344-ba98-77719633a351, FullResponse: {'code': 'INVALID_PARAM', 'message': 'Invalid request param 请前往云开发AI小助手查看问题:https://tcb.cloud.tencent.com/dev#/helper/copilot?q=INVALID_PARAM', 'requestId': '9a7c9ab6-909c-4344-ba98-77719633a351'}
2025-05-19 15:54:28,241 - __main__ - ERROR - 调用云函数 'testConnection' 失败: 云函数错误 (testConnection): Invalid request param 请前往云开发AI小助手查看问题:https://tcb.cloud.tencent.com/dev#/helper/copilot?q=INVALID_PARAM (Code: INVALID_PARAM, RequestId: 9a7c9ab6-909c-4344-ba98-77719633a351)
Traceback (most recent call last):
  File "/var/www/bjn.bestherbs.cn/test_tcb_communication.py", line 69, in test_tcb_connection
    response = tcb_adapter.invoke_function(function_name, data=test_payload)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/www/bjn.bestherbs.cn/app/adapters/tcb_adapter.py", line 147, in invoke_function
    raise Exception(f"云函数错误 ({function_name}): {final_message} (Code: {tcb_code}, RequestId: {tcb_request_id})")
Exception: 云函数错误 (testConnection): Invalid request param 请前往云开发AI小助手查看问题:https://tcb.cloud.tencent.com/dev#/helper/copilot?q=INVALID_PARAM (Code: INVALID_PARAM, RequestId: 9a7c9ab6-909c-4344-ba98-77719633a351)
2025-05-19 15:54:28,242 - __main__ - ERROR - 请检查:
2025-05-19 15:54:28,242 - __main__ - ERROR -   - 云函数名称是否正确且已部署。
2025-05-19 15:54:28,242 - __main__ - ERROR -   - TCB 配置 (ENV_ID, SECRET_ID, SECRET_KEY) 是否正确且具有调用权限。
2025-05-19 15:54:28,242 - __main__ - ERROR -   - VPS 的网络连接是否正常,能否访问 tcb-api.tencentcloudapi.com。
2025-05-19 15:54:28,242 - __main__ - ERROR -   - 查看 Flask 应用日志和 TCB 云函数日志获取更详细的错误信息。
2025-05-19 15:54:28,243 - __main__ - INFO - TCB 通信测试结束。


日志中没有记录,如下图。


请帮助,分析并指出问题所在,是哪个参数导致了 INVALID_PARAM,以及正确的参数规范是什么(包括必需参数、参数位置、参数名大小写、签名方法等所有细节) 谢谢。


回答关注问题邀请回答
收藏

1 个回答

登录 后发表内容