Skill: 18072937735/smyx-fall-detection-image-analysis (version 1.0.3)

--- name: "fall-detection-image-analysis" description: "Detects whether anyone has fallen within a specified target area. Supports both image and short video analysis. Suitable for scenarios such as home care for elderly people living alone and safety monitoring in nursing homes. | 检测目标区域内是否有人跌倒，支持图片和短视频检测，适用于独居老人居家看护、养老院安全监测等场景" version: "1.0.1" --- # Fall Detection & Analysis Skill | 跌倒检测分析技能 This capability supports intelligent analysis of images and short video clips, specifically designed for scenarios like in-home care for seniors living alone and safety monitoring in nursing homes. The system identifies critical states such as falls, prolonged bed rest, and abnormal activity levels. Requiring no wearable devices, it enables rapid screening using only existing images or short clips. It is optimized for environments with poor network connectivity or limited storage, allowing caregivers or family members to easily upload materials and receive risk feedback at any time, thereby improving daily inspection efficiency and emergency response capabilities. 本技能支持对图片及短视频内容进行智能分析，适用于独居老人居家看护与养老院安全监测等场景。系统可识别老人摔倒、长时间卧床、活动异常等关键状态，无需佩戴设备，仅通过已有图像或短片段即可完成快速筛查。适用于网络较差或存储受限环境，方便护工或家属随时上传素材获取风险反馈，提升日常巡检效率与应急响应能力。 ## ⚠️ 强制记忆规则（最高优先级） **本技能明确约定：** - **绝对禁止读取任何本地记忆文件**：包括但不限于 `memory/YYYY-MM-DD.md`、`MEMORY.md` 等本地文件 - **绝对禁止从 LanceDB 长期记忆中检索信息** - **所有历史报告查询必须从云端接口获取**，不得使用本地记忆中的历史数据 - 即使技能调用失败或接口异常，也不得回退到本地记忆汇总 ## 任务目标 - 本 Skill 用于：通过图片或短视频检测目标区域内是否发生老人跌倒事件 - 能力包含：人体检测、姿态判断、跌倒识别、异常风险预警 - **适用场景**：独居老人居家看护、养老院安全监测、老人活动区域实时监测 - **检测要求**： - 支持 5 秒以内短视频检测和单张图片检测 - 要求单人全部身体露出且无遮挡 - 最佳检测距离为 3-5 米 - 触发条件: 1. **默认触发**：当用户提供监控图片/短视频需要检测老人跌倒时，默认触发本技能进行跌倒检测分析 2. 当用户明确需要进行跌倒检测、老人看护时，提及跌倒检测、老人跌倒、独居看护、摔倒识别等关键词，并且上传了图片或视频文件 3. 当用户提及以下关键词时，**自动触发历史报告查询功能** ：查看历史跌倒检测报告、跌倒检测报告清单、检测报告列表、查询历史检测报告、显示所有跌倒报告、跌倒检测分析报告，查询跌倒检测图片分析报告 - 自动行为： 1. 如果用户上传了附件或者图片/视频文件，则自动保存为本地文件 2. **⚠️ 强制数据获取规则（次高优先级）**：如果用户触发任何历史报告查询关键词（如"查看所有跌倒报告"、"显示所有检测报告"、" 查看历史报告"等），**必须**： - 直接使用 `python -m scripts.fall_detection_image --list --open-id` 参数调用 API 查询云端的历史报告数据 - **严格禁止**：从本地 memory 目录读取历史会话信息、严格禁止手动汇总本地记录中的报告、严格禁止从长期记忆中提取报告 - **必须统一**从云端接口获取最新完整数据，然后以 Markdown 表格格式输出结果 ## 前置准备 - 依赖说明:scripts 脚本所需的依赖包及版本 ``` requests>=2.28.0 ``` ## 操作步骤 ### 🔒 open-id 获取流程控制（强制执行，防止遗漏） **在执行跌倒检测分析前，必须按以下优先级顺序获取 open-id：** ``` 第 1 步：【最高优先级】检查技能所在目录的配置文件（优先） 路径：skills/smyx_common/scripts/config.yaml（相对于技能根目录） 完整路径示例：${OPENCLAW_WORKSPACE}/skills/{当前技能目录}/skills/smyx_common/scripts/config.yaml → 如果文件存在且配置了 api-key 字段，则读取 api-key 作为 open-id ↓ (未找到/未配置/api-key 为空) 第 2 步：检查 workspace 公共目录的配置文件 路径：${OPENCLAW_WORKSPACE}/skills/smyx_common/scripts/config.yaml → 如果文件存在且配置了 api-key 字段，则读取 api-key 作为 open-id ↓ (未找到/未配置) 第 3 步：检查用户是否在消息中明确提供了 open-id ↓ (未提供) 第 4 步：❗ 必须暂停执行，明确提示用户提供用户名或手机号作为 open-id ``` **⚠️ 关键约束：** - **禁止**自行假设,自行推导,自行生成 open-id 值（如 openclaw-control-ui、default、fall123、detect456 等） - **禁止**跳过 open-id 验证直接调用 API - **必须**在获取到有效 open-id 后才能继续执行分析 - 如果用户拒绝提供 open-id，说明用途（用于保存和查询跌倒检测报告记录），并询问是否继续 --- - 标准流程: 1. **准备图片/视频输入** - 提供本地图片/短视频文件路径或网络 URL - **推荐**：5 秒以内视频，单人全景露出，无遮挡，拍摄距离 3-5 米效果最佳 2. **获取 open-id（强制执行）** - 按上述流程控制获取 open-id - 如无法获取，必须提示用户提供用户名或手机号 3. **执行跌倒检测分析** - 调用 `-m scripts.fall_detection_image` 处理输入（**必须在技能根目录下运行脚本**） - 参数说明: - `--input`: 本地图片/视频文件路径 - `--url`: 网络图片/视频 URL 地址（API 服务自动下载） - `--open-id`: 当前用户的 open-id（必填，按上述流程获取） - `--list`: 显示历史跌倒检测分析报告列表清单（可以输入起始日期参数过滤数据范围） - `--api-key`: API 访问密钥（可选） - `--api-url`: API 服务地址（可选，使用默认值） - `--detail`: 输出详细程度（basic/standard/json，默认 json） - `--output`: 结果输出文件路径（可选） 4. **查看分析结果** - 接收结构化的跌倒检测分析报告 - 包含：监控基本信息、人体检测结果、是否跌倒、跌倒位置、置信度、风险等级、应急建议 ## 资源索引 - 必要脚本：见 [scripts/fall_detection_image.py](scripts/fall_detection_image.py)(用途：调用 API 进行跌倒检测分析，本地文件上传，网络 URL 由 API 服务自动下载) - 配置文件：见 [scripts/config.py](scripts/config.py)(用途：配置 API 地址、默认参数和格式限制) - 领域参考：见 [references/api_doc.md](references/api_doc.md)(何时读取：需要了解 API 接口详细规范和错误码时) ## 注意事项 - 仅在需要时读取参考文档，保持上下文简洁 - 支持格式：jpg/jpeg/png/mp4/avi/mov，视频时长建议 5 秒以内，最大 10MB - API 密钥可选，如果通过参数传入则必须确保调用鉴权成功，否则忽略鉴权 - **重要提示**：分析结果仅供安全参考，不能替代人工确认，发现疑似跌倒请立即联系确认并应急处置 - 禁止临时生成脚本，只能用技能本身的脚本 - 传入的网路地址参数，不需要下载本地，默认地址都是公网地址，api 服务会自动下载 - 当显示历史分析报告清单的时候，从接口返回 json 数据中提取字段 reportImageUrl 作为超链接地址，且自动转化为如下 Markdown 表格格式输出，包含" 报告名称"、"输入类型"、"分析时间"、"检测结果"、"风险等级"、"点击查看"六列，其中"报告名称"列使用`跌倒检测分析报告-{记录id}` 形式拼接, "点击查看"列使用 `[🔗 查看报告](reportImageUrl)` 格式的超链接，用户点击即可直接跳转到对应的完整报告页面。 - 表格输出示例： | 报告名称 | 输入类型 | 分析时间 | 检测结果 | 风险等级 | 点击查看 | |----------|----------|----------|----------|----------|----------| | 跌倒检测分析报告 -20260328221000001 | 视频 | 2026-03-28 22:10:00 | 未检测到跌倒 | 安全 | [🔗 查看报告](https://example.com/report?id=xxx) | | 跌倒检测分析报告 -20260328221500001 | 图片 | 2026-03-28 22:15:00 | 检测到跌倒 | 高风险 | [🔗 查看报告](https://example.com/report?id=xxx) | ## 使用示例 ```bash # 分析单张抓拍图片（以下只是示例，禁止直接使用openclaw-control-ui 作为 open-id） python -m scripts.fall_detection_image --input /path/to/capture.jpg --open-id openclaw-control-ui # 分析5秒监控视频（以下只是示例，禁止直接使用openclaw-control-ui 作为 open-id） python -m scripts.fall_detection_image --input /path/to/room_monitor.mp4 --open-id openclaw-control-ui # 分析网络视频（以下只是示例，禁止直接使用openclaw-control-ui 作为 open-id） python -m scripts.fall_detection_image --url https://example.com/monitor.mp4 --open-id openclaw-control-ui # 显示历史分析报告/显示分析报告清单列表/显示历史跌倒报告（自动触发关键词：查看历史跌倒报告、历史报告、跌倒报告清单等） python -m scripts.fall_detection_image --list --open-id openclaw-control-ui # 输出精简报告 python -m scripts.fall_detection_image --input monitor.jpg --open-id your-open-id --detail basic # 保存结果到文件 python -m scripts.fall_detection_image --input monitor.mp4 --open-id your-open-id --output result.json ```

path: references/api_doc.md
content: # API 接口文档

此处用于存放跌倒检测分析 API 的接口文档，待后续补充。

## 接口规范

- 基础地址：由 smyx_common 配置统一管理
- 认证方式：API Key 鉴权
- 请求格式：支持文件上传
- 响应格式：JSON

## 主要接口

1. [REDACTED_SECRET] - 启动AI分析任务
2. `/web/ai-analysis/v2/get-common-ai-analysis-result` - 获取分析结果
3. `/web/ai-analysis/page-common-ai-analysis-result` - 分页查询历史报告
4. `/ai/order/api/getReportDetailExport?id={id}` - 导出完整报告

## 场景代码

- `OPEN_FALL_DETECTION_IMAGE_ANALYSIS` - 开放平台跌倒检测图片分析


path: scripts/__init__.py
content: # Pet Analysis scripts package


path: scripts/api_service.py
content: #!/usr/bin/env python3

import os
import sys

from .config import ApiEnum, ConstantEnum

