Skill: 18072937735/smyx-child-dangerous-behavior-recognition-analysis (version 1.0.3)

--- name: [REDACTED_SECRET] description: "Detects climbing, playing with fire, touching power sources, and dangerous actions near windows, providing real-time alerts. It is suitable for child safety supervision in scenarios such as homes, kindergartens, and nurseries. | 儿童危险行为识别技能，识别攀爬、玩火、触电源、窗边危险动作并实时预警，适用于家庭、幼儿园、托儿所等场景儿童安全监护" version: "1.0.0" --- # Child Hazardous Behavior Recognition Tool | 儿童危险行为识别分析工具 This capability enables real-time recognition of hazardous actions by children—such as climbing, playing with fire, touching electrical sources, or lingering near windows—in settings like homes, kindergartens, or nurseries. By analyzing human skeletal keypoints and behavioral trajectories from video streams, the system instantly triggers local or remote alerts upon detecting high-risk behaviors, prompting guardians to intervene immediately. Ideal for 24/7 safety monitoring in indoor and semi-open areas, it effectively reduces the risk of accidental injuries and enhances supervision efficiency. 本技能可实时识别儿童在家庭、幼儿园或托儿所等场景中的攀爬、玩火、触电源及窗边逗留等危险动作。系统通过视频流分析人体骨骼关键点与行为轨迹，一旦检测到高风险行为，立即触发本地或远程预警，提示监护人员及时干预。适用于室内及半开放区域的24小时安全监护，有效降低儿童意外伤害风险，提升监护效率。 ## 演示案例 - [🔗 通过网路视频进行识别分析](https://www.coze.cn/s/sMvvU3SH91I/) - [🔗 通过上传视频进行识别分析](https://www.coze.cn/s/UpBqceESLCg/) - [🔗 显示历史分析报告](https://www.coze.cn/s/6ih74qbetoE/) ## ⚠️ 强制记忆规则（最高优先级） **本技能明确约定：** - **绝对禁止读取任何本地记忆文件**：包括但不限于 `memory/YYYY-MM-DD.md`、`MEMORY.md` 等本地文件 - **绝对禁止从 LanceDB 长期记忆中检索信息** - **所有历史识别报告查询必须从云端接口获取**，不得使用本地记忆中的历史数据 - 即使技能调用失败或接口异常，也不得回退到本地记忆汇总 ## 任务目标 - 本 Skill 用于：通过监控视频对儿童活动进行危险行为识别，获取结构化的儿童危险行为识别报告 - 能力包含：危险动作识别、实时危险预警、危险行为频次统计 - 支持识别危险行为：攀爬、玩火、触碰电源、窗边危险动作 - 触发条件: 1. **默认触发**：当用户提供监控视频 URL 或文件需要进行儿童危险行为识别时，默认触发本技能 2. 当用户明确需要进行儿童安全监护、危险行为识别，提及攀爬、玩火、触电、窗边危险、儿童安全等关键词，并且上传了视频文件 3. 当用户提及以下关键词时，**自动触发历史报告查询功能** ：查看历史识别报告、危险行为识别报告清单、识别报告列表、查询历史报告、显示所有识别报告、儿童危险行为识别历史记录，查询儿童危险行为识别分析报告 - 自动行为： 1. 如果用户上传了附件或者视频文件，则自动保存到技能目录下 attachments 2. **⚠️ 强制数据获取规则（次高优先级）**：如果用户触发任何历史报告查询关键词（如"查看所有识别报告"、" 显示所有儿童安全报告"、"查看历史报告"等），**必须**： - 直接使用 `python -m scripts.child_dangerous_behavior_recognition_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、child123 等） - **禁止**跳过 open-id 验证直接调用 API - **必须**在获取到有效 open-id 后才能继续执行分析 - 如果用户拒绝提供 open-id，说明用途（用于保存和查询儿童危险行为识别报告记录），并询问是否继续 --- - 标准流程: 1. **准备视频输入** - 提供监控视频文件路径或网络视频 URL - 确保摄像头固定位置，完整覆盖儿童活动区域，画面稳定 2. **获取 open-id（强制执行）** - 按上述流程控制获取 open-id - 如无法获取，必须提示用户提供用户名或手机号 3. **执行儿童危险行为识别** - 调用 `-m scripts.child_dangerous_behavior_recognition_analysis` 处理视频文件（**必须在技能根目录下运行脚本**） - 参数说明: - `--input`: 本地视频文件路径（使用 multipart/form-data 方式上传） - `--url`: 网络视频 URL 地址（API 服务自动下载） - `--alert-threshold`: 危险行为预警阈值，高于该分值发出预警，默认 0.6 - `--open-id`: 当前用户的 open-id（必填，按上述流程获取） - `--list`: 显示儿童危险行为识别历史分析报告列表清单（可以输入起始日期参数过滤数据范围） - `--api-key`: API 访问密钥（可选） - `--api-url`: API 服务地址（可选，使用默认值） - `--detail`: 输出详细程度（basic/standard/json，默认 json） - `--output`: 结果输出文件路径（可选） 4. **查看分析结果** - 接收结构化的儿童危险行为识别报告 - 包含：监测基本信息、各类危险行为统计、危险预警提示 ## 资源索引 - 必要脚本：见 [scripts/child_dangerous_behavior_recognition_analysis.py](scripts/child_dangerous_behavior_recognition_analysis.py)( 用途：调用 API 进行儿童危险行为识别，本地文件使用 multipart/form-data 方式上传，网络 URL 由 API 服务自动下载) - 配置文件：见 [scripts/config.py](scripts/config.py)(用途：配置 API 地址、默认参数和视频格式限制) - 领域参考：见 [references/api_doc.md](references/api_doc.md)(何时读取：需要了解 API 接口详细规范和错误码时) ## 注意事项 - 仅在需要时读取参考文档，保持上下文简洁 - 视频要求：支持 mp4/avi/mov 格式，最大 100MB - 本技能仅作儿童安全监护辅助，发现危险预警请立即确认 - API 密钥可选，如果通过参数传入则必须确保调用鉴权成功，否则忽略鉴权 - 禁止临时生成脚本，只能用技能本身的脚本 - 传入的网络地址参数，不需要下载本地，默认地址都是公网地址，api 服务会自动下载 - 当显示历史识别报告清单的时候，从数据 json 中提取字段 reportImageUrl 作为超链接地址，使用 Markdown 表格格式输出，包含" 报告名称"、"识别时间"、"危险次数"、"点击查看"四列，其中"报告名称"列使用`儿童危险行为识别报告-{记录id}`形式拼接, "点击查看" 列使用 `[🔗 查看报告](reportImageUrl)` 格式的超链接，用户点击即可直接跳转到对应的完整报告页面。 - 表格输出示例： | 报告名称 | 识别时间 | 危险次数 | 点击查看 | |----------|----------|----------|----------| | 儿童危险行为识别报告-20260312172200001 | 2026-03-12 17:22:00 | 2 | [🔗 查看报告](https://example.com/report?id=xxx) | ## 使用示例 ```bash # 分析本地监控视频（以下只是示例，禁止直接使用openclaw-control-ui 作为 open-id） python -m scripts.child_dangerous_behavior_recognition_analysis --input /path/to/monitor.mp4 --alert-threshold 0.6 --open-id openclaw-control-ui # 分析网络监控视频，调整预警阈值（以下只是示例，禁止直接使用openclaw-control-ui 作为 open-id） python -m scripts.child_dangerous_behavior_recognition_analysis --url https://example.com/monitor.mp4 --alert-threshold 0.5 --open-id openclaw-control-ui # 显示历史识别报告/显示识别报告清单列表/显示历史儿童危险行为报告（自动触发关键词：查看历史识别报告、历史报告、识别报告清单等） python -m scripts.child_dangerous_behavior_recognition_analysis --list --open-id openclaw-control-ui # 输出精简报告 python -m scripts.child_dangerous_behavior_recognition_analysis --input video.mp4 --open-id your-open-id --detail basic # 保存结果到文件 python -m scripts.child_dangerous_behavior_recognition_analysis --input video.mp4 --open-id your-open-id --output result.json ```

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

