Skill: 18072937735/smyx-adaptive-pet-drying-temperature-analysis (version 1.0.0)

--- name: [REDACTED_SECRET] description: "Triggers when a user provides a full-body image/video of a pet (multi-angle preferred) for analysis; supports local uploads or network URLs to call server-side APIs for breed/body-type recognition and fur-density estimation (sparse / medium / dense), then outputs a recommended drying temperature curve (temperature in ℃ + time in minutes) for personalized care to reduce scald risk (not a medical recommendation). Application scenarios: pet drying boxes, pet grooming salons, smart pet care devices. | 宠物烘干温度自适应推荐工具，当用户提供宠物全身图像/视频（多角度最佳）时，触发本技能进行品种识别与毛发密度估算（稀疏/中等/浓密），输出个性化烘干温度曲线（温度 ℃ + 时长 分钟）参数，实现个性化护理，减少烫伤风险（不提供医疗建议）。应用场景：宠物烘干箱、宠物美容店、智能宠物护理设备。" version: "1.0.0" --- # Pet Adaptive Drying Temperature Recommendation | 宠物烘干温度自适应推荐 ## 🎯 AI 角色 **你是一个专业的宠物护理AI。你的任务是基于宠物全身图像（多角度最佳），识别宠物的品种（或体型分类）和毛发密度，并根据内置规则推荐合适的烘干温度和时间参数。不要提供医疗建议，仅输出基于视觉特征的推荐值。** ## 任务目标 - 本 Skill 用于：通过宠物全身图像/视频识别品种与毛发密度，输出个性化烘干温度曲线（温度 + 时长），实现个性化护理，减少烫伤风险 - 能力包含：图像/视频分析、品种识别（或体型分类）、毛发密度估算（稀疏 / 中等 / 浓密）、毛长估算（短毛/中长毛/长毛）、烘干温度推荐（℃）、烘干时长推荐（分钟）、温度曲线生成、安全护理提示 - 触发条件: 1. **默认触发**：当用户提供宠物全身图像/视频 URL 或文件需要分析时，默认触发本技能进行烘干温度推荐 2. 当用户明确需要进行宠物烘干推荐时，提及烘干箱、吹水机、宠物烘干、烘干温度、烘干曲线、宠物美容、毛发护理、防烫伤等关键词，并且上传了图像/视频文件 3. 当用户提及以下关键词时，**自动触发历史报告查询功能** ：查看历史烘干推荐报告、历史烘干曲线报告、烘干推荐报告清单、查询烘干记录、显示所有宠物烘干报告、显示烘干温度推荐报告，查询宠物毛发护理报告 - 自动行为： 1. 如果用户上传了附件或者图像/视频文件，则自动保存为本地文件 2. **⚠️ 强制数据获取规则（次高优先级）**：如果用户触发任何历史报告查询关键词（如"查看所有烘干推荐报告"、"显示所有烘干曲线报告"、"查看历史报告"等），**必须**： - 直接使用 `python -m scripts.smyx_adaptive_pet_drying_temperature_analysis --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、userC113、user123 等） - **禁止**跳过 open-id 验证直接调用 API - **必须**在获取到有效 open-id 后才能继续执行分析 - 如果用户拒绝提供 open-id，说明用途（用于保存和查询历史报告记录），并询问是否继续 --- - 标准流程: 1. **准备图像/视频输入** - 提供本地图像/视频文件路径或网络 URL - 确保画面清晰展示宠物**完整全身**（含头部、躯干、四肢、尾巴），多角度图像识别更准 - 光线充足、无明显遮挡、宠物毛发干燥或湿润均可（但需可见毛发纹理） 2. **获取 open-id（强制执行）** - 按上述流程控制获取 open-id - 如无法获取，必须提示用户提供用户名或手机号 3. **执行烘干温度自适应推荐分析** - 调用 `-m scripts.smyx_adaptive_pet_drying_temperature_analysis` 处理图像/视频文件（**必须在技能根目录下运行脚本**） - 参数说明: - `--input`: 本地图像/视频文件路径 - `--url`: 网络图像/视频 URL 地址（API 服务自动下载） - `--pet-type`: 宠物类型，可选值：cat/dog/other，默认 dog - `--open-id`: 当前用户的 open-id（必填，按上述流程获取） - `--list`: 显示烘干温度推荐历史分析报告列表清单（可输入起始日期参数过滤数据范围） - `--api-key`: API 访问密钥（可选） - `--api-url`: API 服务地址（可选，使用默认值） - `--detail`: 输出详细程度（basic/standard/json，默认 json） - `--output`: 结果输出文件路径（可选） 4. **查看分析结果** - 接收结构化的烘干温度推荐报告 - 包含：识别品种 / 体型分类（小型/中型/大型）、毛长（短毛/中长毛/长毛）、毛发密度（稀疏/中等/浓密）、推荐烘干温度（℃）、推荐烘干时长（分钟）、温度曲线（分阶段温度+时长）、防烫伤护理提示 - **重要提示**：仅基于视觉特征输出推荐值，不提供医疗建议 ## 烘干温度推荐参考曲线 | 毛发密度 / 毛长 | 短毛 | 中长毛 | 长毛 | |----------------|------|--------|------| | 稀疏 | 38~42 ℃ / 10~15 min | 40~45 ℃ / 15~20 min | 42~48 ℃ / 20~25 min | | 中等 | 40~45 ℃ / 15~20 min | 42~48 ℃ / 20~30 min | 45~50 ℃ / 30~40 min | | 浓密 | 42~48 ℃ / 20~25 min | 45~50 ℃ / 30~40 min | 48~55 ℃ / 40~60 min | > 注：上表仅为参考默认值，最终推荐值以 API 输出曲线为准。 > ⚠️ 安全上限：常规烘干温度不应超过 **60 ℃**；幼宠、老龄宠、扁鼻品种（法斗、巴哥、加菲等）应在常规推荐基础上 **下调 3~5 ℃** 并延长时间。 > 推荐温度曲线可直接由智能烘干箱/吹水机消费，实现分阶段降温护理。 ## 资源索引 - 必要脚本：见 [scripts/smyx_adaptive_pet_drying_temperature_analysis.py](scripts/smyx_adaptive_pet_drying_temperature_analysis.py)(用途：调用 API 进行品种识别与毛发密度估算并输出烘干温度推荐曲线，本地文件上传，网络 URL 由 API 服务自动下载) - 配置文件：见 [scripts/config.py](scripts/config.py)(用途：配置 API 地址、默认参数和场景代码) - 领域参考：见 [references/api_doc.md](references/api_doc.md)(何时读取：需要了解 API 接口详细规范和错误码时) ## 注意事项 - 仅在需要时读取参考文档，保持上下文简洁 - 文件要求：支持 jpg/jpeg/png/bmp/webp 图像 与 mp4/avi/mov 视频，最大 10MB，建议多角度全身画面 - API 密钥可选，如果通过参数传入则必须确保调用鉴权成功，否则忽略鉴权 - 推荐结果仅供烘干设备护理参考，不提供医疗建议 - 禁止临时生成脚本，只能用技能本身的脚本 - 传入的网络地址参数，不需要下载本地，默认地址都是公网地址，API 服务会自动下载 - 推荐曲线由设备端（智能烘干箱 / 吹水机）消费，实现动态风温/风速调节，防止烫伤 - 幼宠、老龄宠、扁鼻品种（法斗、巴哥、加菲等）应在常规推荐基础上下调 3~5 ℃ 并延长烘干时间，AI 角色在输出时需主动提醒 - 当显示历史分析报告清单的时候，从接口返回 json 数据中提取字段 `reportImageUrl` 作为超链接地址，且自动转化为如下 Markdown 表格格式输出，包含" 报告名称"、"宠物类型"、"分析时间"、"点击查看"四列，其中"报告名称"列使用`烘干温度推荐报告-{记录id}`形式拼接, "点击查看" 列使用 `[🔗 查看报告](reportImageUrl)` 格式的超链接，用户点击即可直接跳转到对应的完整报告页面。 - 表格输出示例： | 报告名称 | 宠物类型 | 分析时间 | 点击查看 | |----------|----------|----------|----------| | 烘干温度推荐报告-20260522021800001 | 狗 | 2026-05-22 02:18:00 | [🔗 查看报告](https://example.com/report?id=xxx) | ## 使用示例 ```bash # 分析本地宠物全身图像/视频（以下只是示例，禁止直接使用 openclaw-control-ui 作为 open-id） python -m scripts.smyx_adaptive_pet_drying_temperature_analysis --input /path/to/pet_fullbody.jpg --pet-type dog --open-id your-open-id # 分析网络宠物全身图像/视频（以下只是示例，禁止直接使用 openclaw-control-ui 作为 open-id） python -m scripts.smyx_adaptive_pet_drying_temperature_analysis --url https://example.com/pet_fullbody.mp4 --pet-type dog --open-id your-open-id # 显示历史分析报告清单（自动触发关键词：查看历史烘干推荐报告、烘干曲线报告清单等） python -m scripts.smyx_adaptive_pet_drying_temperature_analysis --list --open-id your-open-id # 输出精简报告 python -m scripts.smyx_adaptive_pet_drying_temperature_analysis --input pet_fullbody.jpg --pet-type dog --open-id your-open-id --detail basic # 保存结果到文件 python -m scripts.smyx_adaptive_pet_drying_temperature_analysis --input pet_fullbody.jpg --pet-type dog --open-id your-open-id --output result.json ```