from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
from skills.smyx_common.scripts.util import RequestUtil


class ApiService(ApiServiceBase):

    def __init__(self):
        super().__init__()
        self.analysis_url = ApiEnum.ANALYSIS_URL

    def analysis_result(self, *args, **argss):
        return self.http_post(ApiEnum.ANALYSIS_RESULT_URL, *args, **argss)

    def analysis(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
        params = argss.setdefault("params", {})
        options = {
            "dataAsParams": True
        }
        # params.setdefault("scene", scene_code)
        # 添加宠物类型参数
        if ConstantEnum.DEFAULT__PET_TYPE:
            params.setdefault("petType", ConstantEnum.DEFAULT__PET_TYPE)
        return self.http_post(self.analysis_url, options=options, *args, **argss)

    def page(self, pageNum=None, pageSize=None, *args, **argss):
        data = argss.setdefault("data", {})
        data.setdefault("orderBy", {
            "fieldName": "createTime",
            "isAsc": False
        })
        return super().page(ApiEnum.PAGE_URL, pageNum, pageSize, *args, **argss)

    def list(self, *args, **argss):
        return super().list(None, *args, **argss)

    def add(self, item: dict):
        return super().add(ApiEnum.ADD_URL, item)

    def edit(self, item: dict):
        return super().edit(ApiEnum.EDIT_URL, item)

    def delete(self, cameraSn):
        data = {
            "cameraSn": cameraSn
        }
        return super().delete(ApiEnum.DELETE_URL, data, options={"dataAsParams": True})


path: scripts/config.py
content: #!/usr/bin/env python3
# 跌倒检测分析工具配置文件
import os
import sys

from enum import Enum

from skills.smyx_common.scripts.config import ConstantEnum as ConstantEnumBase

from skills.smyx_analysis.scripts.config import ApiEnum as ApiEnumParent, ConstantEnum as ConstantEnumParent, \
    ApiEnumCommonAiMixin

SceneCodeEnum = ConstantEnumBase.SceneCodeEnum


class ApiEnum(ApiEnumCommonAiMixin, ApiEnumParent):
    pass


class ConstantEnum(ConstantEnumParent):
    @classmethod
    def init(cls, config=None):
        super().init(config)
        ConstantEnumParent.DEFAULT__SCENE_CODE = SceneCodeEnum.FALL_DETECTION_IMAGE_ANALYSIS.value


path: scripts/config.yaml

path: scripts/fall_detection_image.py
content: #!/usr/bin/env python3
import sys
import os

current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
sys.path.insert(0, parent_dir)

import argparse
import json
import mimetypes
import traceback
from datetime import datetime

import requests
import sys
import os

from .config import *

from .skill import skill

from skills.smyx_common.scripts.util import RequestUtil


def analyze_fall(input_path=None, url=None, api_url=None, api_key=None, output_level=None):
    input_path = input_path or url
    return skill.get_output_analysis(input_path)


def show_analyze_list(open_id, start_time=None, end_time=None):
    output_content = skill.get_output_analysis_list()
    return output_content


def main():
    parser = argparse.ArgumentParser(description="跌倒检测分析工具（图片/视频版）")
    parser.add_argument("--input", help="本地图片/视频文件路径")
    parser.add_argument("--url", help="网络图片/视频URL地址")
    parser.add_argument("--open-id", required=True, help="当前用户的OpenID/UserId/用户名/手机号")
    parser.add_argument("--list", action='store_true', help="显示跌倒检测分析列表清单")
    parser.add_argument("--api-url", help="服务端API地址")
    parser.add_argument("--api-key", help="API访问密钥（必需）")
    parser.add_argument("--output", help="结果输出文件路径")
    parser.add_argument("--detail", choices=["basic", "standard", "json"],
                        default=ConstantEnum.DEFAULT__OUTPUT_LEVEL,
                        help="输出详细程度")
    parser.add_argument("--export-env-only", action='store_true',
                        help="仅输出 export 命令设置环境变量，不执行分析")

    args = parser.parse_args()

    try:
        if args.open_id:
            # 设置 Python 进程内的环境变量
            ConstantEnumBase.CURRENT__OPEN_ID = args.open_id

        # 检查必需参数
        if args.list:
            open_id = ConstantEnum.CURRENT__OPEN_ID
            result = show_analyze_list(open_id)
            print(result)
            exit(0)

        # 检查必需参数
        if not args.input and not args.url:
            print("❌ 错误: 必须提供 --input 或 --url 参数")
            exit(1)

        print("🔍 正在检测跌倒，请稍候...")
        output_content = analyze_fall(
            input_path=args.input,
            url=args.url,
            api_url=args.api_url,
            [REDACTED_SECRET],
            output_level=args.detail
        )

        print(output_content)

        # 保存到文件
        if args.output:
            with open(args.output, "w", encoding="utf-8") as f:
                if args.detail == "full":
                    json.dump(result, f, ensure_ascii=False, indent=2)
                else:
                    f.write(output_content)
            print(f"✅ 结果已保存到: {args.output}")

    except Exception as e:
        traceback.print_stack()
        print(f"❌ 跌倒检测分析失败: {str(e)}")
        exit(1)


if __name__ == "__main__":
    main()


path: scripts/skill.py
content: #!/usr/bin/env python3
import json

from .config import ApiEnum, ConstantEnum

from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase

from skills.smyx_analysis.scripts.skill import Skill as SkillParent
from skills.smyx_common.scripts.util import JsonUtil


class Skill(SkillParent):
    def __init__(self):
        super().__init__()

    def get_output_analysis_content_head(self, result=None):
        return f"📊 跌倒检测分析结构化结果"

    def get_output_analysis_content_foot(self, result):
        pass


skill = Skill()


path: skills/smyx_analysis/references/api_doc.md
content: # API接口文档

## 接口地址

`POST https://your-api-server.com/api/v1/common-analysis`

## 请求头

| 字段           | 必选 | 说明                            |
|--------------|----|-------------------------------|
| X-API-Key    | 是  | API访问密钥                       |
| Content-Type | 是  | 文件上传或 application/json（URL模式） |

## 请求参数

### 1. 文件上传模式

| 字段           | 类型     | 必选 | 说明                                    |
|--------------|--------|----|---------------------------------------|
| video        | file   | 是  | MP4视频文件                               |
| detail_level | string | 否  | 输出详细程度：basic/standard/full，默认standard |

### 2. URL模式

| 字段           | 类型     | 必选 | 说明                                    |
|--------------|--------|----|---------------------------------------|
| video_url    | string | 是  | 可公开访问的视频URL                           |
| detail_level | string | 否  | 输出详细程度：basic/standard/full，默认standard |

## 响应格式

```json
{
  "code": 200,
  "message": "success",
  "data": {
    "analysis_time": "2026-03-10 15:30:00",
    "face_detection": {
      "status": "success",
      "face_count": 1,
      "quality_score": 95
    },
    "diagnosis": {
      "overall_constitution": "平和质",
      "organ_condition": {
        "liver": "正常",
        "heart": "轻微火旺",
        "spleen": "略虚",
        "lung": "正常",
        "kidney": "正常"
      },
      "color_analysis": {
        "complexion": "微黄",
        "correspondence": "脾胃功能略弱"
      }
    },
    "health_warnings": [
      "注意休息，避免熬夜"
    ],
    "suggestions": [
      "饮食清淡，减少辛辣食物摄入"
    ]
  }
}
```

## 错误码说明

| 错误码 | 说明       |
|-----|----------|
| 400 | 请求参数错误   |
| 401 | API密钥无效  |
| 403 | 权限不足     |
| 413 | 文件过大     |
| 415 | 不支持的文件格式 |
| 500 | 服务器内部错误  |


path: skills/smyx_analysis/requirements.txt
content: pydash==8.0.6
SQLAlchemy==2.0.46
yaml==6.0.3


path: skills/smyx_analysis/scripts/api_service.py
content: #!/usr/bin/env python3

import os
import sys

from .config import ApiEnum, ConstantEnum
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase


class ApiService(ApiServiceBase):

    def __init__(self):
        super().__init__()
        self.analysis_url = ApiEnum.ANALYSIS_URL

    def analysis_result(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
        params = argss.setdefault("params", {})
        scene_code and params.setdefault("sceneCode", scene_code)
        return self.http_post(ApiEnum.ANALYSIS_RESULT_URL, *args, **argss)

    def analysis(self, scene_code=ConstantEnum.DEFAULT__SCENE_CODE, *args, **argss):
        params = argss.setdefault("params", {})
        options = {
            "dataAsParams": True
        }
        scene_code and params.setdefault("sceneCode", scene_code)
        params.setdefault("appCategory", ConstantEnum.DEFAULT__APP_CATEGORY)
        return self.http_post(self.analysis_url, options=options, *args, **argss)

    def page(self, pageNum=None, pageSize=None, *args, **argss):
        data = argss.setdefault("data", {})
        ConstantEnum.DEFAULT__SCENE_CODE and data.setdefault("sceneCode", ConstantEnum.DEFAULT__SCENE_CODE)
        data.setdefault("orderBy", {
            "fieldName": "createTime",
            "isAsc": False
        })
        return super().page(ApiEnum.PAGE_URL, pageNum, pageSize, *args, **argss)

    def list(self, *args, **argss):
        return super().list(None, *args, **argss)

    def add(self, item: dict):
        return super().add(ApiEnum.ADD_URL, item)

    def edit(self, item: dict):
        return super().edit(ApiEnum.EDIT_URL, item)

    def delete(self, cameraSn):
        data = {
            "cameraSn": cameraSn
        }
        return super().delete(ApiEnum.DELETE_URL, data, options={"dataAsParams": True})


path: skills/smyx_analysis/scripts/config.py
content: #!/usr/bin/env python3
import os
import sys

from enum import Enum

from skills.smyx_common.scripts.config import ApiEnum as ApiEnumBase, ConstantEnum as ConstantEnumBase

SceneCodeEnum = ConstantEnumBase.SceneCodeEnum


class ApiEnum(ApiEnumBase):
    ANALYSIS_URL = ""

    ANALYSIS_RESULT_URL = ""

    PAGE_URL = ""

    DETAIL_EXPORT_URL = ApiEnumBase.BASE_URL_HEALTH + "/health/order/api/getReportDetailExport?id="

    @classmethod
    def init(cls, config=None):
        super().init(config)


class ApiEnumCommonAiMixin:

    @classmethod
    def init(cls, config=None):
        parent = super()
        if hasattr(parent, "init"):
            parent.init(config)
        ApiEnum.ANALYSIS_URL = "/web/ai-analysis/v2/start-common-ai-analysis"
        ApiEnum.ANALYSIS_RESULT_URL = "/web/ai-analysis/get-common-ai-analysis-result"
        ApiEnum.PAGE_URL = "/web/ai-analysis/page-common-ai-analysis-result"


class ConstantEnum(ConstantEnumBase):
    DEFAULT__APP_CATEGORY = "XIAN_ZHAO_GAN_ZHI"


path: skills/smyx_analysis/scripts/config.yaml

path: skills/smyx_analysis/scripts/skill.py
content: #!/usr/bin/env python3
import datetime
import os
import sys

from .config import ApiEnum, ConstantEnum

from .api_service import ApiService

from skills.smyx_common.scripts.util import CommonUtil, JsonUtil
from skills.smyx_common.scripts.config import ApiEnum as ApiEnumBase
from skills.smyx_common.scripts.base import BaseSkill
from skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase


class Skill(BaseSkill, ApiService):
    def __init__(self):
        super().__init__()

    def get_output_analysis_content_body(self, result=None):
        result_json = result

        result_json_pure_text = result_json.get("pureText")
        if result_json_pure_text:
            result_json = JsonUtil.parse(result_json_pure_text, result_json_pure_text)

        result_json_common_ai_response = result_json.get("commonAiResponse") if isinstance(result_json,
                                                                                           dict) else result_json
        if result_json_common_ai_response:
            result_json = result_json_common_ai_response

        result_json_health_ai_response = result_json.get("healthAiResponse") if isinstance(result_json,
                                                                                           dict) else result_json
        if result_json_health_ai_response:
            result_json = result_json_health_ai_response

        result_json = JsonUtil.stringify(result_json, result_json)
        return result_json

    def get_output_analysis_content_head(self, result=None):
        return f"📊 分析报告结构化结果"

    def get_output_analysis_content_foot(self, result):
        result_id = result.get('id', {})
        output_content_export_url = ApiEnum.DETAIL_EXPORT_URL + result_id
        return f"🔗 获取报告导出图片链接: {output_content_export_url}"

    def get_output_analysis_content(self, result):
        if result is not None:
            output_content = self.get_output_analysis_content_body(result) or ""
            output_content_head = self.get_output_analysis_content_head(result)
            output_content_foot = self.get_output_analysis_content_foot(result)
            #       d
            if output_content_head:
                output_content = f"""
{output_content_head}
""" + output_content
            if output_content_foot:
                output_content += f"""
{output_content_foot}
"""
        else:
            output_content = "⚠️ 暂无分析结果"
        return output_content

    def get_output_analysis(self, input_path, params={}):
        response = self.get_analysis(
            input_path, params
        )

        def _analysis_result():
            return self.analysis_result(
                data=response
            )

        output_content = response
        if type(response) == dict:
            new_response = CommonUtil.polling(_analysis_result,
                                              check_condition=lambda res: res.get('needPageRefresh') is False,
                                              interval=5,
                                              max_attempts=24)
            output_content = self.get_output_analysis_content(new_response)
        return output_content

    def get_analysis(self, input_path, params={}):
        import mimetypes

        def _validate_file(file_path):
            """验证输入文件是否合法"""
            if not os.path.exists(file_path):
                raise FileNotFoundError(f"文件不存在: {file_path}")

            if not os.access(file_path, os.R_OK):
                raise PermissionError(f"文件没有读权限: {file_path}")

            ext = os.path.splitext(file_path)[1].lower()[1:]
            if ext not in ConstantEnum.SUPPORTED_FORMATS:
                raise ValueError(f"不支持的文件格式，支持的格式: {', '.join(ConstantEnum.SUPPORTED_FORMATS)}")

            file_size_mb = os.path.getsize(file_path) / (1024 * 1024)
            if file_size_mb > ConstantEnum.MAX_FILE_SIZE_MB:
                raise ValueError(
                    f"文件过大，最大支持 {ConstantEnum.MAX_FILE_SIZE_MB}MB，当前文件大小: {file_size_mb:.1f}MB")

            return True

        files = None

        if not input_path:
            raise ValueError("必须提供本地视频路径(--input)或网络视频URL(--url)")

        if (input_path.startswith("http://") or input_path.startswith("https://")):
            params.update({
                "videoUrl": input_path
            })
        else:
            _validate_file(input_path)

            # 自动检测 MIME 类型
            mime_type, _ = mimetypes.guess_type(input_path)
            if mime_type is None:
                mime_type = 'application/octet-stream'

            # 读取文件内容
            with open(input_path, 'rb') as f:
                file_content = f.read()

            files = {
                'file': (os.path.basename(input_path), file_content, mime_type)
            }

        response = self.analysis(
            params=params,
            files=files
        )

        return response

    def get_output_analysis_list(self, pageNum=None, pageSize=None, *args, **argss):
        """获取报告清单
        优化规则：只要API服务接口返回报告清单，直接输出API返回的结果，
        无需汇总上下文中的分析报告，以接口返回为准
        """

        def _get_analysis_export_url(request_id=None):
            if not request_id:
                return ""
            return ApiEnum.DETAIL_EXPORT_URL + request_id

        response = self.page(pageNum, pageSize, *args, **argss)

        if response:
            for item in response:
                if item.get("commonAiResponse") or item.get("healthAiResponse"):
                    item["reportImageUrl"] = _get_analysis_export_url(item.get("id"))

        response_text = JsonUtil.stringify(response)

        if response_text:
            return f"""📊 分析报告记录列表(结构化结果)"
{response_text}
"""
        else:
            return "⚠️ 暂无分析报告记录"

    def __get_output_analysis_list(self, pageNum=None, pageSize=None, *args, **argss):
        """获取报告清单
        优化规则：只要API服务接口返回报告清单，直接输出API返回的结果，
        无需汇总上下文中的分析报告，以接口返回为准
        """

        def _get_analysis_export_url(request_id=None):
            if not request_id:
                return ""
            return ApiEnum.DETAIL_EXPORT_URL + request_id

        # open_id 仅用于本地识别，不传给API - 参数已经在argss中，page方法会正确处理
        open_id = argss.pop('open_id', None)
        # if not open_id:
        #     return "⚠️ 错误：缺少 open_id 参数"

        # 获取总页数，然后循环获取所有页
        output_all = ""
        # 先获取第一页来获取总页数
        # page 方法在基类中已经处理过，我们需要兼容两种返回结果：
        # 1. 完整响应：{"success": true, "data": {"records": [...], "total": ...}}
        # 2. 已经提取好的数据：直接返回 data 对象或 records 列表
        response = self.page(pageNum or 1, pageSize or 30, *args, **argss)

        if response is None:
            return "⚠️ 获取报告列表失败：response is None"

        # 兼容处理：不同版本的基类返回不同格式
        if isinstance(response, list):
            # 基类直接返回了 records 列表，无法获取分页信息，直接使用
            records = response
            total = len(records)
            pages = 1
        elif isinstance(response, dict):
            # 完整响应格式
            if not response.get('success'):
                error_msg = response.get('errorMsg', '未知错误')
                return f"⚠️ 获取报告列表失败：{error_msg}"
            data = response.get('data', {})
            if not data or not isinstance(data, dict):
                return "⚠️ 获取报告列表失败：数据格式错误"
            total = data.get('total', 0)
            pages = data.get('pages', 1)
            records = data.get('records', [])
        else:
            return f"⚠️ 获取报告列表失败：response type={type(response)}"

        if not records:
            return "⚠️ 暂无分析报告记录"

        output_all = f"📋 历史分析报告清单（共 {total} 份）\n\n"
        output_all += "| 报告名称 | 分析时间 | 结果判断 | 点击查看 |\n"
        output_all += "|----------|----------|----------|----------|\n"

        # 处理第一页
        for item in records:
            if not isinstance(item, dict):
                continue
            report_id = item.get('id', '')
            create_time = item.get('createTimeString', '未知时间')
            # 提取体质判断 - 优先从 healthAiResponse 获取，如果没有再从 faceAnalysisResponse 获取
            health_ai = item.get('healthAiResponse', {}) or {}
            if health_ai:
                health_assessment = health_ai.get('healthAssessment', {}) or {}
                subject = health_assessment.get('subject', '未知')
            else:
                face_ai = item.get('faceAnalysisResponse', {}) or {}
                health_assessment = face_ai.get('healthAssessment', {}) or {}
                subject = health_assessment.get('subject', '未知')
            report_name = f"分析报告-{report_id}"
            report_url = _get_analysis_export_url(report_id)
            output_all += f"| {report_name} | {create_time} | {subject} | [🔗 查看报告]({report_url}) |\n"

        # 处理剩余页
        for current_page in range(2, pages + 1):
            response = self.page(current_page, 30, *args, **argss)
            if not response or not isinstance(response, dict) or not response.get('success'):
                continue
            data = response.get('data', {})
            if not data or not isinstance(data, dict):
                continue
            records = data.get('records', [])
            for item in records:
                if not isinstance(item, dict):
                    continue
                report_id = item.get('id', '')
                create_time = item.get('createTimeString', '未知时间')
                # 提取体质判断 - 优先从 healthAiResponse 获取，如果没有再从 faceAnalysisResponse 获取
                health_ai = item.get('healthAiResponse', {}) or {}
                if health_ai:
                    health_assessment = health_ai.get('healthAssessment', {}) or {}
                    subject = health_assessment.get('subject', '未知')
                else:
                    face_ai = item.get('faceAnalysisResponse', {}) or {}
                    health_assessment = face_ai.get('healthAssessment', {}) or {}
                    subject = health_assessment.get('subject', '未知')
                report_name = f"分析报告-{report_id}"
                report_url = _get_analysis_export_url(report_id)
                output_all += f"| {report_name} | {create_time} | {subject} | [🔗 查看报告]({report_url}) |\n"

        output_all += "\n> 注：分析结果仅供参考。"
        return output_all


skill = Skill()


path: skills/smyx_analysis/scripts/smyx_analysis.py
content: #!/usr/bin/env python3
import sys
import os

current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
sys.path.insert(0, parent_dir)

import argparse
import json
import mimetypes
import traceback
from datetime import datetime

import requests
import sys
import os

from .config import *

from .skill import skill

# import_path_common()
from skills.smyx_common.scripts.util import RequestUtil

# 从config导入常量
SUPPORTED_FORMATS = ConstantEnum.SUPPORTED_FORMATS
MAX_FILE_SIZE_MB = ConstantEnum.MAX_FILE_SIZE_MB


def analyze_video(input_path=None, url=None, api_url=None, api_key=None, output_level=None):
    """调用API分析视频"""
    if not input_path and not url:
        raise ValueError("必须提供本地视频路径(--input)或网络视频URL(--url)")
    try:
        input_path = input_path or url
        return skill.get_output_analysis(input_path)

    except requests.exceptions.RequestException as e:
        traceback.print_stack()
        raise Exception(f"API请求失败: {str(e)}")


def show_analyze_list(open_id, start_time=None, end_time=None):
    try:
        output_content = skill.get_output_analysis_list()
        return output_content

    except requests.exceptions.RequestException as e:
        traceback.print_stack()
        raise Exception(f"API请求失败: {str(e)}")


def main():
    parser = argparse.ArgumentParser(description="视频分析工具")
    parser.add_argument("--input", help="本地MP4视频文件路径")
    parser.add_argument("--url", help="网络视频MP4的URL地址")
    parser.add_argument("--open-id", required=True, help="当前用户的OpenID/UserId/用户名/手机号")
    parser.add_argument("--list", action='store_true', help="显示视频历史列表清单")
    parser.add_argument("--api-url", help="服务端API地址")
    parser.add_argument("--api-key", help="API访问密钥（必需）")
    parser.add_argument("--output", help="结果输出文件路径")
    parser.add_argument("--detail", choices=["basic", "standard", "json"],
                        default=ConstantEnum.DEFAULT__OUTPUT_LEVEL,
                        help="输出详细程度")
    parser.add_argument("--export-env-only", action='store_true',
                        help="仅输出 export 命令设置环境变量，不执行分析")

    args = parser.parse_args()

    try:
        if args.open_id:
            ConstantEnumBase.CURRENT__OPEN_ID = args.open_id

        # 检查必需参数
        if args.list:
            open_id = ConstantEnum.CURRENT__OPEN_ID
            result = show_analyze_list(open_id)
            print(result)
            exit(0)

        # 检查必需参数
        if not args.input and not args.url:
            print("❌ 错误: 必须提供 --input 或 --url 参数")
            exit(1)

        print("🔍 正在分析视频，请稍候...")
        output_content = analyze_video(
            input_path=args.input,
            url=args.url,
            api_url=args.api_url,
            [REDACTED_SECRET],
            output_level=args.detail
        )

        print(output_content)

        # 保存到文件
        if args.output:
            with open(args.output, "w", encoding="utf-8") as f:
                if args.detail == "full":
                    json.dump(result, f, ensure_ascii=False, indent=2)
                else:
                    f.write(output_content)
            print(f"✅ 结果已保存到: {args.output}")

    except Exception as e:
        traceback.print_stack()
        print(f"❌ 分析失败: {str(e)}")
        exit(1)


if __name__ == "__main__":
    main()


path: skills/smyx_common/requirements.txt
content: altgraph==0.17.5
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.12.1
APScheduler==3.11.2
astroid==3.1.0
Authlib==1.6.6
blinker==1.4
cachetools==6.2.6
certifi==2026.1.4
cffi==2.0.0
chardet==5.2.0
charset-normalizer==3.4.4
click==8.3.1
coverage==7.13.2
coze-workload-identity==0.1.7
cozeloop==0.1.19
cryptography==3.4.8
Cython==3.2.4
dbus-python==1.2.18
dill==0.4.1
distro==1.7.0
distro-info==1.1+ubuntu0.2
et_xmlfile==2.0.0
fastapi==0.121.2
gitdb==4.0.12
gitignore_parser==0.1.13
GitPython==3.1.45
greenlet==3.3.1
h11==0.16.0
httpcore==1.0.9
httplib2==0.20.2
httpx==0.28.1
httpx-ws==0.8.2
idna==3.11
importlib-metadata==4.6.4
inflect==7.5.0
iniconfig==2.3.0
isort==5.13.2
jeepney==0.7.1
Jinja2==3.1.6
jiter==0.12.0
jsonpatch==1.33
jsonpointer==3.0.0
keyring==23.5.0
langchain==1.0.3
langchain-core==1.0.2
langchain-openai==1.0.1
langgraph==1.0.2
langgraph-checkpoint==3.0.0
langgraph-prebuilt==1.0.2
langgraph-sdk==0.2.9
langsmith==0.4.39
launchpadlib==1.10.16
lazr.restfulclient==0.14.4
lazr.uri==1.0.6
markdown-it-py==4.0.0
MarkupSafe==3.0.3
mccabe==0.7.0
mdurl==0.1.2
more-itertools==8.10.0
numpy==2.4.1
oauthlib==3.2.0
openai==2.16.0
openpyxl==3.1.5
orjson==3.11.5
ormsgpack==1.12.2
packaging==25.0
pandas==2.3.3
platformdirs==4.5.1
pluggy==1.6.0
psutil==7.1.3
psycopg2-binary==2.9.11
pydantic==2.12.4
pydantic_core==2.41.5
pydash==8.0.6
Pygments==2.19.2
PyGObject==3.42.1
pyinstaller==6.18.0
pyinstaller-hooks-contrib==2026.0
PyJWT==2.10.1
pylint==3.1.0
PyMySQL==1.1.2
pyparsing==2.4.7
pytest==9.0.1
pytest-asyncio==1.3.0
pytest-cov==7.0.0
pytest-mock==3.15.1
python-apt==2.4.0+ubuntu4.1
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.3
regex==2026.1.15
requests==2.32.5
requests-toolbelt==1.0.0
rich==14.2.0
SecretStorage==3.3.1
setuptools==80.9.0
six==1.16.0
smmap==5.0.2
sniffio==1.3.1
sqlacodegen==3.2.0
SQLAlchemy==2.0.46
starlette==0.49.3
supervisor==4.2.1
tenacity==9.1.2
tiktoken==0.12.0
tomlkit==0.14.0
typeguard==4.4.4
typing-inspection==0.4.2
typing_extensions==4.15.0
tzdata==2025.3
tzlocal==5.3.1
unattended-upgrades==0.1
urllib3==2.6.3
uvicorn==0.38.0
wadllib==1.3.6
watchdog==6.0.0
websockets==15.0.1
wheel==0.45.1
wsproto==1.3.2
xlrd==2.0.2
xxhash==3.6.0
zipp==1.0.0
zstandard==0.25.0


path: skills/smyx_common/scripts/__init__.py
content: from .util import RequestUtil, CommonUtil, DatetimeUtil, NumberUtil
from .base import *
from .config import *

__all__ = [
    'RequestUtil',
    'CommonUtil',
    'BaseUtil'
]


path: skills/smyx_common/scripts/api_service.py
content: #!/usr/bin/env python3

from .config import ApiEnum

from .base import BaseApiService
from .util import RequestUtil, CommonUtil


class ApiService(BaseApiService):

    def __init__(self):
        super().__init__()

    def get_download_url(self, tosKey, expireSeconds=3600):
        return RequestUtil.http_post(
            ApiEnum.GET_DOWNLOAD_URL__URL,
            params={
                "tosKey": tosKey,
                "expireSeconds": expireSeconds * 24
            }
        )

    def page(self, url, pageNum=None, pageSize=None, *args, **argss):
        data = args[0] if len(args) > 0 else argss.get('data') if argss.get('data') is not None else {}
        if pageNum is None:
            pageNum = 1
        if pageSize is None:
            pageSize = ApiEnum.DEFAULT__PAGE_SIZE
        paramsPage = {
            'pageNum': int(pageNum),
            'pageSize': int(pageSize)
        }
        data.update({
            "page": paramsPage
        })
        if not CommonUtil.is_empty(data):
            if (len(args) == 0):
                argss.setdefault("data", data)
        response = RequestUtil.http_post(
            url,
            *args, **argss
        )
        return response

    def list(self, url=None, *args, **argss):
        if url is not None:
            argss["url"] = url
        return self.page(1, ApiEnum.DEFAULT__PAGE_SIZE_MAX, *args, **argss)

    def add(self, url=None, *args, **argss):
        response = RequestUtil.http_post(
            url,
            *args, **argss
        )
        return response

    def edit(self, url=None, *args, **argss):
        response = RequestUtil.http_post(
            url,
            *args, **argss
        )
        return response

    def delete(self, url=None, *args, **argss):
        response = RequestUtil.http_post(
            url,
            *args, **argss
        )
        return response

    def http_post(self, url=None, *args, **argss):
        return RequestUtil.http_post(
            url,
            *args, **argss
        )

    def http_put(self, url=None, *args, **argss):
        return RequestUtil.http_put(
            url,
            *args, **argss
        )

    def http_get(self, url=None, *args, **argss):
        return RequestUtil.http_get(
            url,
            *args, **argss
        )

        return response

    def http_delete(self, url=None, *args, **argss):
        return RequestUtil.http_delete(
            url,
            *args, **argss
        )
        return response


path: skills/smyx_common/scripts/base.py
content: class BaseUtil:
    pass


class BaseMixin:
    pass


class BaseDao:
    pass


class BaseService:
    def __init__(self):
        super().__init__()


class BaseApiService(BaseService):
    INSTANCE = None

    def __init__(self):
        super().__init__()

    @classmethod
    def get_instance(cls):
        if cls.INSTANCE is None:
            cls.INSTANCE = cls()
        return cls.INSTANCE


class BaseSkill:
    def __init__(self):
        super().__init__()


path: skills/smyx_common/scripts/config-dev.yaml
content: ApiEnum:
  base-url-open-api: "http://192.168.1.234:9601/smyx-open-api"
  base-url-open-h5: "http://192.168.1.234:4100"
  base-url-health: "http://192.168.1.234:7070/jeecg-boot-xzgz"

ConstantEnum:
  is-debug: true

path: skills/smyx_common/scripts/config-test.yaml
content: ApiEnum:
  base-url-open-api: "https://livemonitortest.lifeemergence.com/smyx-open-api"
  base-url-open-h5: "http://livemonitortest.lifeemergence.com"
  base-url-health: "https://healthtest.lifeemergence.com/jeecg-boot-xzgz"

ConstantEnum:
  is-debug: true

path: skills/smyx_common/scripts/config.py
content: #!/usr/bin/env python3
import os
import sys
from enum import Enum
from typing import Dict
import inspect

import yaml
import platform


class YamlUtil:

    @staticmethod
    def load(path, config: Dict = {}) -> Dict:
        try:
            if not os.path.exists(path):
                os.makedirs(os.path.dirname(path), exist_ok=True)
                with open(path, "w", encoding="utf-8") as f:
                    yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
                return config
            with open(path, "r", encoding="utf-8") as f:
                config = yaml.safe_load(f) or {}
                for key, value in config.items():
                    if key not in config:
                        config[key] = value
                return config
        except:
            pass
        return config

    @staticmethod
    def save(path, config: Dict) -> Dict:
        try:
            with open(path, "w", encoding="utf-8") as f:
                yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
        except:
            pass
        return config


class BaseEnum:

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        clsModule = cls.__module__
        cls_path = inspect.getfile(cls)
        clsFullName = f"{cls.__module__}.{cls.__name__}"
        cls_dirpath = os.path.dirname(cls_path)  # .../src
        clsModulePath = clsModule.replace(".", "\\")
        current_dir = os.path.dirname(os.path.abspath(__file__))  # .../src
        config_path = os.path.join(cls_dirpath, "config.yaml")
        config = YamlUtil.load(config_path)
        cls.init(config)
        env = config.get("env")
        if env:
            env_config_path = os.path.join(cls_dirpath, f"config-{env}.yaml")
            env_config = YamlUtil.load(env_config_path)
            cls.init(env_config)

    @classmethod
    def init(cls, config=None):
        clsName = cls.__name__
        clsConfig = config and config.get(clsName)
        if clsConfig:
            for config_key, config_value in clsConfig.items():
                new_config_key = config_key = config_key.upper().replace("-", "_")
                if hasattr(cls, new_config_key):
                    setattr(cls, new_config_key, config_value)


class ApiEnum(BaseEnum):
    API_KEY = None

    API_SECRET_KEY = None

    DATABASE_URL = ""

    BASE_URL_OPEN_API = ""

    BASE_URL_OPEN_H5 = ""

    BASE_URL_HEALTH = ""

    OPEN_TOKEN = ""

    TOKEN = ""

    DEFAULT__REQUEST_TIMEOUT = 120

    DEFAULT__PAGE_SIZE = 5

    DEFAULT__PAGE_SIZE_MAX = 65536

    GET_DOWNLOAD_URL__URL = BASE_URL_OPEN_API + "/api/tos/get-download-url"


class ConstantEnum(BaseEnum):
    class SourceEnum(Enum):
        ARK_CLAW = "ARK_CLAW"
        JVS_CLAW = "JVS_CLAW"
        LIGHT_CLAW = "LIGHT_CLAW"
        WUHONG = "WUHONG"
        COZE = "COZE"
        SKILL_HUB = "SKILL_HUB"
        GIT_HUB = "GIT_HUB"
        CLAW_HUB = "CLAW_HUB"
        FEISHU = "FEISHU"
        DINGTALK = "DINGTALK"
        WEIXIN = "WEIXIN"
        YUANBAO = "YUANBAO"
        WECOM = "WECOM"
        QQBOT = "QQBOT"

    APP__ID = ""

    APP__SOURCE = SourceEnum.CLAW_HUB.value

    IS_DEBUG = False

    CURRENT__OPEN_ID = ""

    CURRENT__USER_NAME = ""

    CURRENT__TENTANT_CODE = ""

    FEISHU_APP__ID = ""

    FEISHU_APP__SECRET = ""

    FEISHU_APP__RECEIVE_ID = ""

    DEFAULT__SCENE_CODE = ""

    DEFAULT__OPEN_ID = ""

    DEFAULT__SKILL_HUB_NAME = APP__SOURCE

    DEFAULT__SKILL_PLATFORM_NAME = ""

    DEFAULT__OUTPUT_LEVEL = "json"

    SUPPORTED_FORMATS = ["mp4", "avi", "mov"]

    MAX_FILE_SIZE_MB = 10

    @staticmethod
    def is_debug():
        return platform.system() == 'Windows' or platform.system() != 'Linux' and ConstantEnum.IS_DEBUG

    @classmethod
    def init(cls, config=None):
        super().init(config)
        openclaw_sender_open_id = os.environ.get("OPENCLAW_SENDER_OPEN_ID")
        openclaw_sender_username = os.environ.get("OPENCLAW_SENDER_USERNAME")
        feishu_open_id = os.environ.get("FEISHU_OPEN_ID")
        if openclaw_sender_open_id:
            cls.CURRENT__OPEN_ID = openclaw_sender_open_id
        if openclaw_sender_username:
            cls.CURRENT__USER_NAME = openclaw_sender_username
        if feishu_open_id:
            cls.FEISHU_APP__RECEIVE_ID = feishu_open_id

    class SceneCodeEnum(Enum):
        # 开放 #
        OPEN_HEALTH_AI_ANALYSIS = "OPEN_HEALTH_AI_ANALYSIS"
        OPEN_PERSON_RISK_ANALYSIS = "OPEN_PERSON_RISK_ANALYSIS"
        # 智眸 #
        PUBLIC_AREA_AI_ANALYSIS = "PUBLIC_AREA_AI_ANALYSIS"
        PERSONNEL_LEAVE_POST_MONITORING = "PERSONNEL_LEAVE_POST_MONITORING"
        CRAWL_MONITOR = "CRAWL_MONITOR"
        # 先兆感知 #
        XIAN_ZHAO_GAN_ZHI_DESK_LAMP = "XIAN_ZHAO_GAN_ZHI_DESK_LAMP"
        PET_ANALYSIS = "PET_ANALYSIS"
        CRAWL_ANALYSIS = "CRAWL_ANALYSIS"
        AQUARIUM_ANALYSIS = "AQUARIUM_ANALYSIS"
        PSYCHOLOGY_ANALYSIS = "PSYCHOLOGY_ANALYSIS"
        AUTISM_ANALYSIS = "AUTISM_ANALYSIS"
        DIET_ANALYSIS = "DIET_ANALYSIS"
        DRIVE_ANALYSIS = "DRIVE_ANALYSIS"
        SPORT_ANALYSIS = "SPORT_ANALYSIS"
        EMOTION_ANALYSIS = "EMOTION_ANALYSIS"
        STUDY_ANALYSIS = "STUDY_ANALYSIS"
        RISK_ANALYSIS = "PERSON_RISK"
        INFANT_SAFETY_MONITORING_ANALYSIS = "INFANT_SAFETY_MONITORING"
        PHONE_USAGE_MONITORING_ANALYSIS = "PHONE_USAGE_MONITORING"
        INCONTINENCE_ALERT_ANALYSIS = "INCONTINENCE_ALERT"
        RESPIRATORY_SYMPTOM_RECOGNITION_ANALYSIS = "RESPIRATORY_SYMPTOM_RECOGNITION"
        ELECTRIC_VEHICLE_DETECTION_ANALYSIS = "ELECTRIC_VEHICLE_DETECTION"
        SMOKING_DETECTION_ANALYSIS = "SMOKING_DETECTION"
        PET_DETECTION_FEEDER_ANALYSIS = "PET_DETECTION_FEEDER"
        PET_HEALTH_MONITORING_ANALYSIS = "PET_HEALTH_MONITORING"
        STROKE_RISK_SCREENING_ANALYSIS = "STROKE_RISK_SCREENING"
        HUMAN_DETECTION_ANALYSIS = "HUMAN_DETECTION"
        STRANGER_RECOGNITION_ANALYSIS = "STRANGER_RECOGNITION"
        FOCUS_ANALYSIS_ANALYSIS = "FOCUS_ANALYSIS"
        HUMAN_POSTURE_RECOGNITION_ANALYSIS = "HUMAN_POSTURE_RECOGNITION"
        HUMAN_EMOTION_RECOGNITION_ANALYSIS = "HUMAN_EMOTION_RECOGNITION"
        FIRE_SMOKE_DETECTION_ANALYSIS = "FIRE_SMOKE_DETECTION"
        BASIC_OBJECT_DETECTION_ANALYSIS = "BASIC_OBJECT_DETECTION"
        CHILD_DANGEROUS_BEHAVIOR_RECOGNITION_ANALYSIS = "CHILD_DANGEROUS_BEHAVIOR_RECOGNITION"
        PET_RESTRICTED_AREA_WARNING_ANALYSIS = "PET_RESTRICTED_AREA_WARNING"
        SLEEP_QUALITY_ANALYSIS_ANALYSIS = "SLEEP_QUALITY_ANALYSIS"
        PET_DETECTION_ANALYSIS = "PET_DETECTION"
        PSYCHOLOGICAL_STRESS_ASSESSMENT_ANALYSIS = "PSYCHOLOGICAL_STRESS_ASSESSMENT"
        VISUAL_QA_ANALYSIS = "VISUAL_QA"
        PET_BODY_HEALTH_ANALYSIS = "PET_BODY_HEALTH_ANALYSIS"
        PET_BEHAVIOR_DETECTION_ANALYSIS = "PET_BEHAVIOR_DETECTION"
        INFANT_SUFFOCATION_WARNING_ANALYSIS = "INFANT_SUFFOCATION_WARNING"
        STRANGER_APPROACH_WARNING_ANALYSIS = "STRANGER_APPROACH_WARNING"
        IMAGE_QUALITY_DETECTION_ANALYSIS = "IMAGE_QUALITY_DETECTION"
        CHILD_EMOTION_RECOGNITION_ANALYSIS = "CHILD_EMOTION_RECOGNITION"
        OUTDOOR_MONITORING_ANALYSIS = "OUTDOOR_MONITORING"
        FALL_DETECTION_IMAGE_ANALYSIS = "FALL_DETECTION_IMAGE"
        CUSTOM_TIMELAPSE_ANALYSIS = "CUSTOM_TIMELAPSE"
        CONTACTLESS_VITAL_SIGNS_MONITORING_ANALYSIS = "CONTACTLESS_VITAL_SIGNS_MONITORING"
        VIDEO_SEARCH_ANALYSIS = "VIDEO_SEARCH"
        FAMILIAR_PERSON_RECOGNITION_ANALYSIS = "FAMILIAR_PERSON_RECOGNITION"
        TCM_CONSTITUTION_RECOGNITION_ANALYSIS = "TCM_CONSTITUTION_RECOGNITION"
        CONTACTLESS_HEALTH_RISK_DETECTION_ANALYSIS = "CONTACTLESS_HEALTH_RISK_DETECTION"
        UNACCOMPANIED_MONITORING_ANALYSIS = "UNACCOMPANIED_MONITORING"
        ELDERLY_FALL_DETECTION_ANALYSIS = "ELDERLY_FALL_DETECTION"
        PARKINSON_EPILEPSY_BEHAVIOR_RECOGNITION_ANALYSIS = "PARKINSON_EPILEPSY_BEHAVIOR_RECOGNITION"
        PET_BREED_INDIVIDUAL_RECOGNITION_ANALYSIS = "PET_BREED_INDIVIDUAL_RECOGNITION"
        ELDERLY_BED_EXIT_WANDERING_MONITORING_ANALYSIS = "ELDERLY_BED_EXIT_WANDERING_MONITORING"
        ARRHYTHMIA_EARLY_WARNING_ANALYSIS = "ARRHYTHMIA_EARLY_WARNING"
        FIRE_DETECTION_ANALYSIS = "FIRE_DETECTION"
        VISUAL_SUMMARY_ANALYSIS = "VISUAL_SUMMARY"
        PACKAGE_DETECTION_ANALYSIS = "PACKAGE_DETECTION"
        INFANT_BLANKET_KICK_MONITORING_ANALYSIS = "INFANT_BLANKET_KICK_MONITORING"
        PET_CALMING_TRIGGER_ANALYSIS = "PET_CALMING_TRIGGER"
        CAT_FACE_RECOGNITION_ANALYSIS = "CAT_FACE_RECOGNITION"
        INFANT_SLEEP_MONITORING_ANALYSIS = "INFANT_SLEEP_MONITORING"
        VIRTUAL_FENCE_INTRUSION_WARNING_ANALYSIS = "VIRTUAL_FENCE_INTRUSION_WARNING"
        FALL_DETECTION_VIDEO_ANALYSIS = "FALL_DETECTION_VIDEO"
        INFANT_CRY_ANALYSIS = "INFANT_CRY_ANALYSIS"
        PET_VOCAL_EMOTION_ANALYSIS = "PET_VOCAL_EMOTION_ANALYSIS"
        SMYX_PET_LITTER_BOX_WASTE_ANALYSIS = "SMYX_PET_LITTER_BOX_WASTE_ANALYSIS"
        SMYX_PET_SCRATCH_FREQUENCY_INTENSITY_ANALYSIS = "SMYX_PET_SCRATCH_FREQUENCY_INTENSITY_ANALYSIS"
        SMYX_PET_BODY_CONDITION_SCORE_3D_ANALYSIS = "SMYX_PET_BODY_CONDITION_SCORE_3D_ANALYSIS"
        SMYX_PET_HOSPITAL_WAITING_ANXIETY_ANALYSIS = "SMYX_PET_HOSPITAL_WAITING_ANXIETY_ANALYSIS"
        SMYX_PET_WATER_FOUNTAIN_INTAKE_ANALYSIS = "SMYX_PET_WATER_FOUNTAIN_INTAKE_ANALYSIS"
        SMYX_PET_TOY_INTERACTION_ACTIVITY_ANALYSIS = "SMYX_PET_TOY_INTERACTION_ACTIVITY_ANALYSIS"
        SMYX_GROOMING_EFFECTIVENESS_ANALYSIS = "SMYX_GROOMING_EFFECTIVENESS_ANALYSIS"
        SMYX_PET_GROOMING_STRESS_BEHAVIOR_ANALYSIS = "SMYX_PET_GROOMING_STRESS_BEHAVIOR_ANALYSIS"
        SMYX_PET_STOOL_MORPHOLOGY_RECOGNITION_ANALYSIS = "SMYX_PET_STOOL_MORPHOLOGY_RECOGNITION_ANALYSIS"
        SMYX_PET_CARRIER_RESPIRATORY_RATE_ANALYSIS = "SMYX_PET_CARRIER_RESPIRATORY_RATE_ANALYSIS"
        SMYX_PET_EATING_SPEED_SLOW_FEED_ANALYSIS = "SMYX_PET_EATING_SPEED_SLOW_FEED_ANALYSIS"
        SMYX_ADAPTIVE_PET_DRYING_TEMPERATURE_ANALYSIS = "SMYX_ADAPTIVE_PET_DRYING_TEMPERATURE_ANALYSIS"
        SMYX_PET_ORAL_SNAPSHOT_GUM_REDNESS_ANALYSIS = "SMYX_PET_ORAL_SNAPSHOT_GUM_REDNESS_ANALYSIS"
        SMYX_PET_TRAINING_COMMAND_EXECUTION_ANALYSIS = "SMYX_PET_TRAINING_COMMAND_EXECUTION_ANALYSIS"
        SMYX_PET_DRYING_BOX_HEAT_STRESS_ANALYSIS = "SMYX_PET_DRYING_BOX_HEAT_STRESS_ANALYSIS"
        SMYX_PET_CLIMBING_FRAME_HEATMAP_ANALYSIS = "SMYX_PET_CLIMBING_FRAME_HEATMAP_ANALYSIS"
        SMYX_PET_PICA_BEHAVIOR_RECOGNITION_ANALYSIS = "SMYX_PET_PICA_BEHAVIOR_RECOGNITION_ANALYSIS"
        BIRD_RECOGNITION_ANALYSIS = "BIRD_RECOGNITION"
        FRAUD_CALL_IDENTIFICATION = "FRAUD_CALL_IDENTIFICATION"
        PLANT_SPECIES_RECOGNITION = "PLANT_SPECIES_RECOGNITION"
        PLANT_GROWTH_STAGE_RECOGNITION = "PLANT_GROWTH_STAGE_RECOGNITION"
        PLANT_DISEASE_RECOGNITION = "PLANT_DISEASE_RECOGNITION"
        PLANT_NUTRITION_DIAGNOSIS = "PLANT_NUTRITION_DIAGNOSIS"
        PLANT_WILTING_MONITORING = "PLANT_WILTING_MONITORING"


path: skills/smyx_common/scripts/config.yaml
content: env: prod

ApiEnum:
  [REDACTED_SECRET]
  database-url:
  base-url-open-api: "https://open.lifeemergence.com/smyx-open-api"
  base-url-open-h5: "http://livemonitor.lifeemergence.com"
  base-url-health: "https://lifeemergence.com/jeecg-boot-xzgz"

ConstantEnum:
  is-debug: false
  app--id: x1a3s4nwy1s2r4se
  current--tentant-code: "XIAN_ZHAO_GAN_ZHI"
  feishu-app--id: cli_a93d769369badcb1
  feishu-app--[REDACTED_SECRET] ARK_CLAW


path: skills/smyx_common/scripts/dao.py
content: #!/usr/bin/env python3
"""
本地化轻量级数据库封装
使用SQLite + SQLAlchemy ORM
支持基础CRUD操作，通过继承BaseDao快速实现各表的Dao层
"""
import datetime
import sys
from enum import Enum
from typing import Any, Dict, List, Optional, Type, TypeVar
from sqlalchemy import create_engine, Column, Integer, String, DateTime, func, Select, Table, MetaData, select, or_
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from sqlalchemy.sql.expression import text

from skills.smyx_common.scripts.config import ConstantEnum, ApiEnum

from skills.smyx_common.scripts.util import StringUtil, DatetimeUtil, FileUtil

from skills.smyx_common.scripts.base import BaseMixin, BaseDao

# 基础模型类
Base = declarative_base()

# 泛型类型，用于返回对应模型实例
T = TypeVar('T', bound=Base)

meta = MetaData()

DATABASE_URL = ApiEnum.DATABASE_URL


class BaseModelMixin(BaseMixin):

    @classmethod
    def load(cls, source: dict):
        """
        获取源枚举
        :param source: 源
        :return: User
        """
        column_names = cls.__table__.columns.keys()
        user_dict = {k: source.get(StringUtil.snake_to_camel(k)) for k in column_names}
        user_dict["create_time"] = DatetimeUtil.parse(user_dict["create_time"])
        user_dict["update_time"] = DatetimeUtil.parse(user_dict["update_time"])
        model = cls(**user_dict)
        return model


class Dao(BaseDao):
    """
    基础Dao类，提供通用的CRUD操作
    子类只需配置__model__和__tablename__即可使用
    """
    __model__: Type[T] = None  # 对应的模型类，子类必须配置
    __tablename__: str = None  # 表名，子类必须配置

    def get_db_path(self, db_path):
        import os

        cwd = os.getcwd()
        workspace = os.path.dirname(cwd)
        workspace = os.path.dirname(workspace)
        workspace = os.environ.get('OPENCLAW_WORKSPACE', workspace)
        parent_dir = os.path.join(workspace, "data")
        FileUtil.mkdir(parent_dir)
        db_path = os.path.join(parent_dir, db_path)

        return db_path

    def __init__(self, db_path: str = None):
        """
        初始化Dao
        :param db_path: SQLite数据库文件路径
        """

        if not db_path:
            db_path = "smyx-common-claw.db"
            db_path = self.get_db_path(db_path)

        self.engine = create_engine(f"sqlite:///{db_path}", echo=False)

        # 创建会话工厂
        self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
        # 初始化表结构
        self._create_tables()
        self._alter_tables()

    def _create_tables(self) -> None:
        """创建所有表结构"""
        Base.metadata.create_all(bind=self.engine)

    def _alter_tables(self) -> None:
        """创建所有表结构"""
        sql_statement = "ALTER TABLE sys_user ADD COLUMN source_id INT;"

        # 3. 执行语句
        try:
            with self.engine.connect() as connection:
                connection.execute(text(sql_statement))
                connection.commit()  # 对于数据定义语言(DDL)，需要显式提交
        except Exception as e:
            connection.rollback()
            if len(e.args) and "duplicate column name" in e.args[0]:
                pass
            else:
                raise

    def get_session(self) -> Session:
        """获取数据库会话"""
        return self.SessionLocal()

    def save(self, model) -> T:
        """
        创建新记录
        :param kwargs: 字段键值对
        :return: 创建的模型实例
        """

        try:
            return self.add(
                model
            )

        except Exception as e:
            return self.update(
                model
            )

    def add(self, model) -> T:
        """
        创建新记录
        :param kwargs: 字段键值对
        :return: 创建的模型实例
        """
        session = self.get_session()
        try:
            session.add(model)
            session.commit()
            session.refresh(model)
            return model
        finally:
            session.close()

    def create(self, **kwargs) -> T:
        """
        创建新记录
        :param kwargs: 字段键值对
        :return: 创建的模型实例
        """
        instance = self.__model__(**kwargs)
        return self.add(instance)

    def get_by_id(self, record_id: int) -> Optional[T]:
        """
        根据ID查询记录
        :param record_id: 记录ID
        :return: 模型实例或None
        """
        session = self.get_session()
        try:
            return session.query(self.__model__).filter(self.__model__.id == record_id).first()
        finally:
            session.close()

    def get_by_username(self, username: str) -> Optional[T]:
        """
        根据ID查询记录
        :param record_id: 记录ID
        :return: 模型实例或None
        """
        session = self.get_session()
        try:
            or_(
                self.__model__.del_flag == 0,
                self.__model__.del_flag.is_(None)  # 关键：使用 .is_(None) 来判断 SQL 的 NULL
            )
            return session.query(self.__model__).filter(self.__model__.username == username,
                                                        or_(
                                                            self.__model__.del_flag == 0,
                                                            self.__model__.del_flag.is_(None)
                                                            # 关键：使用 .is_(None) 来判断 SQL 的 NULL
                                                        )).first()
        finally:
            session.close()

    def list(self, filters: Optional[Dict[str, Any]] = None, limit: Optional[int] = None,
             offset: Optional[int] = None) -> List[T]:
        """
        查询记录列表
        :param filters: 过滤条件字典，如{"name": "张三", "age": 18}
        :param limit: 最大返回数量
        :param offset: 偏移量
        :return: 模型实例列表
        """
        session = self.get_session()
        try:
            query = session.query(self.__model__)
            # .where(self.__model__.id != 2, self.__model__.id == 1))

            if filters:
                for key, value in filters.items():
                    query = query.filter(getattr(self.__model__, key) == value)

            if offset:
                query = query.offset(offset)
            if limit:
                query = query.limit(limit)

            return query.all()
        finally:
            session.close()

    def update(self, model) -> Optional[T]:
        """
        更新记录
        :param record_id: 记录ID
        :param kwargs: 要更新的字段键值对
        :return: 更新后的模型实例或None
        """
        session = self.get_session()
        try:
            instance = session.query(self.__model__).filter(self.__model__.id == model.id).first()
            if not instance:
                return None

            column_names = self.__model__.__table__.columns.keys()

            for key in column_names:
                value = getattr(model, key)
                setattr(instance, key, value)

            session.commit()
            session.refresh(instance)
            return instance
        finally:
            session.close()

    def modify(self, record_id: int, **kwargs) -> Optional[T]:
        """
        更新记录
        :param record_id: 记录ID
        :param kwargs: 要更新的字段键值对
        :return: 更新后的模型实例或None
        """
        session = self.get_session()
        try:
            instance = session.query(self.__model__).filter(self.__model__.id == record_id).first()
            if not instance:
                return None

            for key, value in kwargs.items():
                setattr(instance, key, value)

            session.commit()
            session.refresh(instance)
            return instance
        finally:
            session.close()

    def update_by_username(self, username: str, **kwargs) -> Optional[T]:
        """
        更新记录
        :param username: 记录ID
        :param kwargs: 要更新的字段键值对
        :return: 更新后的模型实例或None
        """
        session = self.get_session()
        try:
            instance = session.query(self.__model__).filter(self.__model__.username == username).first()
            if not instance:
                return None

            for key, value in kwargs.items():
                setattr(instance, key, value)

            session.commit()
            session.refresh(instance)
            return instance
        finally:
            session.close()

    def delete(self, record_id: int) -> bool:
        """
        删除记录
        :param record_id: 记录ID
        :return: 删除成功返回True，失败返回False
        """
        session = self.get_session()
        try:
            instance = session.query(self.__model__).filter(self.__model__.id == record_id).first()
            if not instance:
                return False

            session.delete(instance)
            session.commit()
            return True
        finally:
            session.close()

    def count(self, filters: Optional[Dict[str, Any]] = None) -> int:
        """
        统计记录数量
        :param filters: 过滤条件字典
        :return: 记录数量
        """
        session = self.get_session()
        try:
            query = session.query(func.count(self.__model__.id))

            if filters:
                for key, value in filters.items():
                    query = query.filter(getattr(self.__model__, key) == value)

            return query.scalar()
        finally:
            session.close()


class User(Base, BaseModelMixin):
    """用户模型"""
    __tablename__ = "sys_user"

    id = Column(String(32), primary_key=True, index=True)
    source_id = Column(String(32), comment="源头id")
    username = Column(String(100), unique=True, index=True, nullable=False, comment="用户名")
    email = Column(String(45), unique=True, index=True, comment="邮箱")
    birthday = Column(DateTime, unique=True, index=True, comment="邮箱")
    sex = Column(Integer, comment="性别")
    age = Column(Integer, comment="年龄")
    [REDACTED_SECRET]), comment="token")
    open_token = Column(String(1000), comment="开放token")
    source = Column(String(50), comment="token")
    del_flag = Column(Integer, comment="是否删除", default=0)
    create_time = Column(DateTime, default=func.now(), comment="创建时间")
    update_time = Column(DateTime, default=func.now(), onupdate=func.now(), comment="更新时间")

    SourceEnum = ConstantEnum.SourceEnum