此处用于存放宠物健康分析 API 的接口文档，待后续补充。

## 接口规范

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

## 主要接口

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

## 场景代码

- `OPEN_PET_HEALTH_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 = {
            "data_as_params": 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={"data_as_params": True})


path: scripts/child_dangerous_behavior_recognition_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

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


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 SUPPORTED_FORMATS:
        raise ValueError(f"不支持的文件格式，支持的格式: {', '.join(SUPPORTED_FORMATS)}")

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

    return True


def analyze_video(input_path=None, url=None, alert_threshold=None, api_url=None, api_key=None, output_level=None):
    """调用API进行儿童危险行为识别"""
    if not input_path and not url:
        raise ValueError("必须提供本地视频路径(--input)或网络视频URL(--url)")

    # 设置参数
    if alert_threshold:
        ConstantEnum.DEFAULT_ALERT_THRESHOLD = alert_threshold

    try:
        input_path = input_path or url
        # 携带额外参数
        params = {}
        if alert_threshold:
            params["alert_threshold"] = alert_threshold
        return skill.get_output_analysis(input_path, params)

    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):
    # if not open_id:
    #     raise ValueError("必须提供本用户的OpenId/UserId")

    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 get_analysis_export_url(request_id=None):
    """调用API分析视频"""
    if not request_id:
        return ""
    return ApiEnum.DETAIL_EXPORT_URL + request_id


def format_result(result, output_level="standard", alert_threshold=0.6):
    """格式化输出结果"""
    danger_map = {
        "climbing": "攀爬",
        "playing_with_fire": "玩火",
        "touch_power": "触碰电源",
        "window_danger": "窗边危险动作"
    }

    if output_level == "json":
        result_id = None
        if result is not None:
            result_json = result
            result_id = result_json.get('id', {})
            result_json = json.dumps(result_json.get('dangerBehaviorResponse', {}), ensure_ascii=False, indent=2)
        else:
            return "⚠️ 暂无分析结果"
        return f"""
📊 儿童危险行为识别分析结构化结果
{result_json}
""", result_id
    elif output_level == "basic":
        # 精简输出
        data = result.get('data', {})
        detection = data.get('detection', {})
        return f"""
📊 儿童危险行为识别报告
{'=' * 40}
预警阈值: {alert_threshold}
危险行为次数: {detection.get('total_danger_count', 0)}
是否存在高危行为: {'⚠️ 是' if detection.get('has_high_risk') else '✅ 否'}
        """
    elif output_level == "standard":
        # 标准输出
        data = result.get('data', {})
        detection = data.get('detection', {})

        danger_stats = "\n".join([
            f"  ⚠️ {danger_map.get(d.get('behavior'), d.get('behavior'))}: {d.get('count', 0)} 次，最高置信度: {d.get('max_confidence', 0)}"
            for d in detection.get('danger_stats', [])])
        warnings = "\n".join([f"  🚨  {item}" for item in data.get('alert_messages', [])])

        return f"""
📊 儿童危险行为识别分析报告
{'=' * 50}
⏰ 识别时间: {data.get('detection_time', '未知')}
🚨 预警阈值: {alert_threshold}

🔍 识别结果:
  危险行为总次数: {detection.get('total_danger_count', 0)}
  是否存在高危行为: {'⚠️ 是' if detection.get('has_high_risk') else '✅ 否'}

  危险行为统计:
{danger_stats if danger_stats else '  ✅ 未检测到危险行为'}

🚨 危险预警提示:
{warnings if warnings else '  无危险预警'}
{'=' * 50}
> 注：本技能仅作儿童安全监护辅助，发现预警请立即确认现场情况。
        """
    else:
        # 完整输出（JSON格式）
        return json.dumps(result, ensure_ascii=False, indent=2)


def main():
    parser = argparse.ArgumentParser(description="儿童危险行为识别工具")
    parser.add_argument("--input", help="本地视频文件路径")
    parser.add_argument("--url", help="网络视频的URL地址")
    parser.add_argument("--alert-threshold", type=float, default=ConstantEnum.DEFAULT__ALERT_THRESHOLD,
                        help="危险行为预警阈值，高于该分值发出预警，默认 0.6")
    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,
            alert_threshold=args.alert_threshold,
            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/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.face_analysis.scripts.config import ApiEnum as ApiEnumParent, ConstantEnum as ConstantEnumParent, \
    SceneCodeEnum, ApiEnumCommonAiMixin


class ApiEnum(ApiEnumCommonAiMixin, ApiEnumParent):
    pass


class ConstantEnum(ConstantEnumParent):
    DEFAULT__ALERT_THRESHOLD = 0.6

    @classmethod
    def init(cls, config=None):
        super().init(config)
        ConstantEnumParent.DEFAULT__SCENE_CODE = SceneCodeEnum.CHILD_DANGEROUS_BEHAVIOR_RECOGNITION_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.face_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/face_analysis/README.md
content: # 中医面诊分析工具 (face-analysis)

## 技能介绍
这是一个基于AI的中医面诊分析技能，可以通过面部视频自动分析健康状况，返回结构化的诊断结果和养生建议。

## 快速开始
### 1. 配置API信息
编辑 `scripts/config.py`，设置你的API地址和密钥：
```python
DEFAULT_API_URL = "https://your-api-server.com/api/v1/face-analysis"
DEFAULT_API_KEY = "your-api-key-here"
```

### 2. 分析本地视频
```bash
python scripts/face_analysis.py --input /path/to/your/video.mp4
```

### 3. 分析网络视频
```bash
python scripts/face_analysis.py --url https://example.com/video.mp4
```

## 功能特性
- ✅ 支持本地MP4视频上传
- ✅ 支持网络视频URL分析
- ✅ 三种输出详细程度：精简/标准/完整
- ✅ 结构化JSON结果输出
- ✅ 自动保存结果到文件
- ✅ 内置视频格式和大小校验

## 目录结构
```
face-analysis/
├── SKILL.md              # 技能说明文件（系统自动加载）
├── README.md             # 本说明文件
├── scripts/
│   ├── face_analysis.py  # 主程序
│   └── config.py         # 配置文件
├── references/
│   ├── api_doc.md        # API接口文档
│   ├── tcm_theory.md     # 中医面诊理论参考
│   └── faq.md            # 常见问题
└── assets/
    └── template.json     # 返回结果模板