path: _meta.json
content: {
  "slug": "smyx-adaptive-pet-drying-temperature-analysis",
  "version": "1.0.0",
  "dependencies": {
    "skills": [
      "smyx-common",
      "risk-analysis"
    ]
  }
}


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

此处用于存放宠物烘干温度自适应推荐 API 的接口文档，待后续补充。

## 接口规范

- 基础地址：由 smyx_common 配置统一管理
- 认证方式：API Key 鉴权
- 响应格式：JSON

## 主要接口

1. `/web/ai-analysis/v2/start-common-ai-analysis` - 启动品种+毛发密度识别与烘干曲线推荐任务
2. `/web/ai-analysis/get-common-ai-analysis-result` - 获取分析结果
3. `/web/ai-analysis/page-common-ai-analysis-result` - 分页查询历史报告
4. `/health/order/api/getReportDetailExport?id={id}` - 导出完整报告

## 场景代码

- `SMYX_ADAPTIVE_PET_DRYING_TEMPERATURE_ANALYSIS` - 宠物烘干温度自适应推荐

## 设备联动

- 推荐结果包含温度（℃）与时长（分钟）曲线，可直接被智能烘干箱/吹水机消费
- 设备侧依据推荐曲线动态调节风温/风速，防止烫伤


path: scripts/__init__.py
content: # Pet Adaptive Drying Temperature Recommendation 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, \
    SceneCodeEnum, ApiEnumCommonAiMixin


class ApiEnum(ApiEnumCommonAiMixin, ApiEnumParent):
    pass


class ConstantEnum(ConstantEnumParent):
    DEFAULT__PET_TYPE = "dog"

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


path: scripts/config.yaml

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: scripts/smyx_adaptive_pet_drying_temperature_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

from skills.smyx_common.scripts.util import RequestUtil


def analyze_video(input_path=None, url=None, pet_type=None, api_url=None, api_key=None, output_level=None):
    if pet_type:
        ConstantEnum.DEFAULT__PET_TYPE = pet_type

    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("--pet-type", choices=["cat", "dog", "other"], default=ConstantEnum.DEFAULT__PET_TYPE,
                        help="宠物类型：cat(猫), dog(狗), other(其他)，默认 dog")
    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_video(
            input_path=args.input,
            url=args.url,
            pet_type=args.pet_type,
            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_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_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)