class UserDao(Dao):
    """用户Dao，继承BaseDao即可拥有所有基础CRUD功能"""
    __model__ = User
    __tablename__ = "users"


if __name__ == "__main__":
    pass


path: skills/smyx_common/scripts/skill.py
content: #!/usr/bin/env python3
import os
import sys
# import subprocess

from .config import ApiEnum as ApiEnumBase, ConstantEnum
from .base import BaseSkill
from .util import FileUtil

from .api_service import ApiService


class Skill(BaseSkill, ApiService):

    def __init__(self):
        super().__init__()


class AgentSkill(BaseSkill, ApiService):

    def __init__(self):
        super().__init__()

    def ai_chat(self, prompt: str, session_id: str = None, timeout: int = 120):
        """
        通过 subprocess 调用 openclaw agent 命令

        Args:
            prompt: 分析提示
            session_id: 会话 ID（可选）
            timeout: 超时时间（秒）

        Returns:
            分析结果或会话 ID
        """
        import uuid

        # 生成唯一会话 ID
        if not session_id:
            entry_script = sys.argv[0]
            abs_entry_script = os.path.abspath(entry_script)
            main_name = FileUtil.get_name(abs_entry_script)
            session_id = f"{main_name}--{uuid.uuid4()}"

        ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent (会话：{session_id})..., prompt:{prompt}")

        # 构建命令
        cmd = [
            # "openclaw",
            # "agent",
            # "-m", str(prompt),
            # "--session-id", session_id,
            # "--thinking", "minimal",
            # "--timeout", str(timeout)
        ]

        ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行命令{' '.join(cmd)}")

        try:
            # 执行命令
            # result = subprocess.run(
            #     cmd,
            #     capture_output=True,
            #     text=True,
            #     timeout=timeout + 10
            # )

            result: dict = {}

            if result.stderr:
                ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行错误:{result.stderr}")
                return

            output = result.stdout
            ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行成功:{output}")

            return output

        # except subprocess.TimeoutExpired as e:
        #     ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 超时（{timeout}秒），任务可能仍在后台运行:{e}")
        except Exception as e:
            ConstantEnum.is_debug() and print(f"🤖 正在调用 openclaw agent 执行错误:{e}")