```

## 使用示例
### 标准输出
```
📊 中医面诊分析报告
==================================================
⏰ 分析时间: 2026-03-10 15:30:00
🎯 人脸检测: success (置信度: 95分)

🔍 诊断结果:
  整体体质: 平和质
  脏腑状况:
    liver: 正常
    heart: 轻微火旺
    spleen: 略虚
    lung: 正常
    kidney: 正常
  面色分析: 微黄
  对应提示: 脾胃功能略弱

⚠️ 健康警示:
  ⚠️  注意休息，避免熬夜

💡 养生建议:
  💡 饮食清淡，减少辛辣食物摄入
  💡 保持规律作息，每晚11点前入睡
  💡 适当进行有氧运动，如散步、太极拳
==================================================
```

### 输出到JSON文件
```bash
python scripts/face_analysis.py --input video.mp4 --detail full --output result.json
```

## 注意事项
1. 视频要求：清晰正面面部，光线充足，时长5-30秒为宜
2. 支持格式：mp4、avi、mov，最大100MB
3. API需要自行部署或接入第三方服务
4. 结果仅供参考，不能替代专业医生诊断


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

## 接口地址
`POST https://your-api-server.com/api/v1/face-analysis`

## 请求头
| 字段 | 必选 | 说明 |
|------|------|------|
| X-API-Key | 是 | API访问密钥 |
| Content-Type | 是 | multipart/form-data（文件上传）或 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/face_analysis/requirements.txt
content: pydash==8.0.6
SQLAlchemy==2.0.46
yaml==6.0.3