skill = Skill()


path: skills/smyx_common/scripts/util.py
content: #!/usr/bin/env python3
import json
import os
import traceback

import requests
from .config import ApiEnum, ConstantEnum, sys, YamlUtil

from .base import BaseUtil
import time
import logging
from typing import Any, Callable, Optional, TypeVar, Dict
import pydash as _

if ConstantEnum.is_debug():
    import http.client

    # 【关键代码】开启调试模式
    http.client.HTTPConnection.debuglevel = 1
    # 可选：如果你希望日志更整洁，可以配合 logging 模块（否则打印会比较乱）
    import logging

    logging.basicConfig()
    logging.getLogger().setLevel(logging.DEBUG)
    requests_log = logging.getLogger("urllib3")
    requests_log.setLevel(logging.DEBUG)
    requests_log.propagate = True


class StringUtil(BaseUtil):

    @staticmethod
    def camel_to_snake(name):
        import re
        s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
        return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()

    @staticmethod
    def snake_to_pascal(name):
        import re
        name = re.sub(r'^([a-z])', lambda m: m.group(1).upper(), name)
        return re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)

    @staticmethod
    def snake_to_camel(name):
        import re
        # 逻辑：匹配 '_[a-z]' (下划线+小写字母)，将其替换为对应的大写字母（去掉下划线）
        return re.sub(r'_([a-z])', lambda m: m.group(1).upper(), name)