path: skills/face_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/face_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 = "/web/health-analysis/v2/start-health-analysis"

    ANALYSIS_RESULT_URL = "/web/health-analysis/get-health-analysis-result"

    PAGE_URL = "/web/health-analysis/page-health-analysis-result"

    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/face_analysis/scripts/config.yaml

path: skills/face_analysis/scripts/face_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 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 SUPPORTED_FORMATS:
        raise ValueError(f"不支持的文件格式，支持的格式: {', '.join(SUPPORTED_FORMATS)}")

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

    return True


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):
    # if not open_id:
    #     raise ValueError("必须提供本用户的OpenId/UserId")

    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 get_analysis_export_url(request_id=None):
    """调用API分析视频"""
    if not request_id:
        return ""
    return ApiEnum.DETAIL_EXPORT_URL + request_id


def format_result(result, output_level="standard"):
    """格式化输出结果"""
    if output_level == "json":
        result_id = None
        # if result.get('success'):
        if result is not None:
            result_json = result
            result_id = result_json.get('id', {})
            result_json = json.dumps(result_json.get('faceAnalysisResponse', {}), ensure_ascii=False, indent=2)
        else:
            # result_json = json.dumps(result, ensure_ascii=False, indent=2)
            return "⚠️ 暂无分析结果"
        return f"""
📊 面诊分析结构化结果
{result_json}
""", result_id
    elif output_level == "basic":
        # 精简输出
        data = result.get('data', {})
        diagnosis = data.get('diagnosis', {})
        return f"""
📊 面诊分析结果
{'=' * 40}
整体体质: {diagnosis.get('overall_constitution', '未知')}
主要状况: {', '.join([f'{k}: {v}' for k, v in diagnosis.get('organ_condition', {}).items() if v != '正常'])}
健康提示: {data.get('health_warnings', ['无特殊警示'])[0] if data.get('health_warnings') else '无特殊警示'}
        """
    elif output_level == "standard":
        # 标准输出
        data = result.get('data', {})
        diagnosis = data.get('diagnosis', {})
        face_detection = data.get('face_detection', {})

        organ_status = "\n".join([f"  {k}: {v}" for k, v in diagnosis.get('organ_condition', {}).items()])
        warnings = "\n".join([f"  ⚠️  {item}" for item in data.get('health_warnings', [])])
        suggestions = "\n".join([f"  💡 {item}" for item in data.get('suggestions', [])])

        return f"""
📊 中医面诊分析报告
{'=' * 50}
⏰ 分析时间: {data.get('analysis_time', '未知')}
🎯 人脸检测: {face_detection.get('status', '未知')} (置信度: {face_detection.get('quality_score', 0)}分)

🔍 诊断结果:
  整体体质: {diagnosis.get('overall_constitution', '未知')}
  脏腑状况:
{organ_status}
  面色分析: {diagnosis.get('color_analysis', {}).get('complexion', '未知')}
  对应提示: {diagnosis.get('color_analysis', {}).get('correspondence', '未知')}

⚠️ 健康警示:
{warnings}

💡 养生建议:
{suggestions}
{'=' * 50}
        """
    else:
        # 完整输出（JSON格式）
        return json.dumps(result, ensure_ascii=False, indent=2)


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/face_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 result_json_common_ai_response:
            result_json = result_json_common_ai_response

        result_json_health_ai_response = result_json.get("healthAiResponse")
        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()

            # 构建 multipart/form-data 格式的请求
            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_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
pycparser==3.0
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
tqdm==4.67.1
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"
        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() != '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"
        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"
        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 skills.smyx_common.scripts.api_service import ApiService as ApiServiceBase
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 = {}

            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

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

        while attempts < max_attempts:
            attempts += 1

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

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

                # 3. 条件未满足，准备重试
                if on_retry:
                    on_retry(result, attempts)
                else:
                    # 默认日志行为
                    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}")
                    print(f"⚠️ [{description}] 遇到错误，{interval}秒后重试...")

                time.sleep(interval)

        # 5. 超时处理
        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 导致输出缓冲区溢出）
            safe_headers = {k: (v[:20] + "..." if len(v) > 30 else v) for k, v in headers.items()}
            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:
                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
            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')
            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: clean | SkillSpector: suspicious (CRITICAL)