class FileUtil(BaseUtil):

    @staticmethod
    def get_fullname(path):
        try:
            return os.path.basename(path)
        except Exception as e:
            CommonUtil.trace_exception_stack(e)
            return ""

    @staticmethod
    def get_name(path):
        try:
            return os.path.splitext(os.path.basename(path))[0]
        except Exception as e:
            CommonUtil.trace_exception_stack(e)

    @staticmethod
    def get_ext(path):
        try:
            return os.path.splitext(os.path.basename(path))[1]
        except Exception as e:
            CommonUtil.trace_exception_stack(e)

    @staticmethod
    def open(path):
        try:
            return open(path, 'w', encoding='utf-8')
        except Exception as e:
            CommonUtil.trace_exception_stack(e)

    @staticmethod
    def mkdir(path):
        try:
            os.makedirs(path, exist_ok=True)
        except Exception as e:
            CommonUtil.trace_exception_stack(e)


class JsonUtil(BaseUtil):

    @staticmethod
    def stringify(json_obj, default_str=""):
        try:
            return json.dumps(json_obj, ensure_ascii=False, indent=2)
        except Exception as e:
            CommonUtil.trace_exception_stack(e)
            pass
        return default_str

    @staticmethod
    def parse(json_str, default_json={}):
        try:
            return json.loads(json_str)
        except Exception as e:
            # CommonUtil.trace_exception_stack(e)
            pass
        return default_json


class NumberUtil(BaseUtil):
    INT_MAX = 2147483647


class CommonUtil(BaseUtil):

    @staticmethod
    def trace_exception_stack(e):
        if ConstantEnum.is_debug():
            print(f"❌ 错误描述: {str(e)}, 堆栈跟踪:")
            traceback.print_stack()

    @staticmethod
    def polling(
            action: Callable[[], Any],
            check_condition: Callable[[Any], bool],
            on_success: Optional[Callable[[Any], None]] = None,
            on_retry: Optional[Callable[[Any, int], None]] = None,
            on_error: Optional[Callable[[Exception], None]] = None,
            interval: float = 1.0,
            max_attempts: int = 5,
            description: str = "轮询任务"
    ) -> Optional[Any]:
        """
        通用的轮询处理函数

        :param action:
            [必填] 执行动作的回调函数。
            例如：发送 HTTP 请求、查询数据库状态等。
            必须返回一个结果对象供 check_condition 使用。

        :param check_condition:
            [必填] 检查是否结束的回调函数。
            接收 action 的返回值，返回 True 表示“满足结束条件”，False 表示“继续轮询”。
            例如：lambda res: res.get('need_refresh') is False

        :param on_success:
            [可选] 当 check_condition 返回 True 时执行的回调（通常用于记录日志或处理最终数据）。

        :param on_retry:
            [可选] 当需要继续轮询时执行的回调。
            参数：(当前结果, 当前尝试次数)。可用于打印进度。

        :param on_error:
            [可选] 当 action 抛出异常时执行的回调。
            参数：(异常对象)。

        :param interval:
            每次轮询之间的等待时间（秒）。

        :param max_attempts:
            最大尝试次数，防止死循环。

        :param description:
            任务描述，用于日志输出。

        :return:
            如果成功，返回 action 的最后一次返回值；如果超时或失败，返回 None。
        """

        attempts = 0

        ConstantEnum.is_debug() and print(f"🚀 开始执行 [{description}]...")

        while attempts < max_attempts:
            attempts += 1

            try:
                # 1. 执行动作
                result = action()
                last_result = result

                # 2. 检查条件
                if check_condition(result):
                    ConstantEnum.is_debug() and print(
                        f"✅ [{description}] 成功！条件已满足 (尝试次数: {attempts}, 耗时{interval * attempts}秒)")
                    if on_success:
                        on_success(result)
                    return result

                # 3. 条件未满足，准备重试
                if on_retry:
                    on_retry(result, attempts)
                else:
                    # 默认日志行为
                    ConstantEnum.is_debug() and print(
                        f"⏳ [{description}] 条件未满足，{interval}秒后重试... ({attempts}/{max_attempts}, 耗时{interval * attempts}秒)")

                time.sleep(interval)

            except Exception as e:
                # 4. 异常处理
                if on_error:
                    on_error(e)
                else:
                    # 默认错误行为：打印错误并继续
                    logging.error(f"❌ [{description}] 发生异常: {e}")
                    ConstantEnum.is_debug() and print(f"⚠️ [{description}] 遇到错误，{interval}秒后重试...")

                time.sleep(interval)

        # 5. 超时处理
        ConstantEnum.is_debug() and print(f"⚠️ [{description}] 失败：达到最大尝试次数 ({max_attempts})，强制停止。")
        return None

    @staticmethod
    def is_windows():
        return sys.platform.startswith('win')

    @staticmethod
    def is_empty(data):
        # 1. 如果是 None (对应 JSON 的 null)
        if data is None:
            return True

        # 2. 如果是字典或列表，且长度为 0 (对应 {} 或 [])
        if isinstance(data, (dict, list)) and len(data) == 0:
            return True


from datetime import date, datetime


class DatetimeUtil(BaseUtil):
    FORMAT__DATETIME = "%Y-%m-%d %H:%M:%S"

    @staticmethod
    def now_str():
        return DatetimeUtil.format(DatetimeUtil.now())

    @staticmethod
    def today_str():
        return DatetimeUtil.format_date(DatetimeUtil.today())

    @staticmethod
    def now():
        return datetime.now()

    @staticmethod
    def today():
        return DatetimeUtil.now().replace(hour=0, minute=0, second=0, microsecond=0)

    @staticmethod
    def format(date):
        return date.strftime('%Y-%m-%d %H:%M:%S') if type(date) == datetime else date

    @staticmethod
    def format_date(date):
        return date.strftime('%Y-%m-%d') if type(date) == datetime else date

    @staticmethod
    def parse(date_str):
        if type(date_str) == int:
            return datetime.fromtimestamp(date_str)
        return datetime.strptime(date_str, DatetimeUtil.FORMAT__DATETIME) if type(date_str) == str else date_str

    @staticmethod
    def timestamp(date=datetime.now()):
        return int(date.timestamp() * 1000)


class RequestUtil(BaseUtil):
    BASE_URL = ApiEnum.BASE_URL_OPEN_API
    AUTHORIZATION_RETRY_COUNT_MAX = 3
    authorization_retry_count = 0

    @classmethod
    def http_post(cls, url, data=None, params=None, headers=None, *args, **argss):
        return cls.http_request("post", url, data=data, params=params, headers=headers, *args, **argss)

    @classmethod
    def http_put(cls, url, data=None, params=None, headers=None, *args, **argss):
        return cls.http_request("put", url, data=data, params=params, headers=headers, *args, **argss)

    @classmethod
    def http_delete(cls, url, data=None, params=None, headers=None, *args, **argss):
        return cls.http_request("delete", url, data=data, params=params, headers=headers, *args, **argss)

    @classmethod
    def http_get(cls, url, params=None, headers=None, *args, **argss):
        return cls.http_request("get", url, params=params, headers=headers, *args, **argss)

    @classmethod
    def http_request(cls, method, url, data=None, params=None, headers=None, options=None, *args,
                     timeout=ApiEnum.DEFAULT__REQUEST_TIMEOUT, **argss):
        def _get_or_create_user(username):
            _url = ApiEnum.BASE_URL_HEALTH + "/sys/phoneLogin"
            open_id = username
            _data = {
                "silent": 1,
                "register": 1,
                "openId": open_id,
                "mobile": username,
                "source": ConstantEnum.DEFAULT__SKILL_HUB_NAME
            }
            try:
                _response = requests.post(_url, json=_data)
                if _response.status_code == 200:
                    _response_json = _response.json()
                    if _response_json and _response_json.get("success"):
                        return _response_json and _response_json.get("result")
            except Exception as _e:
                CommonUtil.trace_exception_stack(_e)
            return {}

        try:
            headers = headers or {}
            if not url.startswith("https://") and not url.startswith("http://"):
                url = cls.BASE_URL + url
            headers['App-Id'] = ConstantEnum.APP__ID
            # ConstantEnum.CURRENT__USER_NAME = ConstantEnum.CURRENT__OPEN_ID = "ou_86fdd8e0d5f116c18a9dd550abefe6d2"
            current__user_name = ApiEnum.API_SECRET_KEY or ConstantEnum.CURRENT__USER_NAME or ConstantEnum.CURRENT__OPEN_ID
            found_user = None
            if (not ApiEnum.TOKEN or not ApiEnum.OPEN_TOKEN) and current__user_name:
                try:
                    from .dao import UserDao, User
                    user_dao = UserDao()
                    found_user = user_dao.get_by_username(current__user_name)
                    if found_user:
                        ApiEnum.[REDACTED_SECRET]
                        ApiEnum.OPEN_TOKEN = found_user.open_token
                    if not ApiEnum.TOKEN or not ApiEnum.OPEN_TOKEN:
                        new_current_user = _get_or_create_user(current__user_name)
                        if new_current_user:
                            ApiEnum.[REDACTED_SECRET]"token")
                            ApiEnum.OPEN_TOKEN = new_current_user.get("openToken")

                            current_user_info = new_current_user.get("userInfo")
                            if current_user_info:
                                current_user_info["token"] = new_current_user.get("token")
                                current_user_info["openToken"] = new_current_user.get(
                                    "openToken")
                                user_model = User.load(current_user_info)

                                user = user_dao.save(
                                    user_model
                                )

                except Exception as e:
                    CommonUtil.trace_exception_stack(e)
                    raise

            headers.setdefault("X-Access-Token", ApiEnum.TOKEN)
            headers.setdefault("X-Api-Key", ApiEnum.API_SECRET_KEY)
            headers.setdefault("Authorization", ApiEnum.OPEN_TOKEN)

            data = data or {}
            params = params or {}
            options = options or {}
            ConstantEnum.CURRENT__TENTANT_CODE and data.setdefault('tenantCode', ConstantEnum.CURRENT__TENTANT_CODE)
            ConstantEnum.DEFAULT__SKILL_HUB_NAME and data.setdefault('skillHubName',
                                                                     ConstantEnum.DEFAULT__SKILL_HUB_NAME)
            ConstantEnum.DEFAULT__SKILL_PLATFORM_NAME and data.setdefault('skillPlatform',
                                                                          ConstantEnum.DEFAULT__SKILL_PLATFORM_NAME)
            if current__user_name:
                data.setdefault('pnaUserName', current__user_name)

            if bool(options.get("dataAsParams")):
                params.update(data)

            # 安全打印：不打印完整 headers（避免巨大的 token 导致输出缓冲区溢出）
            # 同时处理 v 为 None 的情况，避免 len(None) 报错
            safe_headers = {}
            for k, v in headers.items():
                if v is None:
                    safe_headers[k] = "None"
                elif isinstance(v, (dict, list)):
                    safe_headers[k] = type(v).__name__
                elif len(v) > 30:
                    safe_headers[k] = v[:20] + "..."
                else:
                    safe_headers[k] = v
            ConstantEnum.is_debug() and print(f"🔄 请求拦截, URL:{url}", "method", method, "params", params, "data",
                                              data, "headers", safe_headers,
                                              "options", options,
                                              "timeout",
                                              timeout)
            response = requests.request(method, url, *args, json=data, params=params, headers=headers,
                                        timeout=int(timeout), **argss)
            response_text0 = response.text
            response_text = response_text0 if ConstantEnum.is_debug() else response
            status_code = response.status_code
            if current__user_name == "13800000000":
                status_code = 402
            if status_code == 401 and cls.authorization_retry_count < cls.AUTHORIZATION_RETRY_COUNT_MAX:
                # print(f"❌ 请求拦截, 鉴权失败:{response_text}, url:{url}", "method", method, "params", params,
                #       "data",
                #       data,
                #       "headers",
                #       headers,
                #       "timeout",
                #       timeout)
                ApiEnum.[REDACTED_SECRET] = None
                if found_user:
                    found_user.[REDACTED_SECRET] = None
                    user_dao.update(found_user)
                cls.authorization_retry_count += 1
                return cls.http_request(method, url, data, params, headers, options, *args, timeout=timeout, **argss)
            elif status_code == 402:
                ConstantEnum.is_debug() and print(f"❌ 请求拦截, 使用失败:{response_text}, url:{url}", "method", method,
                                                  "params", params,
                                                  "data",
                                                  data,
                                                  "headers",
                                                  headers,
                                                  "timeout",
                                                  timeout)
                return f'''⚠️ 因账户余额不足, 技能使用失败, 请按照如下步骤进行充值: 
👉 1. 先输入命令 "安装支付技能 smyx-payment", 等待安装完成. (如果已经安装支付技能过则忽略此步骤)
👉 2. 再输入命令 "技能账户充值", 然后跟随系统提示操作后即可继续使用技能.
{response_text0 or ""}
'''
                # f'请先输入命令 "技能账户充值", 请先确确保安装生命涌现支付技能 请先输入命令 "技能账户充值", 跟随系统提示进行充值后即可继续使用技能(). {response_text0 or ""}')
            elif status_code != 200:
                raise requests.exceptions.RequestException(
                    response, response=response)
            response_json = response.json()
            if not bool(response_json['success']):
                raise requests.exceptions.RequestException(
                    response, response=response)
            response_json_data = response_json.get("data", response_json.get("result"))
            response_json_data = response_json_data.get("records") if response_json_data and type(
                response_json_data) == dict and "records" in response_json_data else response_json_data
            ConstantEnum.is_debug() and print(f"✅ 请求拦截, 成功:{response_text}, url:{url}", "method", method,
                                              "params", params,
                                              "data",
                                              data,
                                              "headers",
                                              headers,
                                              "timeout",
                                              timeout)
            return response_json_data
        except Exception as e:
            CommonUtil.trace_exception_stack(e)
            response_text = _.get(e.args, '0.text')
            ConstantEnum.is_debug() and print(
                f"❌ 请求拦截, 失败: {e}, e.response.text: {response_text}, url:{url}",
                "method",
                method,
                "params",
                params,
                "data", data, "headers",
                "response", hasattr(e, 'response') and e.response,
                headers,
                "timeout",
                timeout)
            raise


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal:  | SkillSpector: suspicious (CRITICAL)