Skill: liangdabiao/amazon-research-reviews-skill (version 1.0.0)

--- name: simple-review-analyzer description: | AI驱动的电商评论深度分析工具，支持22维度智能标签、用户画像识别、VOC洞察和可视化看板生成。 当用户需要以下功能时触发： - 分析电商产品评论（Amazon/eBay/AliExpress等平台） - 从评论中提取用户画像、痛点和VOC（客户之声） - 生成产品洞察报告和机会点分析 - 创建专业的可视化分析看板 - 进行竞品分析和市场定位研究 触发关键词：电商评论分析、评论分析、竞品分析、用户洞察、VOC分析、产品优化、市场调研、评论数据挖掘 AI Agent 约束：必须通过 AskUserQuestion 收集分析数量后再执行分析 license: MIT allowed-tools: - AskUserQuestion - Bash - Read - Write - Edit --- # Simple Review Analyzer AI驱动的电商评论深度分析工具，提供22维度智能标签、用户画像识别和专业的可视化看板。 ## 核心特性 ### 22维度智能标签系统 全面覆盖评论信息的8大维度： - **人群维度 (4)**: 性别、年龄段、职业、购买角色 - **场景维度 (1)**: 使用场景 - **功能维度 (2)**: 满意度、具体功能 - **质量维度 (3)**: 材质、做工、耐用性 - **服务维度 (5)**: 发货速度、包装质量、客服响应、退换货、保修 - **体验维度 (4)**: 舒适度、易用性、外观设计、价格感知 - **市场维度 (2)**: 竞品对比、复购意愿 - **情感维度 (1)**: 总体评价 ### 三位一体输出 1. **CSV标签数据**: 原始评论 + 22维度 AI 标签 2. **Markdown洞察报告**: 战略机会点、痛点、优化建议 3. **HTML可视化看板**: 6个交互式图表、黑金设计 ### 四位一体VOC系统 - 用户画像识别（3-4类典型用户） - 黄金样本（每类6条精选评论） - 3D头像系统（6种风格） - 情感分析（分布与关键词） ## 快速开始 ### 首次使用 1. 准备 CSV 文件（支持自动模糊匹配列名） 2. 在 Claude Code 中调用： ``` 请分析这个产品的评论：reviews.csv ``` ### CSV 文件格式要求 **必需列**（自动模糊匹配）： - 评论内容：内容/评价/body/review/text/comment - 评分：打分/rating/score/star **可选列**： - 时间：时间/date/日期/time - 标题：标题/title/summary - 用户名：用户/user/username ## 核心工作流程 ### 第一步：收集参数 使用 AskUserQuestion 收集： - 分析数量（100条/300条/全部） ### 第二步：执行分析 1. 读取并解析 CSV 文件 2. 批量标签提取（每批最多30条） 3. 统计分析与用户画像识别 4. 生成洞察报告 5. 生成 HTML 可视化看板 ### 第三步：展示结果 在 `output/` 目录下按**产品名_日期**创建文件夹，生成三种报告： ``` output/ ├── 产品A_20260320/ │ ├── reviews_labeled.csv │ ├── 分析洞察报告.md │ └── 可视化洞察报告.html ├── 产品B_20260321/ │ ├── reviews_labeled.csv │ ├── 分析洞察报告.md │ └── 可视化洞察报告.html └── ... ``` **命名规则**： - 文件夹：`{产品名}_{日期YYYYMMDD}` - CSV：`reviews_labeled.csv` - Markdown：`分析洞察报告.md` - HTML：`可视化洞察报告.html` ## 技术架构 ### 文件结构 ``` simple-review-analyzer/ ├── skill.md # Skill 定义文件 ├── prompts/ # 提示词模板 │ ├── tagging.txt # 单条评论打标 │ ├── tagging_batch.txt # 批量打标 │ └── insights.txt # 洞察报告生成 ├── templates/ # 输出模板 │ └── report.html # HTML 可视化模板 └── utils/ # 工具脚本 ├── transform_logic.py # JSON↔CSV双向转换工具 └── csv_reader.sh # CSV 读取辅助 输出文件：生成到 output/{产品名}_{日期}/ 目录 ``` ### 提示词模板 - **单条打标**: 基于 prompts/tagging.txt，分析单条评论返回22维度标签 - **批量打标**: 基于 prompts/tagging_batch.txt，一次处理最多30条评论 - **洞察报告**: 基于 prompts/insights.txt，生成深度分析报告 ## 使用场景 ### 场景1：产品优化 分析自己产品的评论，发现用户痛点，优化产品功能和设计。 ### 场景2：竞品分析 分析竞品评论，了解竞争对手的优势和劣势，寻找差异化机会。 ### 场景3：市场调研 批量分析多个产品的评论，了解市场需求、用户偏好和行业趋势。 ### 场景4：用户洞察 深度了解目标用户群体，构建精准用户画像，优化营销策略。 ## 输出文件说明 ### CSV 标签数据 - 原始评论数据（保留所有原始列） - 22维度 AI 标签列（新增） - 用途：数据分析、导入 BI 工具、筛选过滤 ### Markdown 洞察报告 - 执行摘要：分析概述、关键发现 - 战略机会点：市场机会、产品差异化 - 用户痛点：问题分类、改进建议 - 用户画像：典型用户类型、需求特点 - VOC 分析：黄金样本、情感分布 ### HTML 可视化看板 - 黑金奢华设计风格 - 6个交互式 Chart.js 图表 - 响应式设计，支持移动端 - 纯 HTML 文件，可直接浏览器打开 ## 工具使用 ### transform_logic.py - 数据格式转换工具 位置：`utils/transform_logic.py` **功能**：JSON（嵌套结构）与 CSV（扁平格式）之间的双向转换 **使用方法**： ```bash # JSON → CSV（扁平化输出，22维度标签展开为独立列） python3 .claude/skills/simple-review-analyzer/utils/transform_logic.py json \ tagged_reviews.json \ reviews_labeled.csv # CSV → JSON（恢复嵌套结构，22维度标签聚合为tags对象） python3 .claude/skills/simple-review-analyzer/utils/transform_logic.py csv \ reviews_labeled.csv \ tagged_reviews.json ``` **数据结构对比**： | JSON (嵌套) | CSV (扁平) | |-------------|------------| | `tags.人群_性别` | `人群_性别` | | `tags.场景_使用场景` | `场景_使用场景` | | `tags.功能_满意度` | `功能_满意度` | | ... | ... | **应用场景**： - 将AI分析结果导出为Excel/BI工具兼容格式 - 从已有标签数据恢复嵌套JSON结构 - 数据格式验证和转换 ## 注意事项 1. **分析数量**: 推荐选择 100 条评论以平衡速度与质量 2. **并发限制**: 每批最多处理 30 条评论 3. **输出目录**: `output/{产品名}_{日期}/` （自动创建） 4. **文件命名**: 固定文件名，便于对比同一产品的多次分析 5. **编码支持**: UTF-8/GBK/GB2312 自动检测 6. **工具使用**: 可单独使用 `transform_logic.py` 进行数据格式转换

path: README.md
content: # Review Analyzer Skill

AI驱动的电商评论深度分析工具，支持22维度智能标签、用户画像识别、VOC洞察和可视化看板生成。

## 特性

### 22维度智能标签系统
全面覆盖评论信息的8大维度：
- **人群维度 (4)**: 性别、年龄段、职业、购买角色
- **场景维度 (1)**: 使用场景
- **功能维度 (2)**: 满意度、具体功能
- **质量维度 (3)**: 材质、做工、耐用性
- **服务维度 (5)**: 发货速度、包装质量、客服响应、退换货、保修
- **体验维度 (4)**: 舒适度、易用性、外观设计、价格感知
- **市场维度 (2)**: 竞品对比、复购意愿
- **情感维度 (1)**: 总体评价

### 三位一体输出
1. **CSV标签数据**: 原始评论 + 22维度 AI 标签
2. **Markdown洞察报告**: 战略机会点、痛点、优化建议
3. **HTML可视化看板**: 交互式图表、黑金设计

## 快速开始

### 安装

```bash
# 克隆到 Claude Code skills 目录
cp -r review-analyzer-skill ~/.claude/skills/

# 或直接使用
cd review-analyzer-skill
```

### 使用

在 Claude Code 中用自然语言调用：

```bash
# 示例1：指定文件分析
请分析这个产品的评论：reviews.csv

# 示例2：描述需求
帮我做一下竞品评论的深度分析

# 示例3：指定数量
分析最近100条评论数据
```

## CSV 文件格式要求

**必需列**（自动模糊匹配）：
- 评论内容：内容/评价/body/review/text/comment
- 评分：打分/rating/score/star

**可选列**：
- 时间：时间/date/日期/time
- 标题：标题/title/summary
- 用户名：用户/user/username

## 输出文件

分析完成后，将在 `output/` 目录生成三种报告：

| 文件 | 格式 | 内容说明 |
|------|------|----------|
| `评论采集及打标数据_{ASIN}.csv` | CSV | 原始评论 + 22维度标签数据 |
| `分析洞察报告_{ASIN}.md` | Markdown | 深度洞察分析与建议 |
| `可视化洞察报告_{ASIN}.html` | HTML | 高品质可视化看板 |

## 目录结构

```
review-analyzer-skill/
├── skill.md              # Skill 定义文件
├── README.md             # 说明文档
├── prompts/              # 提示词模板
│   ├── tagging.txt      # 单条评论打标
│   ├── tagging_batch.txt # 批量打标
│   └── insights.txt     # 洞察报告生成
├── templates/            # 输出模板
│   └── report.html      # HTML 可视化模板
├── utils/               # 工具脚本
│   ├── csv_reader.sh    # CSV 读取
│   ├── tagging_core.sh  # 标签提取核心
│   ├── insights_generator.sh  # 洞察报告生成
│   ├── html_generator.sh  # HTML 生成
│   ├── analyzer_controller.sh  # 主控制器
│   ├── merge_csv.py     # CSV 合并工具
│   └── test.sh          # 测试脚本
└── output/              # 输出目录
```

## 工作流程

1. **参数收集**：询问分析数量（100条/300条/全部）
2. **CSV解析**：读取并处理CSV文件
3. **批量打标**：每批最多30条评论并发处理
4. **统计分析**：计算情感分布、用户画像等
5. **报告生成**：生成洞察报告和可视化看板

## 测试

运行测试脚本验证安装：

```bash
cd utils
chmod +x test.sh
./test.sh
```

## 依赖

- **必需**: Bash, sed, awk, grep
- **推荐**: Python3 (用于CSV合并)
- **推荐**: jq (用于JSON处理)

## 使用场景

### 场景1：产品优化
分析自己产品的评论，发现用户痛点，优化产品功能和设计。

### 场景2：竞品分析
分析竞品评论，了解竞争对手的优势和劣势，寻找差异化机会。

### 场景3：市场调研
批量分析多个产品的评论，了解市场需求、用户偏好和行业趋势。

### 场景4：用户洞察
深度了解目标用户群体，构建精准用户画像，优化营销策略。

## 注意事项

1. **分析数量**: 推荐选择 100 条评论以平衡速度与质量
2. **并发限制**: 每批最多处理 30 条评论
3. **输出目录**: 默认 output/，可自定义
4. **编码支持**: UTF-8/GBK/GB2312 自动检测

## 技术架构

- **不依赖 Python**: 使用 Bash 脚本处理核心逻辑
- **AI 驱动**: 使用 Claude 内置模型进行标签提取和分析
- **批量处理**: 支持并发处理提高效率
- **模块化设计**: 各模块独立，易于维护和扩展

## License

MIT


path: utils/ai_column_detector.py
content: #!/usr/bin/env python3
"""
AI 驱动的 CSV 列检测器
让 AI 观察 CSV 前 N 行数据，智能识别正确的列索引
"""

import csv
import json
import sys
from pathlib import Path


def read_csv_sample(csv_path: str, max_rows: int = 20) -> dict:
    """
    读取 CSV 文件的前 N 行用于 AI 分析

    Returns:
        包含表头和样本数据的字典
    """
    csv_path = Path(csv_path)
    if not csv_path.exists():
        raise FileNotFoundError(f"文件不存在: {csv_path}")

    sample_data = {
        "file_name": csv_path.name,
        "total_rows": 0,
        "headers": [],
        "sample_rows": []
    }

    with open(csv_path, 'r', encoding='utf-8-sig') as f:
        reader = csv.reader(f)

        # 读取表头
        try:
            headers = next(reader)
            sample_data["headers"] = headers
        except StopIteration:
            return sample_data

        # 读取样本数据
        for i, row in enumerate(reader):
            if i >= max_rows:
                break
            # 只保存非空行
            if row and any(cell.strip() for cell in row):
                sample_data["sample_rows"].append(row)

        sample_data["total_rows"] = i + 1

    return sample_data


def create_ai_prompt(sample_data: dict) -> str:
    """
    创建 AI 分析提示词

    Args:
        sample_data: CSV 样本数据

    Returns:
        AI 提示词
    """
    headers = sample_data["headers"]
    rows = sample_data["sample_rows"][:5]  # 只用前5行作为示例

    # 构建表头展示
    header_display = "\n".join([
        f"列{i}: {h}" for i, h in enumerate(headers) if h
    ])

    # 构建数据行展示
    row_displays = []
    for row_idx, row in enumerate(rows[:3], 1):
        row_display = f"行{row_idx}: " + " | ".join([
            f"[{i}]{str(row[i])[:50] if i < len(row) else ''}"
            for i in range(min(len(headers), len(row)))
        ])
        row_displays.append(row_display)

    return f"""你是一个 CSV 数据分析专家。请分析以下电商评论数据的 CSV 结构，找出正确的列索引。

# CSV 表头
{header_display}

# 样本数据（前3行）
{chr(10).join(row_displays)}

# 你的任务

请仔细观察以上数据，找出以下列的**索引号**（从 0 开始）：

1. **rating_col**: 评分/星级列
   - 特征：包含数字 1-5，或包含 "star"、"rating"、"评分" 等关键词
   - 可能是 SVG URL（如 stars-5.svg）

2. **title_col**: 评论标题列（如果有）
   - 特征：简短的标题，通常 5-20 个字
   - 可能包含 "title"、"标题"、"subject" 等关键词

3. **content_col**: 评论内容列（必需）
   - 特征：较长的评论文本，通常 50-500 字
   - 这是核心列，请务必仔细识别
   - **跳过 CSS 类名列**（如 typography_body-m__k2UI7 这种包含 __ 或 class 关键词的）
   - 寻找真正包含用户评论内容的列

# 输出格式

请**只输出 JSON**，不要有任何其他文字：

```json
{{
  "rating_col": 索引数字,
  "title_col": 索引数字或null,
  "content_col": 索引数字,
  "confidence": "high/medium/low",
  "reasoning": "简短说明你的判断依据"
}}
```

# 注意事项

- 如果没有标题列，title_col 设为 null
- content_col 是最重要的，请确保找到正确的评论内容列
- confidence 表示你的判断信心程度
- reasoning 简要说明你如何识别这些列（比如"列7包含stars-5.svg"）
"""


def detect_columns_with_ai(csv_path: str) -> dict:
    """
    使用 AI 检测 CSV 列索引

    Args:
        csv_path: CSV 文件路径

    Returns:
        列索引映射字典
    """
    print(f"📂 正在分析 CSV 文件: {csv_path}")

    # 读取样本数据
    sample_data = read_csv_sample(csv_path, max_rows=20)

    if not sample_data["headers"]:
        raise ValueError("CSV 文件没有表头或为空")

    print(f"📊 CSV 信息:")
    print(f"   总列数: {len(sample_data['headers'])}")
    print(f"   样本行数: {len(sample_data['sample_rows'])}")
    print(f"   总行数: {sample_data['total_rows']}")

    # 生成提示词
    prompt = create_ai_prompt(sample_data)

    # 输出提示词供 AI 使用
    print("\n" + "=" * 60)
    print("🤖 请将以下内容发送给 AI 进行分析:")
    print("=" * 60)
    print(prompt)
    print("=" * 60)

    # 返回提示词供外部使用
    return {
        "prompt": prompt,
        "sample_data": sample_data
    }


def parse_ai_response(response_text: str) -> dict:
    """
    解析 AI 返回的 JSON 结果

    Args:
        response_text: AI 返回的文本

    Returns:
        解析后的列索引字典
    """
    # 提取 JSON 代码块
    import re

    # 查找 ```json ... ``` 代码块
    json_match = re.search(r'```json\s*(.*?)\s*```', response_text, re.DOTALL)
    if json_match:
        json_str = json_match.group(1)
    else:
        # 如果没有代码块，尝试直接解析
        json_str = response_text.strip()

    try:
        result = json.loads(json_str)
        print("\n✅ AI 检测结果:")
        print(f"   rating_col: {result.get('rating_col')}")
        print(f"   title_col: {result.get('title_col')}")
        print(f"   content_col: {result.get('content_col')}")
        print(f"   confidence: {result.get('confidence')}")
        print(f"   reasoning: {result.get('reasoning')}")
        return result
    except json.JSONDecodeError as e:
        print(f"\n❌ 解析 AI 响应失败: {e}")
        print(f"原始响应: {response_text[:200]}")
        raise


def main():
    """命令行入口"""
    import argparse

    parser = argparse.ArgumentParser(description='AI 驱动的 CSV 列检测器')
    parser.add_argument('input_csv', help='输入 CSV 文件路径')
    parser.add_argument('--save-prompt', help='将提示词保存到文件')
    parser.add_argument('--parse-response', help='解析 AI 响应（从文件或字符串）')

    args = parser.parse_args()

    # 模式1: 生成提示词
    if not args.parse_response:
        result = detect_columns_with_ai(args.input_csv)

        if args.save_prompt:
            with open(args.save_prompt, 'w', encoding='utf-8') as f:
                f.write(result['prompt'])
            print(f"\n💾 提示词已保存到: {args.save_prompt}")

        # 同时保存样本数据供调试
        sample_file = Path(args.input_csv).stem + '_sample.json'
        with open(sample_file, 'w', encoding='utf-8') as f:
            json.dump(result['sample_data'], f, ensure_ascii=False, indent=2)
        print(f"💾 样本数据已保存到: {sample_file}")

    # 模式2: 解析 AI 响应
    else:
        # 从文件读取或直接使用字符串
        try:
            with open(args.parse_response, 'r', encoding='utf-8') as f:
                response_text = f.read()
        except FileNotFoundError:
            response_text = args.parse_response

        column_map = parse_ai_response(response_text)

        # 输出为可用的参数格式
        print("\n" + "=" * 60)
        print("📋 可用于 csv_processor.py 的参数:")
        print("=" * 60)
        print(f"--rating-col {column_map['rating_col']} \\")
        print(f"--content-col {column_map['content_col']}")
        if column_map.get('title_col'):
            print(f"--title-col {column_map['title_col']}")


if __name__ == "__main__":
    main()


path: utils/analyzer_controller.sh
content: #!/bin/bash

# Main Controller for Review Analyzer Skill
# Orchestrates the entire analysis workflow

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
PROJECT_ROOT="$(dirname "$(dirname "$ROOT_DIR")")"  # 项目根目录
PROMPTS_DIR="$ROOT_DIR/prompts"
TEMPLATES_DIR="$ROOT_DIR/templates"
UTILS_DIR="$ROOT_DIR/utils"
OUTPUT_DIR="$PROJECT_ROOT/output"  # 统一输出到项目根目录

# Source utility scripts
source "$UTILS_DIR/csv_reader.sh"
source "$UTILS_DIR/tagging_core.sh"
source "$UTILS_DIR/insights_generator.sh"
source "$UTILS_DIR/html_generator.sh"

# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m'

# Global variables
CSV_FILE=""
MAX_RECORDS=0
ASIN="unknown"
PRODUCT_NAME=""
OUTPUT_PREFIX=""
ANALYSIS_COUNT=100

# ============================================
# LOGGING FUNCTIONS
# ============================================

log_info() {
    echo -e "${BLUE}[INFO]${NC} $1"
}

log_success() {
    echo -e "${GREEN}[SUCCESS]${NC} $1"
}

log_warning() {
    echo -e "${YELLOW}[WARNING]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

log_progress() {
    echo -e "${CYAN}[PROGRESS]${NC} $1"
}

# ============================================
# INITIALIZATION
# ============================================

init_output_dir() {
    mkdir -p "$OUTPUT_DIR"
    log_info "Output directory: $OUTPUT_DIR"
}

# ============================================
# PARAMETER COLLECTION
# ============================================

collect_parameters() {
    log_info "收集分析参数..."

    # This function is meant to be called from the main skill
    # The actual AskUserQuestion calls will be done by Claude

    # Set defaults
    ANALYSIS_COUNT=${ANALYSIS_COUNT:-100}
    OUTPUT_PREFIX=${OUTPUT_PREFIX:-""}

    log_info "分析数量: $ANALYSIS_COUNT"
}

# ============================================
# TASK SIZE EVALUATION
# ============================================

evaluate_task_size() {
    local count="$1"
    local threshold=1000

    if [[ "$count" -ge "$threshold" ]]; then
        log_info "检测到大型任务: $count 条评论 (≥$threshold)"
        log_info "将调用 planning-with-files skill 进行任务管理"
        return 0  # Large task
    else
        log_info "检测到小型任务: $count 条评论 (<$threshold)"
        return 1  # Small task
    fi
}

# ============================================
# CSV PROCESSING
# ============================================

process_csv() {
    local csv_file="$1"

    log_info "处理 CSV 文件: $csv_file"

    if [[ ! -f "$csv_file" ]]; then
        log_error "文件不存在: $csv_file"
        return 1
    fi

    # Get file info
    CSV_FILE="$csv_file"
    local count=$("$UTILS_DIR/csv_reader.sh" "$csv_file" "count")

    log_info "检测到 $count 条评论"

    # Evaluate task size
    evaluate_task_size "$count"
    local is_large_task=$?

    # Extract ASIN from filename if possible
    local basename=$(basename "$csv_file" .csv)
    if [[ "$basename" =~ [A-Z0-9]{10} ]]; then
        ASIN=$(echo "$basename" | grep -oE "[A-Z0-9]{10}")
        log_info "检测到 ASIN: $ASIN"
    fi

    # Determine output prefix
    if [[ -z "$OUTPUT_PREFIX" ]]; then
        OUTPUT_PREFIX="$OUTPUT_DIR/评论采集及打标数据_${ASIN}"
    fi

    return $is_large_task
}

# ============================================
# BATCH TAGGING
# ============================================

batch_tag_reviews() {
    log_info "开始批量标签提取..."

    local temp_reviews="$OUTPUT_DIR/reviews_extracted.json"
    local temp_tagged="$OUTPUT_DIR/reviews_tagged.json"

    # Extract reviews from CSV
    log_progress "提取评论数据..."
    "$UTILS_DIR/csv_reader.sh" "$CSV_FILE" "extract" "$ANALYSIS_COUNT" > "$temp_reviews"

    local extracted_count=$(jq '. | length' "$temp_reviews" 2>/dev/null || echo "0")
    log_info "已提取 $extracted_count 条评论"

    if [[ "$extracted_count" -eq 0 ]]; then
        log_error "未能提取任何评论"
        return 1
    fi

    # Split into batches of 30
    log_progress "分批处理 (每批最多30条)..."
    local batch_size=30
    local total_batches=$(( (extracted_count + batch_size - 1) / batch_size ))

    log_info "共 $total_batches 个批次"

    # Process batches
    local all_tagged="[]"
    local current_batch=1

    for ((i=0; i<extracted_count; i+=batch_size)); do
        local end=$((i + batch_size))
        if [[ $end -gt $extracted_count ]]; then
            end=$extracted_count
        fi

        log_progress "处理批次 $current_batch/$total_batches (评论 $((i+1))-$end)..."

        # Extract batch
        local batch_json=$(jq ".[$i:$end]" "$temp_reviews")

        # Create prompt for batch tagging
        local prompt=$(cat "$PROMPTS_DIR/tagging_batch.txt")
        prompt="${prompt//{batch_size}/$((end - i))}"
        prompt="${prompt//{reviews_json}/$batch_json}"

        # Save prompt for AI processing
        local prompt_file="$OUTPUT_DIR/batch_${current_batch}_prompt.txt"
        echo "$prompt" > "$prompt_file"

        log_info "批次 $current_batch 提示词已保存到: $prompt_file"
        log_info "等待 AI 处理..."

        # Note: The actual AI processing will be done by the main skill
        # For now, we save the prompts and expect results to be provided

        ((current_batch++))
    done

    log_success "批次准备完成，共 $total_batches 个批次"

    # Return the prompt files list
    ls "$OUTPUT_DIR"/batch_*_prompt.txt 2>/dev/null
}

# ============================================
# RESULTS PROCESSING
# ============================================

process_tagging_results() {
    local results_dir="$1"

    log_info "处理打标结果..."

    local all_tagged="[]"
    local tagged_count=0

    # Combine all batch results
    for result_file in "$results_dir"/batch_*_result.json; do
        if [[ -f "$result_file" ]]; then
            local batch_results=$(cat "$result_file")
            all_tagged=$(echo "$all_tagged" | jq ". + $batch_results")
            ((tagged_count+=$(echo "$batch_results" | jq '. | length')))
        fi
    done

    log_success "成功打标 $tagged_count 条评论"

    # Save combined results
    local output_json="$OUTPUT_DIR/reviews_tagged.json"
    echo "$all_tagged" > "$output_json"

    echo "$output_json"
}

# ============================================
# GENERATE OUTPUTS
# ============================================

generate_csv_output() {
    local tagged_json="$1"

    log_progress "生成 CSV 标签数据..."

    local output_csv="${OUTPUT_PREFIX}.csv"

    # Use Python to merge CSV with tags
    if command -v python3 &> /dev/null; then
        python3 "$UTILS_DIR/merge_csv.py" "$CSV_FILE" "$tagged_json" "$output_csv"

        if [[ $? -eq 0 ]]; then
            log_success "CSV 标签数据已生成: $output_csv"
        else
            log_warning "CSV 生成失败，使用原始数据"
        fi
    else
        log_warning "Python3 未安装，跳过 CSV 合并"
    fi
}

generate_insights_report() {
    local tagged_json="$1"

    log_progress "生成洞察分析报告..."

    # Calculate statistics
    local stats_json=$("$UTILS_DIR/insights_generator.sh" "stats" "$tagged_json")

    # Select golden samples
    local samples_json=$("$UTILS_DIR/insights_generator.sh" "samples" "$tagged_json" "12")

    # Extract personas
    local personas_json=$(echo "$stats_json" | jq '.personas')

    # Create insights prompt
    local insights_prompt=$("$UTILS_DIR/insights_generator.sh" "prompt" \
        "$stats_json" "$personas_json" "$samples_json" "$ASIN" "$PRODUCT_NAME")

    # Save prompt for AI processing
    local prompt_file="$OUTPUT_DIR/insights_prompt.txt"
    echo "$insights_prompt" > "$prompt_file"

    log_info "洞察报告提示词已保存到: $prompt_file"
    log_info "等待 AI 生成洞察报告..."

    echo "$prompt_file"
}

process_insights_result() {
    local insights_md="$1"

    log_progress "处理洞察报告结果..."

    local output_md="${OUTPUT_DIR}/../分析洞察报告_${ASIN}.md"

    # Save markdown report
    echo "$insights_md" > "$output_md"

    log_success "洞察分析报告已生成: $output_md"

    echo "$output_md"
}

generate_html_dashboard() {
    local stats_json="$1"
    local tagged_json="$2"
    local insights_md="$3"

    log_progress "生成 HTML 可视化看板..."

    local output_html="${OUTPUT_DIR}/../可视化洞察报告_${ASIN}.html"

    # Generate HTML
    "$UTILS_DIR/html_generator.sh" "generate" \
        "$stats_json" "$tagged_json" "$insights_md" "$output_html"

    if [[ -f "$output_html" ]]; then
        log_success "HTML 可视化看板已生成: $output_html"
    fi

    echo "$output_html"
}

# ============================================
# MAIN WORKFLOW
# ============================================

run_analysis() {
    log_info "=========================================="
    log_info "Review Analyzer - 开始分析"
    log_info "=========================================="

    # Initialize
    init_output_dir

    # Process CSV
    process_csv "$1"
    local is_large_task=$?

    if [[ $is_large_task -eq 0 ]]; then
        log_info "=========================================="
        log_info "大型任务处理流程"
        log_info "=========================================="
        log_info "请调用 planning-with-files skill 进行任务管理"
        log_info "任务描述：分析 $count 条电商评论"
        log_info "=========================================="
        return 0
    fi

    # Collect parameters (done by main skill)
    collect_parameters

    # Batch tagging
    local batch_prompts=$(batch_tag_reviews)

    # Note: At this point, control returns to the main skill
    # The skill will process each prompt with AI and collect results

    log_info "=========================================="
    log_info "批次准备完成"
    log_info "=========================================="
    log_info "请使用 AI 处理以下提示词文件："
    echo "$batch_prompts"
    log_info "=========================================="
}

show_results() {
    log_info "=========================================="
    log_info "分析完成！"
    log_info "=========================================="
    log_info "输出文件位置："
    log_info "  - CSV 标签数据: ${OUTPUT_PREFIX}.csv"
    log_info "  - 洞察分析报告: ${OUTPUT_DIR}/../分析洞察报告_${ASIN}.md"
    log_info "  - HTML 可视化看板: ${OUTPUT_DIR}/../可视化洞察报告_${ASIN}.html"
    log_info "=========================================="
}

# ============================================
# MAIN ENTRY POINT
# ============================================

main() {
    local action="$1"
    shift

    case "$action" in
        "analyze")
            run_analysis "$@"
            ;;
        "results")
            show_results
            ;;
        "collect")
            collect_parameters "$@"
            ;;
        *)
            echo "Usage: $0 {analyze|results|collect} ..."
            exit 1
            ;;
    esac
}

main "$@"


path: utils/csv_processor.py
content: #!/usr/bin/env python3
"""
电商评论数据处理工具 - 简化版
固定列名匹配，找不到就报错
支持 Excel 文件自动转换
"""

import csv
import json
import os
import tempfile
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Any, Optional


class ReviewProcessor:
    """评论数据处理器"""

    # 22维度标签列名
    TAG_COLUMNS = [
        '人群_性别', '人群_年龄段', '人群_职业', '人群_购买角色',
        '场景_使用场景',
        '功能_满意度', '功能_具体功能',
        '质量_材质', '质量_做工', '质量_耐用性',
        '服务_发货速度', '服务_包装质量', '服务_客服响应', '服务_退换货', '服务_保修',
        '体验_舒适度', '体验_易用性', '体验_外观设计', '体验_价格感知',
        '竞品_竞品对比', '复购_复购意愿',
        '情感_总体评价'
    ]

    # 固定列名匹配规则（中英文）
    COLUMN_NAMES = {
        'rating': ['星级', '评分', '打分', 'rating', 'star', 'score', 'stars', 'star-rating', 'src'],
        'title': ['标题', '题目', 'subject', 'title', 'headline'],
        'content': ['内容', '评论', '评价', '反馈', 'content', 'body', 'review', 'comment', 'text', 'message']
    }

    @staticmethod
    def _is_excel_file(file_path: Path) -> bool:
        """判断文件是否为 Excel 格式"""
        excel_extensions = ['.xlsx', '.xls', '.xlsm']
        return file_path.suffix.lower() in excel_extensions

    @staticmethod
    def _convert_excel_to_csv(excel_path: Path) -> Path:
        """将 Excel 文件转换为 CSV（使用 openpyxl）"""
        try:
            import openpyxl
        except ImportError:
            print("📦 正在安装 openpyxl...")
            os.system("pip install openpyxl -q")
            import openpyxl

        print(f"🔄 检测到 Excel 文件，正在转换为 CSV...")

        # 读取 Excel
        wb = openpyxl.load_workbook(excel_path)
        ws = wb.active

        # 创建临时 CSV 文件
        temp_csv = tempfile.NamedTemporaryFile(mode='w', suffix='.csv', delete=False, encoding='utf-8-sig', newline='')
        temp_path = Path(temp_csv.name)

        # 写入 CSV
        writer = csv.writer(temp_csv)
        for row in ws.iter_rows(values_only=True):
            # 处理 None 值
            processed_row = [str(cell) if cell is not None else '' for cell in row]
            writer.writerow(processed_row)

        temp_csv.close()

        print(f"✅ Excel 已转换为临时 CSV: {temp_path.name}")
        return temp_path

    @staticmethod
    def _is_css_column(header: str) -> bool:
        """判断是否为CSS类名格式的列"""
        if not header:
            return False
        header_lower = header.lower()

        # 如果以 src 结尾，可能是图片/资源列，不是CSS列
        if header_lower.endswith(' src') or header_lower == 'src':
            return False

        # 包含 __ 和其他 CSS 相关关键词则是CSS列
        has_double_underscore = '__' in header_lower
        css_keywords = ['typography', 'style', 'class', 'css', 'avatar']
        has_css_keywords = any(kw in header_lower for kw in css_keywords)

        return has_double_underscore and has_css_keywords

    def __init__(self, input_csv: str, output_dir: Optional[str] = None):
        """初始化处理器"""
        self.input_path = Path(input_csv)
        if not self.input_path.exists():
            raise FileNotFoundError(f"❌ 文件不存在: {input_csv}")

        # 自动检测并转换 Excel 文件
        self.temp_csv_file = None
        if self._is_excel_file(self.input_path):
            self.temp_csv_file = self._convert_excel_to_csv(self.input_path)
            self.input_csv = self.temp_csv_file
            self.is_excel_converted = True
            print(f"📋 原始文件: {self.input_path.name}")
        else:
            self.input_csv = self.input_path
            self.is_excel_converted = False

        # 从文件名推断产品名
        product_name = self.input_path.stem
        date_str = datetime.now().strftime('%Y%m%d')

        self.output_dir = Path(output_dir or f"output/{product_name}_{date_str}")
        self.output_dir.mkdir(parents=True, exist_ok=True)

        # 检测列索引
        self.column_map = self._detect_columns()

    def _detect_columns(self) -> Dict[str, int]:
        """检测CSV列索引"""
        # 尝试多种编码
        encodings = ['utf-8-sig', 'utf-8', 'gbk', 'gb2312', 'latin1']
        headers = None
        used_encoding = None

        for encoding in encodings:
            try:
                with open(self.input_csv, 'r', encoding=encoding) as f:
                    reader = csv.reader(f)
                    headers = next(reader)
                used_encoding = encoding
                break
            except (UnicodeDecodeError, UnicodeError):
                continue

        if headers is None:
            raise ValueError("❌ 无法读取CSV文件，请检查文件编码（支持UTF-8/GBK/GB2312）")

        print(f"   📝 使用编码: {used_encoding}")

        # 转小写用于匹配
        headers_lower = [h.lower() if h else '' for h in headers]

        column_map = {}
        print(f"\n📋 CSV表头检测:")

        for col_type, names in self.COLUMN_NAMES.items():
            found = False
            for idx, header_lower in enumerate(headers_lower):
                # 跳过 CSS 类名列
                if self._is_css_column(headers[idx]):
                    continue

                for name in names:
                    # 使用包含匹配（而不是精确匹配）
                    if name.lower() in header_lower:
                        column_map[col_type] = idx
                        print(f"   ✅ {col_type}: 列{idx} ({headers[idx]})")
                        found = True
                        break
                if found:
                    break

            if not found:
                print(f"   ❌ {col_type}: 未找到")

        # 验证必需列
        required = {'rating', 'content'}
        missing = required - set(column_map.keys())
        if missing:
            print(f"\n❌ 错误: CSV缺少必需列: {missing}")
            print(f"\n📌 你的CSV必须包含以下列名之一:")
            for col_type, names in self.COLUMN_NAMES.items():
                print(f"   {col_type}: {', '.join(names)}")
            print(f"\n💡 请修改你的CSV表头，使其包含上述列名")
            raise ValueError(f"缺少必需列: {missing}")

        return column_map

    def _open_csv(self, mode='r'):
        """打开CSV文件，自动检测编码"""
        encodings = ['utf-8-sig', 'utf-8', 'gbk', 'gb2312', 'latin1']
        last_error = None

        for encoding in encodings:
            try:
                f = open(self.input_csv, mode, encoding=encoding, newline='')
                # 验证是否能正常读取第一行
                if 'r' in mode:
                    first_byte = f.read(1024)
                    f.seek(0)
                return f, encoding
            except (UnicodeDecodeError, UnicodeError) as e:
                last_error = e
                continue

        raise ValueError(f"❌ 无法读取CSV文件，请检查文件编码: {last_error}")

    def read_csv(self, min_length: int = 10) -> List[Dict[str, Any]]:
        """读取CSV文件，提取评论和评分"""
        reviews = []

        f, encoding = self._open_csv()
        try:
            reader = csv.reader(f)
            headers = next(reader)  # 跳过表头

            for row in reader:
                # 跳过空行
                if not row or len(row) <= max(self.column_map.values()):
                    continue

                # 跳过广告行
                if any(ad in str(row).lower() for ad in ['advertisement', 'sponsored', '广告']):
                    continue

                # 解析评分
                rating = self._parse_rating(row[self.column_map['rating']])

                # 提取内容
                content = row[self.column_map['content']] or ""

                # 如果有标题列，合并标题和内容
                if 'title' in self.column_map:
                    title = row[self.column_map['title']] or ""
                    if title and content:
                        content = f"{title}. {content}"
                    elif title:
                        content = title

                content = content.strip()
                # 清理多余空白
                content = ' '.join(content.split())

                if content and len(content) > min_length:
                    reviews.append({
                        "id": len(reviews) + 1,
                        "content": content,
                        "rating": rating
                    })
        finally:
            f.close()

        return reviews

    def _parse_rating(self, value: str) -> int:
        """解析评分"""
        if not value:
            return 3

        value_lower = value.lower()

        # 从SVG URL解析
        if 'stars-5' in value_lower:
            return 5
        elif 'stars-4' in value_lower:
            return 4
        elif 'stars-3' in value_lower:
            return 3
        elif 'stars-2' in value_lower:
            return 2
        elif 'stars-1' in value_lower:
            return 1

        # 从数字解析
        import re
        numbers = re.findall(r'\d+', value)
        if numbers:
            rating = int(numbers[0])
            return max(1, min(5, rating))

        return 3  # 默认

    def create_batches(self, reviews: List[Dict], batch_size: int = 30) -> List[Dict]:
        """将评论分成批次"""
        batches = []
        total_batches = (len(reviews) + batch_size - 1) // batch_size

        for i in range(0, len(reviews), batch_size):
            batch = reviews[i:i+batch_size]
            batches.append({
                "batch_num": len(batches) + 1,
                "total_batches": total_batches,
                "reviews": batch
            })

        return batches

    def save_batch_json(self, reviews: List[Dict], batch_num: int):
        """保存批次为JSON文件"""
        output_file = self.output_dir / f"batch{batch_num}_input.json"
        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(reviews, f, ensure_ascii=False, indent=2)
        print(f"✅ 批次 {batch_num} 已保存: {output_file}")

    def save_batch_info(self, reviews: List[Dict], batch_size: int = 30):
        """保存批次信息"""
        batches = self.create_batches(reviews, batch_size)

        batch_info = {
            "total_reviews": len(reviews),
            "total_batches": len(batches),
            "batch_size": batch_size,
            "batches": [{"batch_num": b["batch_num"], "count": len(b["reviews"])} for b in batches],
            "column_map": self.column_map
        }

        output_file = self.output_dir / "batch_info.json"
        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(batch_info, f, ensure_ascii=False, indent=2)

        return batch_info

    def merge_tags(self, tag_files: List[str] = None) -> List[Dict]:
        """合并多个批次标签文件"""
        if tag_files is None:
            tag_files = sorted(self.output_dir.glob("batch*_tags.json"))
            tag_files = [str(f) for f in tag_files]

        all_tags = []
        for file in tag_files:
            with open(file, 'r', encoding='utf-8') as f:
                tags = json.load(f)
                all_tags.extend(tags)

        print(f"✅ 合并完成: 共 {len(all_tags)} 条标签")
        return all_tags

    def export_csv(self, reviews: List[Dict], tags: List[Dict], filename: str = "reviews_labeled.csv"):
        """导出带标签的CSV文件"""
        tags_dict = {t['review_id']: t for t in tags}

        output_file = self.output_dir / filename
        with open(output_file, 'w', encoding='utf-8-sig', newline='') as f:
            writer = csv.writer(f)

            headers = ['ID', '评分', '评论内容', '情感倾向', '信息密度'] + self.TAG_COLUMNS
            writer.writerow(headers)

            for review in reviews:
                review_id = review['id']
                tag_data = tags_dict.get(review_id, {})

                row = [
                    review_id,
                    review['rating'],
                    review['content'][:500],
                    tag_data.get('sentiment', ''),
                    tag_data.get('info_score', '')
                ]

                tags_obj = tag_data.get('tags', {})
                for col in self.TAG_COLUMNS:
                    row.append(tags_obj.get(col, ''))

                writer.writerow(row)

        print(f"✅ CSV 生成完成: {output_file}")

    def calculate_stats(self, reviews: List[Dict], tags: List[Dict]) -> Dict:
        """计算统计数据"""
        sentiment_counts = {}
        for t in tags:
            s = t['sentiment']
            sentiment_counts[s] = sentiment_counts.get(s, 0) + 1

        rating_dist = {}
        for r in reviews:
            rating = r['rating']
            rating_dist[rating] = rating_dist.get(rating, 0) + 1

        return {
            "total_reviews": len(reviews),
            "total_tagged": len(tags),
            "sentiment_distribution": sentiment_counts,
            "rating_distribution": rating_dist,
            "average_rating": sum(r['rating'] for r in reviews) / len(reviews) if reviews else 0
        }

    def save_stats(self, stats: Dict, filename: str = "stats.json"):
        """保存统计数据"""
        output_file = self.output_dir / filename
        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(stats, f, ensure_ascii=False, indent=2)
        print(f"✅ 统计数据已保存: {output_file}")

    def print_stats(self, stats: Dict):
        """打印统计信息"""
        print("\n" + "=" * 60)
        print("📊 统计分析")
        print("=" * 60)
        print(f"总评论数: {stats['total_reviews']}")
        print(f"平均评分: {stats['average_rating']:.2f}/5")

        print("\n情感分布:")
        for s, c in sorted(stats['sentiment_distribution'].items(), key=lambda x: -x[1]):
            pct = c / stats['total_tagged'] * 100
            print(f"  {s}: {c} ({pct:.1f}%)")

        print("\n评分分布:")
        for r, c in sorted(stats['rating_distribution'].items()):
            pct = c / stats['total_reviews'] * 100
            print(f"  {r}星: {c} ({pct:.1f}%)")

    def cleanup(self):
        """清理临时文件（Excel 转换产生的 CSV）"""
        if self.temp_csv_file and self.temp_csv_file.exists():
            try:
                self.temp_csv_file.unlink()
                print(f"🧹 已清理临时文件: {self.temp_csv_file.name}")
            except Exception as e:
                print(f"⚠️  清理临时文件失败: {e}")


def main():
    """命令行入口"""
    import argparse

    parser = argparse.ArgumentParser(description='电商评论数据处理工具（支持 CSV 和 Excel 文件）')
    parser.add_argument('input_csv', help='输入文件路径（支持 CSV、XLSX、XLS）')
    parser.add_argument('-o', '--output', help='输出目录')
    parser.add_argument('-b', '--batch-size', type=int, default=30, help='批次大小 (默认: 30)')
    parser.add_argument('--export-batches', action='store_true', help='导出批次 JSON 文件')
    parser.add_argument('--merge-tags', action='store_true', help='合并标签文件并导出 CSV')
    parser.add_argument('--stats', action='store_true', help='显示统计信息')

    args = parser.parse_args()

    # 初始化处理器（会自动检测并转换 Excel）
    print(f"📂 读取文件: {args.input_csv}")
    processor = ReviewProcessor(args.input_csv, args.output)

    try:
        # 读取数据
        reviews = processor.read_csv()
        print(f"✅ 共读取 {len(reviews)} 条评论")

        # 导出批次
        if args.export_batches:
            batches = processor.create_batches(reviews, args.batch_size)
            for batch in batches:
                processor.save_batch_json(batch['reviews'], batch['batch_num'])
            processor.save_batch_info(reviews, args.batch_size)

        # 合并标签并导出 CSV
        if args.merge_tags:
            tags = processor.merge_tags()
            processor.export_csv(reviews, tags)

            stats = processor.calculate_stats(reviews, tags)
            processor.save_stats(stats)

            if args.stats:
                processor.print_stats(stats)

        # 仅统计
        if args.stats and not args.merge_tags:
            tags = processor.merge_tags()
            stats = processor.calculate_stats(reviews, tags)
            processor.print_stats(stats)

    finally:
        # 清理临时文件（如果有 Excel 转换）
        processor.cleanup()


if __name__ == "__main__":
    main()


path: utils/csv_reader.sh
content: #!/bin/bash

# CSV Reader Utility for Review Analyzer
# Supports fuzzy column matching, multiple encodings, and proper CSV parsing

# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Function to print error message
error() {
    echo -e "${RED}[ERROR]${NC} $1" >&2
}

# Function to print success message
success() {
    echo -e "${GREEN}[SUCCESS]${NC} $1"
}

# Function to print info message
info() {
    echo -e "${YELLOW}[INFO]${NC} $1"
}

# Function to detect file encoding
detect_encoding() {
    local file="$1"

    # Try to detect encoding using file command
    if command -v file &> /dev/null; then
        local encoding=$(file -b --mime-encoding "$file" 2>/dev/null)
        echo "$encoding"
        return 0
    fi

    # Fallback: try common encodings
    for enc in utf-8 gbk gb2312 latin-1; do
        if iconv -f "$enc" -t utf-8 "$file" > /dev/null 2>&1; then
            echo "$enc"
            return 0
        fi
    done

    echo "unknown"
}

# Function to convert file to UTF-8
convert_to_utf8() {
    local file="$1"
    local output="$2"
    local encoding=$(detect_encoding "$file")

    info "Detected encoding: $encoding"

    if [[ "$encoding" == *"utf-8"* ]] || [[ "$encoding" == *"utf8"* ]]; then
        cp "$file" "$output"
        return 0
    fi

    # Try to convert to UTF-8
    if iconv -f "$encoding" -t utf-8 "$file" -o "$output" 2>/dev/null; then
        success "Converted to UTF-8"
        return 0
    else
        # Try common encodings as fallback
        for enc in gbk gb2312 latin-1 iso-8859-1; do
            if iconv -f "$enc" -t utf-8 "$file" -o "$output" 2>/dev/null; then
                success "Converted using fallback encoding: $enc"
                return 0
            fi
        done
    fi

    error "Failed to convert file to UTF-8"
    return 1
}

# Function to detect delimiter
detect_delimiter() {
    local file="$1"
    local first_line=$(head -n 1 "$file")

    # Count delimiters in first line
    local comma_count=$(echo "$first_line" | grep -o ',' | wc -l)
    local semicolon_count=$(echo "$first_line" | grep -o ';' | wc -l)
    local tab_count=$(echo "$first_line" | grep -o $'\t' | wc -l)
    local pipe_count=$(echo "$first_line" | grep -o '|' | wc -l)

    # Return delimiter with most occurrences
    if (( pipe_count > comma_count )) && (( pipe_count > semicolon_count )) && (( pipe_count > tab_count )); then
        echo "|"
    elif (( tab_count > comma_count )) && (( tab_count > semicolon_count )); then
        echo $'\t'
    elif (( semicolon_count > comma_count )); then
        echo ";"
    else
        echo ","
    fi
}

# Function to find column index by fuzzy matching
find_column() {
    local headers="$1"
    local column_name="$2"

    # Convert to lowercase for comparison
    local lower_headers=$(echo "$headers" | tr '[:upper:]' '[:lower:]')
    local lower_name=$(echo "$column_name" | tr '[:upper:]' '[:lower:]')

    # Array of possible column names
    local names=($lower_name)

    # Add synonyms based on column type
    case "$lower_name" in
        "body"|"content"|"review")
            names+=("body" "content" "review" "text" "comment" "内容" "评价" "评论")
            ;;
        "rating"|"score"|"star")
            names+=("rating" "score" "stars" "star" "打分" "评分" "rating_score")
            ;;
        "date"|"time")
            names+=("date" "time" "timestamp" "created_at" "时间" "日期")
            ;;
        "title"|"summary")
            names+=("title" "summary" "subject" "标题")
            ;;
        "user"|"author"|"name")
            names+=("user" "username" "author" "reviewer_name" "用户")
            ;;
        "id")
            names+=("id" "review_id" "reviewid")
            ;;
    esac

    # Try to find matching column
    local index=1
    while IFS= read -r header; do
        local lower_header=$(echo "$header" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
        for name in "${names[@]}"; do
            local clean_name=$(echo "$name" | tr -d '[:space:]')
            if [[ "$lower_header" == *"$clean_name"* ]]; then
                echo $index
                return 0
            fi
        done
        ((index++))
    done <<< "$headers"

    # Not found
    echo "0"
}

# Function to get column value
get_column() {
    local line="$1"
    local col_index="$2"
    local delimiter="$3"

    if [[ "$col_index" -eq 0 ]]; then
        echo ""
        return
    fi

    # Use awk to properly handle CSV quoted fields
    awk -v idx="$col_index" -F"$delimiter" '{
        # Handle quoted fields
        for(i=1; i<=NF; i++) {
            # Remove leading/trailing quotes
            gsub(/^"|"$/, "", $i)
            # Unescape quotes
            gsub(/""/, "\"", $i)
            if(i == idx) {
                print $i
                exit
            }
        }
    }' <<< "$line"
}

# Function to count actual records (not lines, as CSV may have multi-line records)
count_records() {
    local file="$1"
    local delimiter="$2"

    # Use Python if available for accurate CSV parsing
    if command -v python3 &> /dev/null; then
        python3 -c "
import csv
import sys
try:
    with open('$file', 'r', encoding='utf-8', errors='ignore') as f:
        reader = csv.reader(f, delimiter='$delimiter')
        count = sum(1 for row in reader)
        print(count - 1)  # Exclude header
except:
    print(0)
" 2>/dev/null
    else
        # Fallback: subtract 1 for header, but this may be inaccurate
        local lines=$(wc -l < "$file")
        echo $((lines - 1))
    fi
}

# Function to extract reviews in JSON format
extract_reviews() {
    local file="$1"
    local delimiter="$2"
    local max_records="${3:-0}"

    # Use Python for accurate CSV parsing if available
    if command -v python3 &> /dev/null; then
        python3 -c "
import csv
import json
import sys

max_records = $max_records

with open('$file', 'r', encoding='utf-8', errors='ignore') as f:
    reader = csv.DictReader(f, delimiter='$delimiter')

    # Get headers
    headers = reader.fieldnames
    if not headers:
        sys.exit(1)

    # Find column indices
    body_col = None
    rating_col = None
    title_col = None
    id_col = None
    date_col = None

    for h in headers:
        hl = h.lower().strip()
        if hl in ['body', 'content', 'review', 'text', 'comment', '内容', '评价', '评论']:
            body_col = h
        elif hl in ['rating', 'score', 'stars', 'star', '打分', '评分']:
            rating_col = h
        elif hl in ['title', 'summary', 'subject', '标题']:
            title_col = h
        elif hl in ['id', 'review_id', 'reviewid']:
            id_col = h
        elif hl in ['date', 'time', 'timestamp', 'created_at', '时间', '日期']:
            date_col = h

    # Extract reviews
    reviews = []
    for i, row in enumerate(reader):
        if max_records > 0 and i >= max_records:
            break

        body = row.get(body_col, '').strip() if body_col else ''
        if not body:
            continue

        review = {
            'review_id': row.get(id_col, f'r{i+1}') if id_col else f'r{i+1}',
            'title': row.get(title_col, '').strip() if title_col else '',
            'body': body,
            'rating': row.get(rating_col, '3').strip() if rating_col else '3'
        }

        reviews.append(review)

    print(json.dumps(reviews, ensure_ascii=False, indent=2))
" 2>/dev/null
    else
        error "Python3 not found. Cannot parse CSV accurately."
        return 1
    fi
}

# Main function
main() {
    local file="$1"
    local action="${2:-info}"
    local max_records="${3:-0}"

    if [[ ! -f "$file" ]]; then
        error "File not found: $file"
        exit 1
    fi

    # Create temp file for UTF-8 conversion
    local temp_file=$(mktemp)
    trap "rm -f $temp_file" EXIT

    # Convert to UTF-8
    if ! convert_to_utf8 "$file" "$temp_file"; then
        exit 1
    fi

    # Detect delimiter
    local delimiter=$(detect_delimiter "$temp_file")
    info "Detected delimiter: '${delimiter}'"

    # Get headers
    local headers=$(head -n 1 "$temp_file")
    info "Headers: $headers"

    # Perform action
    case "$action" in
        "count")
            local count=$(count_records "$temp_file" "$delimiter")
            echo "$count"
            ;;
        "extract")
            extract_reviews "$temp_file" "$delimiter" "$max_records"
            ;;
        "info")
            local count=$(count_records "$temp_file" "$delimiter")
            info "Total records: $count"

            # Find key columns
            local body_idx=$(find_column "$headers" "body")
            local rating_idx=$(find_column "$headers" "rating")
            local title_idx=$(find_column "$headers" "title")

            info "Body column index: $body_idx"
            info "Rating column index: $rating_idx"
            info "Title column index: $title_idx"
            ;;
        *)
            error "Unknown action: $action"
            echo "Usage: $0 <file> <action> [max_records]"
            echo "Actions: info, count, extract"
            exit 1
            ;;
    esac
}

# Run main function
main "$@"


path: utils/html_generator.sh
content: #!/bin/bash

# HTML Generator Module for Review Analyzer
# Generates interactive HTML visualization dashboard

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEMPLATES_DIR="$(dirname "$SCRIPT_DIR")/templates"

# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# Function to print progress
progress() {
    echo -e "${BLUE}[PROGRESS]${NC} $1"
}

success() {
    echo -e "${GREEN}[SUCCESS]${NC} $1"
}

error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# Function to escape HTML special characters
escape_html() {
    local text="$1"
    text="${text//&/&amp;}"
    text="${text//</&lt;}"
    text="${text//>/&gt;}"
    text="${text//\"/&quot;}"
    text="${text//\'/&#39;}"
    echo "$text"
}

# Function to convert markdown to HTML (basic)
markdown_to_html() {
    local md="$1"

    # Headers
    md=$(echo "$md" | sed 's/^# \(.*\)/<h1>\1<\/h1>/')
    md=$(echo "$md" | sed 's/^## \(.*\)/<h2>\1<\/h2>/')
    md=$(echo "$md" | sed 's/^### \(.*\)/<h3>\1<\/h3>/')

    # Bold
    md=$(echo "$md" | sed 's/\*\*\([^*]*\)\*\*/<strong>\1<\/strong>/g')

    # Italic
    md=$(echo "$md" | sed 's/\*\([^*]*\)\*/<em>\1<\/em>/g')

    # Links
    md=$(echo "$md" | sed 's/\[\([^]]*\)\](\([^)]*\))/<a href="\2">\1<\/a>/g')

    # Code blocks (basic)
    md=$(echo "$md" | sed 's/`\([^`]*\)`/<code>\1<\/code>/g')

    # Blockquotes
    md=$(echo "$md" | sed 's/^> \(.*\)/<blockquote>\1<\/blockquote>/')

    # Unordered lists (basic - single line)
    md=$(echo "$md" | sed 's/^- \(.*\)/<li>\1<\/li>/g')

    # Paragraphs (wrap text lines)
    # This is simplified; real markdown parsing is more complex

    echo "$md"
}

# Function to generate sentiment bar HTML
generate_sentiment_bar() {
    local label="$1"
    local count="$2"
    local total="$3"
    local color="$4"

    local percent=0
    if [[ "$total" -gt 0 ]]; then
        percent=$(awk "BEGIN {printf \"%.1f\", ($count / $total) * 100}")
    fi

    cat << EOF
                    <div class="sentiment-bar">
                        <div class="sentiment-label">$label</div>
                        <div class="sentiment-track">
                            <div class="sentiment-fill" style="width: $percent%; --sentiment-color: $color;"></div>
                        </div>
                        <div class="sentiment-value">$count ($percent%)</div>
                    </div>
EOF
}

# Function to generate persona card HTML
generate_persona_card() {
    local name="$1"
    local count="$2"
    local color="$3"
    local tags_json="$4"

    # Extract first few tags
    local tags_html=""
    if command -v jq &> /dev/null; then
        local tag_count=$(echo "$tags_json" | jq '. | length' 2>/dev/null || echo "0")
        local i=0
        while [[ $i -lt $tag_count && $i -lt 6 ]]; do
            local key=$(echo "$tags_json" | jq -r ".keys()[$i]" 2>/dev/null)
            local value=$(echo "$tags_json" | jq -r ".\"$key\"" 2>/dev/null)

            if [[ "$value" && "$value" != "未提及" && "$value" != "不明" && "$value" != "无" ]]; then
                tags_html="$tags_html                        <div class=\"persona-tag\">
                            ${key}: <span class=\"persona-tag-value\">${value}</span>
                        </div>
"
            fi
            ((i++))
        done
    fi

    cat << EOF
                <div class="persona-card" style="--persona-color: $color;">
                    <div class="persona-header">
                        <div class="persona-name">$name</div>
                        <div class="persona-count">$count 条</div>
                    </div>
                    <div class="persona-tags">
$tags_html                    </div>
                </div>
EOF
}

# Function to generate tag item HTML
generate_tag_item() {
    local tag_name="$1"
    local count="$2"

    cat << EOF
                <div class="tag-item">
                    <span class="tag-name">$tag_name</span>
                    <span class="tag-count">$count</span>
                </div>
EOF
}

# Function to generate sample card HTML
generate_sample_card() {
    local sentiment="$1"
    local rating="$2"
    local body="$3"
    local info_score="$4"

    # Determine sentiment class
    local sentiment_class="neutral"
    case "$sentiment" in
        "强烈推荐") sentiment_class="strong-positive" ;;
        "推荐") sentiment_class="positive" ;;
        "中立") sentiment_class="neutral" ;;
        "不推荐") sentiment_class="negative" ;;
        "强烈不推荐") sentiment_class="strong-negative" ;;
    esac

    # Generate stars
    local stars=""
    local rating_int=${rating%.*}
    for ((i=0; i<rating_int && i<5; i++)); do
        stars="$stars                            <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"currentColor\">
                                <path d=\"M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z\"/>
                            </svg>
"
    done

    # Escape body
    local body_escaped=$(escape_html "$body")

    cat << EOF
                <div class="sample-card" onclick="this.classList.toggle('expanded')">
                    <div class="sample-header">
                        <span class="sample-sentiment $sentiment_class">$sentiment</span>
                        <div class="sample-rating">
$stars                        </div>
                    </div>
                    <div class="sample-body">
                        <p class="sample-text">$body_escaped</p>
EOF

    if [[ -n "$info_score" ]]; then
        cat << EOF
                        <div class="sample-tags">
                            <span class="sample-tag" style="border-color: var(--accent-gold); color: var(--accent-gold);">信息密度: $info_score/20</span>
                        </div>
EOF
    fi

    cat << EOF
                    </div>
                </div>
EOF
}

# Function to generate HTML report
generate_html_report() {
    local stats_json="$1"
    local tagged_data="$2"
    local insights_md="$3"
    local output_file="$4"

    progress "Generating HTML visualization dashboard..."

    # Extract statistics
    local total=$(echo "$stats_json" | jq -r '.total // 0')
    local tagged=$(echo "$stats_json" | jq -r '.tagged // 0')
    local avg_rating=$(echo "$stats_json" | jq -r '.avg_rating // 0')

    # Get current date
    local analysis_date=$(date +"%Y年%m月%d日")

    # Determine ASIN/product name
    local asin="UNKNOWN"
    local product_name="产品评论分析"

    # Read template
    local template="$TEMPLATES_DIR/report.html"
    if [[ ! -f "$template" ]]; then
        # Use fallback minimal template
        template=""
    fi

    # Build HTML content
    local html_content=""

    if [[ -n "$template" ]]; then
        # Read and process template
        html_content=$(cat "$template")

        # Replace placeholders (using sed for simple replacements)
        html_content="${html_content//\{\{ asin \}\}/$asin}"
        html_content="${html_content//\{\{ analysis_date \}\}/$analysis_date}"
        html_content="${html_content//\{\{ product_name \}\}/$product_name}"
        html_content="${html_content//\{\{ summary.total_reviews \}\}/$total}"
        html_content="${html_content//\{\{ summary.tagged_reviews \}\}/$tagged}"
        html_content="${html_content//\{\{ summary.avg_rating \}\}/$avg_rating}"

        # Note: Complex Jinja2 template processing would require Python/jinja2
        # For bash-only solution, we need a simpler template or pre-processing
    else
        # Generate minimal HTML
        html_content=$(generate_minimal_html "$stats_json" "$tagged_data" "$insights_md")
    fi

    # Write output
    echo "$html_content" > "$output_file"

    success "HTML report generated: $output_file"
}

# Function to generate minimal HTML (fallback)
generate_minimal_html() {
    local stats_json="$1"
    local tagged_data="$2"
    local insights_md="$3"

    local total=$(echo "$stats_json" | jq -r '.total // 0')
    local tagged=$(echo "$stats_json" | jq -r '.tagged // 0')
    local date=$(date +"%Y年%m月%d日")

    # Convert insights markdown to HTML
    local insights_html=$(markdown_to_html "$insights_md")

    cat << EOF
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>评论分析报告</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #0d1117; color: #f0f6fc; line-height: 1.6; padding: 2rem; }
        .container { max-width: 1200px; margin: 0 auto; }
        h1 { color: #d29922; border-bottom: 1px solid rgba(210, 153, 34, 0.2); padding-bottom: 1rem; }
        h2 { color: #f0f6fc; margin-top: 2rem; }
        .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin: 2rem 0; }
        .stat-card { background: #161b22; padding: 1.5rem; border-left: 3px solid #d29922; }
        .stat-value { font-size: 2rem; font-weight: bold; color: #f0f6fc; }
        .stat-label { color: #8b949e; font-size: 0.875rem; text-transform: uppercase; }
        .insights { background: #161b22; padding: 2rem; border-radius: 8px; }
        .footer { text-align: center; margin-top: 3rem; color: #6e7681; font-size: 0.875rem; }
    </style>
</head>
<body>
    <div class="container">
        <h1>评论深度洞察报告</h1>
        <p style="color: #8b949e;">分析时间: $date · 样本量: $total 条</p>

        <div class="stats">
            <div class="stat-card">
                <div class="stat-value">$total<span style="font-size: 1rem; color: #8b949e;"> 条</span></div>
                <div class="stat-label">Total Reviews</div>
            </div>
            <div class="stat-card">
                <div class="stat-value">$tagged<span style="font-size: 1rem; color: #8b949e;"> 条</span></div>
                <div class="stat-label">Tagged</div>
            </div>
        </div>

        <h2>深度洞察分析</h2>
        <div class="insights">
$insights_html
        </div>

        <div class="footer">
            <p>Generated by AI · $date</p>
            <p>Powered by Claude AI · 22-Dimension Tag System</p>
        </div>
    </div>
</body>
</html>
EOF
}

# Main function
main() {
    local action="$1"
    shift

    case "$action" in
        "generate")
            generate_html_report "$@"
            ;;
        "markdown")
            markdown_to_html "$@"
            ;;
        *)
            echo "Usage: $0 {generate|markdown} ..."
            exit 1
            ;;
    esac
}

main "$@"


path: utils/insights_generator.sh
content: #!/bin/bash

# Insights Generator Module for Review Analyzer
# Generates deep insights report from tagged reviews

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROMPTS_DIR="$(dirname "$SCRIPT_DIR")/prompts"

# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# Function to print progress
progress() {
    echo -e "${BLUE}[PROGRESS]${NC} $1"
}

success() {
    echo -e "${GREEN}[SUCCESS]${NC} $1"
}

error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# Function to calculate statistics from tagged reviews
calculate_statistics() {
    local tagged_json="$1"

    if ! command -v python3 &> /dev/null; then
        error "Python3 required for statistics calculation"
        return 1
    fi

    python3 << 'PYTHON_SCRIPT'
import json
import sys
from collections import Counter
from datetime import datetime

def main():
    # Load tagged data
    try:
        with open(sys.argv[1], 'r', encoding='utf-8') as f:
            data = json.load(f)
    except:
        print(json.dumps({"error": "Failed to load tagged data"}))
        sys.exit(1)

    if not isinstance(data, list):
        data = [data]

    total = len(data)
    tagged = sum(1 for item in data if item.get('tags'))

    # Sentiment distribution
    sentiment_dist = Counter()
    for item in data:
        s = item.get('sentiment', 'unknown')
        sentiment_dist[s] += 1

    # Top tags extraction
    all_tags = Counter()
    for item in data:
        tags = item.get('tags', {})
        for key, value in tags.items():
            if value and value not in ['未提及', '不明', 'unknown']:
                tag_name = f"{key}:{value}"
                all_tags[tag_name] += 1

    top_tags = dict(all_tags.most_common(15))

    # User personas clustering (simplified)
    personas = []

    # Group by unique combinations of gender + age + occupation
    persona_groups = {}
    for item in data:
        tags = item.get('tags', {})
        gender = tags.get('人群_性别', '不明')
        age = tags.get('人群_年龄段', '不明')
        occupation = tags.get('人群_职业', '不明')

        key = f"{gender}_{age}_{occupation}"
        if key not in persona_groups:
            persona_groups[key] = {
                'name': f"{occupation} ({age}, {gender})",
                'count': 0,
                'tags': {}
            }
        persona_groups[key]['count'] += 1
        persona_groups[key]['tags'] = tags

    # Convert to list and sort by count
    for p in persona_groups.values():
        personas.append(p)

    personas.sort(key=lambda x: x['count'], reverse=True)

    # Output statistics
    stats = {
        'total': total,
        'tagged': tagged,
        'sentiment': dict(sentiment_dist),
        'top_tags': top_tags,
        'personas': personas[:4]  # Top 4 personas
    }

    print(json.dumps(stats, ensure_ascii=False, indent=2))

if __name__ == '__main__':
    main()
PYTHON_SCRIPT
}

# Function to select golden samples
select_golden_samples() {
    local tagged_json="$1"
    local count="${2:-12}"

    if ! command -v python3 &> /dev/null; then
        error "Python3 required for sample selection"
        return 1
    fi

    python3 << 'PYTHON_SCRIPT'
import json
import sys

def main():
    # Load tagged data
    try:
        with open(sys.argv[1], 'r', encoding='utf-8') as f:
            data = json.load(f)
    except:
        print(json.dumps([]))
        sys.exit(1)

    if not isinstance(data, list):
        data = [data]

    # Sort by info_score
    scored = [(item, item.get('info_score', 0)) for item in data]
    scored.sort(key=lambda x: x[1], reverse=True)

    # Select top samples
    count = int(sys.argv[2]) if len(sys.argv) > 2 else 12
    samples = [item for item, score in scored[:count]]

    print(json.dumps(samples, ensure_ascii=False, indent=2))

if __name__ == '__main__':
    main()
PYTHON_SCRIPT
}

# Function to format personas details
format_personas() {
    local personas_json="$1"

    if ! command -v python3 &> /dev/null; then
        echo "$personas_json"
        return
    fi

    python3 << 'PYTHON_SCRIPT'
import json
import sys

def main():
    try:
        personas = json.load(sys.stdin)
    except:
        return

    for i, p in enumerate(personas):
        print(f"### 画像 {i+1}: {p['name']} ({p['count']} 条)")

        tags = p.get('tags', {})
        valid_tags = [f"{k}:{v}" for k, v in tags.items()
                     if v and v not in ['未提及', '不明', 'unknown']]

        if valid_tags:
            print(f"标签特征: {', '.join(valid_tags[:8])}")
        print()

if __name__ == '__main__':
    main()
PYTHON_SCRIPT
}

# Function to format sentiment distribution
format_sentiment() {
    local stats_json="$1"
    local total="$2"

    if ! command -v python3 &> /dev/null; then
        return
    fi

    python3 << 'PYTHON_SCRIPT'
import json
import sys

def main():
    try:
        stats = json.load(sys.stdin)
        sentiment = stats.get('sentiment', {})
        total = stats.get('total', 1)

        for s, c in sentiment.items():
            pct = (c / total * 100) if total > 0 else 0
            print(f"- **{s}**: {c} 条 ({pct:.1f}%)")
    except:
        pass

if __name__ == '__main__':
    main()
PYTHON_SCRIPT
}

# Function to format top tags
format_top_tags() {
    local stats_json="$1"

    if ! command -v python3 &> /dev/null; then
        return
    fi

    python3 << 'PYTHON_SCRIPT'
import json
import sys

def main():
    try:
        stats = json.load(sys.stdin)
        top_tags = stats.get('top_tags', {})

        for i, (tag, count) in enumerate(list(top_tags.items())[:15]):
            print(f"{i+1}. **{tag}**: {count} 次")
    except:
        pass

if __name__ == '__main__':
    main()
PYTHON_SCRIPT
}

# Function to format golden samples
format_samples() {
    local samples_json="$1"

    if ! command -v python3 &> /dev/null; then
        return
    fi

    python3 << 'PYTHON_SCRIPT'
import json
import sys

def main():
    try:
        samples = json.load(sys.stdin)
    except:
        return

    for i, s in enumerate(samples):
        print(f"### 样本 {i+1}")
        print(f"**情感**: {s.get('sentiment', '不明')}")
        print(f"**内容**: {s.get('body', '')[:200]}...")

        tags = s.get('tags', {})
        valid_tags = [f"{k}:{v}" for k, v in tags.items()
                     if v and v not in ['未提及', '不明', 'unknown']]

        if valid_tags:
            print(f"**标签**: {', '.join(valid_tags[:6])}")
        print()

if __name__ == '__main__':
    main()
PYTHON_SCRIPT
}

# Function to create insights prompt
create_insights_prompt() {
    local stats_json="$1"
    local personas_json="$2"
    local samples_json="$3"
    local asin="${4:-unknown}"
    local product_name="${5:-}"

    local prompt_template="$PROMPTS_DIR/insights.txt"

    if [[ ! -f "$prompt_template" ]]; then
        error "Prompt template not found: $prompt_template"
        return 1
    fi

    # Extract statistics
    local total=$(echo "$stats_json" | jq -r '.total // 0')
    local tagged=$(echo "$stats_json" | jq -r '.tagged // 0')

    # Format sections
    local personas_details=$(echo "$personas_json" | format_personas)
    local sentiment_distribution=$(echo "$stats_json" | format_sentiment "$total")
    local top_tags=$(echo "$stats_json" | format_top_tags)
    local golden_samples=$(echo "$samples_json" | format_samples)

    # Get current date
    local date=$(date +"%Y-%m-%d")

    # Read and format prompt
    local prompt=$(cat "$prompt_template")
    prompt="${prompt//{total}/$total}"
    prompt="${prompt//{tagged}/$tagged}"
    prompt="${prompt//{asin}/$asin}"
    prompt="${prompt//{product_name}/$product_name}"
    prompt="${prompt//{date}/$date}"
    prompt="${prompt//{personas_count}/$(echo "$personas_json" | jq '. | length')}"
    prompt="${prompt//{personas_details}/$personas_details}"
    prompt="${prompt//{sentiment_distribution}/$sentiment_distribution}"
    prompt="${prompt//{top_tags}/$top_tags}"
    prompt="${prompt//{samples_count}/$(echo "$samples_json" | jq '. | length')}"
    prompt="${prompt//{golden_samples}/$golden_samples}"

    echo "$prompt"
}

# Function to extract strategic JSON from report
extract_strategic_json() {
    local report_file="$1"

    # Extract JSON block between <strategic_json> tags
    sed -n '/<strategic_json>/,/<\/strategic_json>/p' "$report_file" | \
        sed '1d;$d' | \
        jq '.' 2>/dev/null || echo '{}'
}

# Function to validate insights report
validate_insights_report() {
    local report_file="$1"

    # Check required sections
    local required_sections=(
        "洞察总览"
        "数据统计"
        "核心用户画像"
        "核心卖点"
        "痛点"
        "改进建议"
        "潜在机会"
        "典型用户"
        "洞察总结"
        "strategic_json"
    )

    local missing=()
    for section in "${required_sections[@]}"; do
        if ! grep -q "$section" "$report_file"; then
            missing+=("$section")
        fi
    done

    if [[ ${#missing[@]} -gt 0 ]]; then
        error "Missing sections in report: ${missing[*]}"
        return 1
    fi

    # Validate strategic JSON
    local strategic_json=$(extract_strategic_json "$report_file")
    if ! echo "$strategic_json" | jq '.moat and .vulnerability and .execution_matrix' > /dev/null 2>&1; then
        error "Invalid strategic JSON structure"
        return 1
    fi

    return 0
}

# Main function
main() {
    local action="$1"
    shift

    case "$action" in
        "stats")
            calculate_statistics "$@"
            ;;
        "samples")
            select_golden_samples "$@"
            ;;
        "prompt")
            create_insights_prompt "$@"
            ;;
        "validate")
            validate_insights_report "$@"
            ;;
        "extract-json")
            extract_strategic_json "$@"
            ;;
        *)
            echo "Usage: $0 {stats|samples|prompt|validate|extract-json} ..."
            exit 1
            ;;
    esac
}

main "$@"


path: utils/merge_csv.py
content: #!/usr/bin/env python3
"""
CSV Merge Utility for Review Analyzer
Merges original CSV with tagged data
"""

import csv
import json
import sys
import os
from datetime import datetime


def main():
    if len(sys.argv) < 3:
        print("Usage: merge_csv.py <original_csv> <tagged_json> <output_csv>")
        sys.exit(1)

    original_csv = sys.argv[1]
    tagged_json = sys.argv[2]
    output_csv = sys.argv[3]

    # Load tagged data
    try:
        with open(tagged_json, 'r', encoding='utf-8') as f:
            tagged_data = json.load(f)
    except Exception as e:
        print(f"Error loading tagged data: {e}")
        sys.exit(1)

    # Create lookup
    tagged_lookup = {item.get('review_id', ''): item for item in tagged_data}

    # Read original CSV and append tags
    try:
        with open(original_csv, 'r', encoding='utf-8', errors='ignore') as infile, \
             open(output_csv, 'w', encoding='utf-8', newline='', errors='ignore') as outfile:

            reader = csv.DictReader(infile)
            fieldnames = list(reader.fieldnames or [])

            # Add new columns
            new_columns = ['情感_总体评价', '评论价值打分', '打标时间']

            # Collect all tag keys
            all_tag_keys = set()
            for item in tagged_data:
                for tag_key in item.get('tags', {}).keys():
                    all_tag_keys.add(tag_key)

            # Sort tag keys for consistency
            new_columns.extend(sorted(all_tag_keys))

            # Write header
            fieldnames.extend(new_columns)
            writer = csv.DictWriter(outfile, fieldnames=fieldnames)
            writer.writeheader()

            # Process rows
            written_count = 0
            for row in reader:
                # Find review ID
                rid = (row.get('review_id') or
                       row.get('id') or
                       row.get('Id') or
                       row.get('ID') or
                       f"row_{written_count + 1}")

                tagged_item = tagged_lookup.get(rid)

                if tagged_item:
                    # Add basic fields
                    row['情感_总体评价'] = tagged_item.get('sentiment', '')
                    row['评论价值打分'] = tagged_item.get('info_score', '')
                    row['打标时间'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

                    # Add tags
                    tags = tagged_item.get('tags', {})
                    for tag_key in all_tag_keys:
                        tag_value = tags.get(tag_key, '未提及')
                        row[tag_key] = tag_value
                    written_count += 1
                else:
                    # Add empty columns
                    row['情感_总体评价'] = ''
                    row['评论价值打分'] = ''
                    row['打标时间'] = ''
                    for tag_key in all_tag_keys:
                        row[tag_key] = '未提及'

                writer.writerow(row)

        print(f"Merged {written_count} tagged reviews to {output_csv}")

    except Exception as e:
        print(f"Error processing CSV: {e}")
        sys.exit(1)


if __name__ == '__main__':
    main()


path: utils/tagging_core.sh
content: #!/bin/bash

# Tagging Core Module for Review Analyzer
# Handles single and batch review tagging using Claude AI

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROMPTS_DIR="$(dirname "$SCRIPT_DIR")/prompts"

# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# Progress tracking
declare -a TAGGING_RESULTS=()
TAGGING_SUCCESS=0
TAGGING_FAILED=0

# Function to print progress
progress() {
    echo -e "${BLUE}[PROGRESS]${NC} $1"
}

success() {
    echo -e "${GREEN}[SUCCESS]${NC} $1"
}

error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# Function to format a review for tagging
format_review() {
    local review_json="$1"

    # Extract fields using jq if available, else use grep/sed
    if command -v jq &> /dev/null; then
        echo "$review_json" | jq -r '"[\(.review_id // "unknown")] \(.title // "No title") - \(.body // "")"'
    else
        echo "$review_json"
    fi
}

# Function to create single tagging prompt
create_single_prompt() {
    local review_json="$1"
    local prompt_template="$PROMPTS_DIR/tagging.txt"

    if [[ ! -f "$prompt_template" ]]; then
        error "Prompt template not found: $prompt_template"
        return 1
    fi

    # Extract review data
    local review_id=$(echo "$review_json" | grep -o '"review_id"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: "\(.*\)".*/\1/')
    local title=$(echo "$review_json" | grep -o '"title"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: "\(.*\)".*/\1/' | head -1)
    local body=$(echo "$review_json" | grep -o '"body"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: "\(.*\)".*/\1/' | head -1)
    local rating=$(echo "$review_json" | grep -o '"rating"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*: "\(.*\)".*/\1/' | head -1)

    # Set defaults
    review_id=${review_id:-"unknown"}
    title=${title:-""}
    body=${body:-""}
    rating=${rating:-"3"}

    # Read and format prompt
    local prompt=$(cat "$prompt_template")
    prompt="${prompt//{review_id}/$review_id}"
    prompt="${prompt//{title}/$title}"
    prompt="${prompt//{body}/$body}"
    prompt="${prompt//{rating}/$rating}"

    echo "$prompt"
}

# Function to create batch tagging prompt
create_batch_prompt() {
    local reviews_json="$1"
    local prompt_template="$PROMPTS_DIR/tagging_batch.txt"
    local batch_size=$(echo "$reviews_json" | jq '. | length' 2>/dev/null || echo "1")

    if [[ ! -f "$prompt_template" ]]; then
        error "Prompt template not found: $prompt_template"
        return 1
    fi

    # Read and format prompt
    local prompt=$(cat "$prompt_template")
    prompt="${prompt//{batch_size}/$batch_size}"
    prompt="${prompt//{reviews_json}/$reviews_json}"

    echo "$prompt"
}

# Function to parse tagging result
parse_tagging_result() {
    local result="$1"
    local review_id="$2"

    # Extract JSON from result (handles markdown code blocks)
    local json_data=$(echo "$result" | sed -n '/```json/,/```/p' | sed '1d;$d' || echo "$result")

    # Validate JSON structure
    if command -v jq &> /dev/null; then
        if ! echo "$json_data" | jq '.' > /dev/null 2>&1; then
            error "Invalid JSON result for review: $review_id"
            return 1
        fi

        # Ensure required fields exist
        local has_review_id=$(echo "$json_data" | jq 'has("review_id")')
        local has_tags=$(echo "$json_data" | jq 'has("tags")')

        if [[ "$has_review_id" != "true" ]] || [[ "$has_tags" != "true" ]]; then
            error "Missing required fields in result for: $review_id"
            return 1
        fi
    fi

    echo "$json_data"
    return 0
}

# Function to tag a single review
tag_single_review() {
    local review_json="$1"

    progress "Tagging review: $(format_review "$review_json")"

    # Create prompt
    local prompt=$(create_single_prompt "$review_json")
    if [[ $? -ne 0 ]]; then
        ((TAGGING_FAILED++))
        return 1
    fi

    # The actual AI call will be done by the main skill
    # This function prepares the prompt and returns it
    echo "$prompt"
}

# Function to tag a batch of reviews
tag_batch_reviews() {
    local reviews_json="$1"
    local batch_size=$(echo "$reviews_json" | jq '. | length' 2>/dev/null || echo "1")

    progress "Tagging batch of $batch_size reviews..."

    # Create prompt
    local prompt=$(create_batch_prompt "$reviews_json")
    if [[ $? -ne 0 ]]; then
        return 1
    fi

    # The actual AI call will be done by the main skill
    echo "$prompt"
}

# Function to split reviews into batches
split_into_batches() {
    local reviews_json="$1"
    local batch_size="${2:-30}"
    local total=$(echo "$reviews_json" | jq '. | length' 2>/dev/null || echo "0")

    if [[ "$total" -le "$batch_size" ]]; then
        echo "$reviews_json"
        return 0
    fi

    # Split into batches
    for ((i=0; i<total; i+=batch_size)); do
        local end=$((i + batch_size - 1))
        if [[ $end -ge $total ]]; then
            end=$((total - 1))
        fi
        echo "$reviews_json" | jq ".[$i:$end]"
    done
}

# Function to merge tagging results
merge_results() {
    local all_results="["

    local first=true
    for result in "${TAGGING_RESULTS[@]}"; do
        if [[ "$first" == "true" ]]; then
            all_results="$all_results$result"
            first=false
        else
            all_results="$all_results,$result"
        fi
    done

    all_results="$all_results]"

    echo "$all_results"
}

# Function to save results to CSV
save_to_csv() {
    local results_json="$1"
    local original_csv="$2"
    local output_csv="$3"

    if ! command -v python3 &> /dev/null; then
        error "Python3 required for CSV output"
        return 1
    fi

    python3 -c "
import csv
import json
import sys

# Load tagging results
with open('$results_json', 'r', encoding='utf-8') as f:
    tagged = json.load(f)

# Create lookup
tagged_lookup = {item['review_id']: item for item in tagged}

# Read original CSV and append tags
with open('$original_csv', 'r', encoding='utf-8', errors='ignore') as infile, \\
     open('$output_csv', 'w', encoding='utf-8', newline='', errors='ignore') as outfile:

    reader = csv.DictReader(infile)
    fieldnames = reader.fieldnames + ['情感_总体评价', '评论价值打分', '打标时间']

    # Add all tag columns
    all_tags = set()
    for item in tagged:
        for tag_key in item.get('tags', {}).keys():
            all_tags.add(tag_key)
    fieldnames.extend(sorted(all_tags))

    writer = csv.DictWriter(outfile, fieldnames=fieldnames)
    writer.writeheader()

    for row in reader:
        # Find review ID
        rid = row.get('review_id') or row.get('id') or row.get('Id') or ''
        tagged_item = tagged_lookup.get(rid)

        if tagged_item:
            row['情感_总体评价'] = tagged_item.get('sentiment', '')
            row['评论价值打分'] = tagged_item.get('info_score', '')
            row['打标时间'] = tagged_item.get('timestamp', '')

            # Add all tags
            for tag_key in all_tags:
                tag_value = tagged_item.get('tags', {}).get(tag_key, '未提及')
                row[tag_key] = tag_value

        writer.writerow(row)

print(f'Saved {len(tagged)} tagged reviews to {output_csv}')
"
}

# Main function
main() {
    local action="$1"
    shift

    case "$action" in
        "single")
            tag_single_review "$@"
            ;;
        "batch")
            tag_batch_reviews "$@"
            ;;
        "split")
            split_into_batches "$@"
            ;;
        "merge")
            merge_results
            ;;
        "csv")
            save_to_csv "$@"
            ;;
        *)
            echo "Usage: $0 {single|batch|split|merge|csv} ..."
            exit 1
            ;;
    esac
}

main "$@"


path: utils/test.sh
content: #!/bin/bash

# Test Script for Review Analyzer Skill
# Runs a complete analysis workflow using sample data

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"

# Color codes
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Simple Review Analyzer - 测试脚本${NC}"
echo -e "${BLUE}========================================${NC}"

# Test 1: Check directory structure
echo -e "\n${YELLOW}[测试 1] 检查目录结构...${NC}"
required_dirs=("$ROOT_DIR/prompts" "$ROOT_DIR/templates" "$ROOT_DIR/utils" "$ROOT_DIR/output")
all_exist=true
for dir in "${required_dirs[@]}"; do
    if [[ -d "$dir" ]]; then
        echo -e "  ${GREEN}✓${NC} $dir"
    else
        echo -e "  ${RED}✗${NC} $dir (不存在)"
        all_exist=false
    fi
done

if [[ "$all_exist" == "true" ]]; then
    echo -e "${GREEN}所有目录检查通过${NC}"
else
    echo -e "${RED}目录结构检查失败${NC}"
    exit 1
fi

# Test 2: Check required files
echo -e "\n${YELLOW}[测试 2] 检查必需文件...${NC}"
required_files=(
    "$ROOT_DIR/skill.md"
    "$ROOT_DIR/prompts/tagging.txt"
    "$ROOT_DIR/prompts/tagging_batch.txt"
    "$ROOT_DIR/prompts/insights.txt"
    "$ROOT_DIR/templates/report.html"
    "$ROOT_DIR/utils/csv_reader.sh"
    "$ROOT_DIR/utils/tagging_core.sh"
    "$ROOT_DIR/utils/insights_generator.sh"
    "$ROOT_DIR/utils/html_generator.sh"
    "$ROOT_DIR/utils/analyzer_controller.sh"
    "$ROOT_DIR/utils/merge_csv.py"
)

all_exist=true
for file in "${required_files[@]}"; do
    if [[ -f "$file" ]]; then
        echo -e "  ${GREEN}✓${NC} $(basename "$file")"
    else
        echo -e "  ${RED}✗${NC} $(basename "$file") (不存在)"
        all_exist=false
    fi
done

if [[ "$all_exist" == "true" ]]; then
    echo -e "${GREEN}所有文件检查通过${NC}"
else
    echo -e "${RED}文件检查失败${NC}"
    exit 1
fi

# Test 3: Check script permissions
echo -e "\n${YELLOW}[测试 3] 检查脚本权限...${NC}"
scripts=(
    "$ROOT_DIR/utils/csv_reader.sh"
    "$ROOT_DIR/utils/tagging_core.sh"
    "$ROOT_DIR/utils/insights_generator.sh"
    "$ROOT_DIR/utils/html_generator.sh"
    "$ROOT_DIR/utils/analyzer_controller.sh"
)

all_executable=true
for script in "${scripts[@]}"; do
    if [[ -x "$script" ]]; then
        echo -e "  ${GREEN}✓${NC} $(basename "$script") (可执行)"
    else
        echo -e "  ${YELLOW}⚠${NC} $(basename "$script") (不可执行，运行: chmod +x $script)"
        all_executable=false
    fi
done

# Test 4: Check Python
echo -e "\n${YELLOW}[测试 4] 检查 Python 环境...${NC}"
if command -v python3 &> /dev/null; then
    py_version=$(python3 --version 2>&1)
    echo -e "  ${GREEN}✓${NC} Python3: $py_version"
else
    echo -e "  ${YELLOW}⚠${NC} Python3 未安装 (CSV 合并功能需要)"
fi

# Test 5: Check jq
echo -e "\n${YELLOW}[测试 5] 检查 JSON 工具...${NC}"
if command -v jq &> /dev/null; then
    jq_version=$(jq --version 2>&1)
    echo -e "  ${GREEN}✓${NC} jq: $jq_version"
else
    echo -e "  ${YELLOW}⚠${NC} jq 未安装 (JSON 处理功能需要)"
fi

# Test 6: Validate prompts
echo -e "\n${YELLOW}[测试 6] 验证提示词模板...${NC}"
prompts=(
    "$ROOT_DIR/prompts/tagging.txt"
    "$ROOT_DIR/prompts/tagging_batch.txt"
    "$ROOT_DIR/prompts/insights.txt"
)

all_valid=true
for prompt in "${prompts[@]}"; do
    if grep -q "{review_id}" "$prompt" 2>/dev/null || \
       grep -q "{batch_size}" "$prompt" 2>/dev/null || \
       grep -q "{total}" "$prompt" 2>/dev/null; then
        echo -e "  ${GREEN}✓${NC} $(basename "$prompt") (包含占位符)"
    else
        echo -e "  ${YELLOW}⚠${NC} $(basename "$prompt") (可能缺少占位符)"
        all_valid=false
    fi
done

# Test 7: Sample data check
echo -e "\n${YELLOW}[测试 7] 检查示例数据...${NC}"
sample_csv="../assets/examples/reviews_sample.csv"
if [[ -f "$sample_csv" ]]; then
    echo -e "  ${GREEN}✓${NC} 示例 CSV 文件存在"
    line_count=$(wc -l < "$sample_csv" 2>/dev/null || echo "0")
    echo -e "    行数: $line_count"
else
    echo -e "  ${YELLOW}⚠${NC} 示例 CSV 文件不存在 ($sample_csv)"
fi

# Summary
echo -e "\n${BLUE}========================================${NC}"
echo -e "${BLUE}测试总结${NC}"
echo -e "${BLUE}========================================${NC}"

if [[ "$all_exist" == "true" && "$all_valid" == "true" ]]; then
    echo -e "${GREEN}✓ 所有测试通过！Skill 已准备就绪。${NC}"
    echo -e "\n${YELLOW}下一步：${NC}"
    echo -e "  1. 在 Claude Code 中加载此 Skill"
    echo -e "  2. 使用自然语言调用：\"请分析评论：reviews.csv\""
    exit 0
else
    echo -e "${RED}✗ 部分测试失败，请检查上述错误。${NC}"
    exit 1
fi


path: utils/transform_logic.py
content: #!/usr/bin/env python3
"""
评论数据转换工具

完整数据流：
1. 原始CSV (reviews_sample.csv)
2. AI分析生成标签 (tagged_reviews.json)
3. 扁平化输出CSV (reviews_labeled_*.csv)

支持双向转换：
- JSON -> CSV: json_to_csv()
- CSV -> JSON: csv_to_json()
"""

import json
import csv
import sys
from pathlib import Path

def json_to_csv(json_file, csv_file):
    """
    将嵌套JSON结构的标签数据转换为扁平CSV格式

    JSON结构：
    {
      "review_id": "...",
      "body": "...",
      "rating": 5.0,
      "sentiment": "...",
      "info_score": 15,
      "tags": {
        "人群_性别": "...",
        "场景_使用场景": "...",
        ...
      }
    }

    输出CSV：将tags内的字段展开为独立列
    """

    # 读取JSON数据
    with open(json_file, 'r', encoding='utf-8') as f:
        reviews = json.load(f)

    if not reviews:
        print("错误：JSON文件为空")
        return

    # CSV列定义：原始列 + 分析列 + 22维度标签列
    base_columns = ['review_id', 'body', 'rating', 'date', 'sentiment', 'info_score']

    # 22维度标签列（按8大维度分组）
    tag_columns = [
        # 人群维度 (4)
        '人群_性别', '人群_年龄段', '人群_职业', '人群_购买角色',
        # 场景维度 (1)
        '场景_使用场景',
        # 功能维度 (2)
        '功能_满意度', '功能_具体功能',
        # 质量维度 (3)
        '质量_材质', '质量_做工', '质量_耐用性',
        # 服务维度 (5)
        '服务_发货速度', '服务_包装质量', '服务_客服响应',
        '服务_退换货', '服务_保修',
        # 体验维度 (4)
        '体验_舒适度', '体验_易用性', '体验_外观设计', '体验_价格感知',
        # 市场维度 (2)
        '竞品_竞品对比', '复购_复购意愿',
        # 情感维度 (1)
        '情感_总体评价'
    ]

    all_columns = base_columns + tag_columns

    # 写入CSV
    with open(csv_file, 'w', encoding='utf-8', newline='') as f:
        writer = csv.DictWriter(f, fieldnames=all_columns)
        writer.writeheader()

        for review in reviews:
            row = {
                'review_id': review.get('review_id', ''),
                'body': review.get('body', ''),
                'rating': review.get('rating', ''),
                'date': review.get('date', ''),
                'sentiment': review.get('sentiment', ''),
                'info_score': review.get('info_score', '')
            }

            # 展开tags字典
            tags = review.get('tags', {})
            for col in tag_columns:
                row[col] = tags.get(col, '未提及')

            writer.writerow(row)

    print(f"✓ 转换完成：{len(reviews)} 条评论")
    print(f"  输入：{json_file}")
    print(f"  输出：{csv_file}")


def csv_to_json(csv_file, json_file):
    """
    将扁平CSV格式转换回嵌套JSON结构

    CSV格式：review_id, body, rating, ..., 人群_性别, 场景_使用场景, ...

    输出JSON：嵌套tags结构
    """
    # 22维度标签列
    tag_columns = [
        '人群_性别', '人群_年龄段', '人群_职业', '人群_购买角色',
        '场景_使用场景',
        '功能_满意度', '功能_具体功能',
        '质量_材质', '质量_做工', '质量_耐用性',
        '服务_发货速度', '服务_包装质量', '服务_客服响应',
        '服务_退换货', '服务_保修',
        '体验_舒适度', '体验_易用性', '体验_外观设计', '体验_价格感知',
        '竞品_竞品对比', '复购_复购意愿',
        '情感_总体评价'
    ]

    reviews = []

    with open(csv_file, 'r', encoding='utf-8') as f:
        reader = csv.DictReader(f)
        for row in reader:
            # 提取基础字段
            review = {
                'review_id': row.get('review_id', ''),
                'body': row.get('body', ''),
                'rating': float(row.get('rating', 0)) if row.get('rating') else 0,
                'date': row.get('date', ''),
                'sentiment': row.get('sentiment', ''),
                'info_score': int(row.get('info_score', 0)) if row.get('info_score') else 0
            }

            # 提取tags字段
            tags = {}
            for col in tag_columns:
                val = row.get(col, '')
                if val and val != '未提及':
                    tags[col] = val

            review['tags'] = tags
            reviews.append(review)

    with open(json_file, 'w', encoding='utf-8') as f:
        json.dump(reviews, f, ensure_ascii=False, indent=2)

    print(f"✓ 转换完成：{len(reviews)} 条评论")
    print(f"  输入：{csv_file}")
    print(f"  输出：{json_file}")


if __name__ == '__main__':
    print("=" * 50)
    print("评论数据转换工具")
    print("=" * 50)

    if len(sys.argv) < 3:
        print("\n使用方法：")
        print("  JSON -> CSV: python3 transform_logic.py json <input.json> <output.csv>")
        print("  CSV -> JSON: python3 transform_logic.py csv <input.csv> <output.json>")
        print("\n示例：")
        print("  python3 transform_logic.py json tagged.json output.csv")
        print("  python3 transform_logic.py csv reviews.csv tags.json")
        sys.exit(1)

    mode = sys.argv[1].lower()
    input_file = sys.argv[2]
    output_file = sys.argv[3]

    if mode == 'json':
        json_to_csv(input_file, output_file)
    elif mode == 'csv':
        csv_to_json(input_file, output_file)
    else:
        print(f"错误：未知模式 '{mode}'，请使用 'json' 或 'csv'")


path: templates/report.html
content: <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{PRODUCT_NAME}} - 评论分析报告</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
    <style>
        :root {
            /* 基础色彩系统 - 专业数据分析风格 */
            --bg-base: #f8fafc;
            --bg-surface: #ffffff;
            --bg-muted: #f1f5f9;

            /* 文字色彩 */
            --text-primary: #0f172a;
            --text-secondary: #475569;
            --text-tertiary: #94a3b8;
            --text-inverse: #ffffff;

            /* 语义色彩 - 数据可视化专用 */
            --data-success: #10b981;
            --data-success-light: #d1fae5;
            --data-warning: #f59e0b;
            --data-warning-light: #fef3c7;
            --data-danger: #ef4444;
            --data-danger-light: #fee2e2;
            --data-info: #3b82f6;
            --data-info-light: #dbeafe;

            /* 品牌色彩 - 专业蓝 */
            --brand-primary: #1e40af;
            --brand-secondary: #3b82f6;
            --brand-accent: #6366f1;

            /* 边框和分割线 */
            --border-subtle: #e2e8f0;
            --border-medium: #cbd5e1;

            /* 图表调色板 - 色盲友好 */
            --chart-1: #3b82f6;   /* 蓝色 */
            --chart-2: #10b981;   /* 绿色 */
            --chart-3: #f59e0b;   /* 橙色 */
            --chart-4: #ef4444;   /* 红色 */
            --chart-5: #8b5cf6;   /* 紫色 */
            --chart-6: #06b6d4;   /* 青色 */
            --chart-7: #ec4899;   /* 粉色 */
            --chart-8: #84cc16;   /* 黄绿色 */

            /* 阴影 */
            --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
            --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
            --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
        }

        * { margin: 0; padding: 0; box-sizing: border-box; }

        body {
            font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            background: var(--bg-base);
            color: var(--text-primary);
            line-height: 1.6;
            padding: 2rem;
            font-size: 14px;
        }

        .container {
            max-width: 1400px;
            margin: 0 auto;
        }

        /* ========== 头部区域 ========== */
        .header {
            background: var(--bg-surface);
            border-radius: 12px;
            padding: 2rem;
            margin-bottom: 2rem;
            box-shadow: var(--shadow-sm);
            border: 1px solid var(--border-subtle);
        }

        .header-top {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            margin-bottom: 1rem;
        }

        .header-title h1 {
            font-size: 1.75rem;
            font-weight: 700;
            color: var(--text-primary);
            margin-bottom: 0.5rem;
            letter-spacing: -0.025em;
        }

        .header-meta {
            display: flex;
            gap: 1.5rem;
            font-size: 0.875rem;
            color: var(--text-secondary);
        }

        .header-meta-item {
            display: flex;
            align-items: center;
            gap: 0.375rem;
        }

        .header-meta-item::before {
            content: '';
            width: 6px;
            height: 6px;
            background: var(--brand-secondary);
            border-radius: 50%;
        }

        .header-subtitle {
            color: var(--text-secondary);
            font-size: 0.875rem;
        }

        /* ========== 数据卡片网格 ========== */
        .metrics-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
            gap: 1rem;
            margin-bottom: 2rem;
        }

        .metric-card {
            background: var(--bg-surface);
            border-radius: 12px;
            padding: 1.5rem;
            box-shadow: var(--shadow-sm);
            border: 1px solid var(--border-subtle);
            transition: all 0.2s ease;
        }

        .metric-card:hover {
            box-shadow: var(--shadow-md);
            transform: translateY(-2px);
        }

        .metric-card.primary {
            border-left: 4px solid var(--brand-primary);
        }

        .metric-card.success {
            border-left: 4px solid var(--data-success);
        }

        .metric-card.warning {
            border-left: 4px solid var(--data-warning);
        }

        .metric-card.danger {
            border-left: 4px solid var(--data-danger);
        }

        .metric-label {
            font-size: 0.75rem;
            font-weight: 600;
            text-transform: uppercase;
            letter-spacing: 0.075em;
            color: var(--text-tertiary);
            margin-bottom: 0.5rem;
        }

        .metric-value {
            font-size: 2rem;
            font-weight: 700;
            color: var(--text-primary);
            letter-spacing: -0.05em;
        }

        .metric-unit {
            font-size: 0.875rem;
            font-weight: 500;
            color: var(--text-tertiary);
            margin-left: 0.25rem;
        }

        .metric-delta {
            font-size: 0.75rem;
            margin-top: 0.5rem;
            display: flex;
            align-items: center;
            gap: 0.25rem;
        }

        .metric-delta.positive { color: var(--data-success); }
        .metric-delta.negative { color: var(--data-danger); }
        .metric-delta.neutral { color: var(--text-tertiary); }

        /* ========== 章节 ========== */
        .section {
            margin-bottom: 2rem;
        }

        .section-header {
            display: flex;
            align-items: center;
            gap: 1rem;
            margin-bottom: 1.25rem;
            padding-bottom: 0.75rem;
            border-bottom: 2px solid var(--border-subtle);
        }

        .section-number {
            font-size: 2rem;
            font-weight: 700;
            color: var(--brand-primary);
            opacity: 0.15;
            letter-spacing: -0.05em;
        }

        .section-title {
            font-size: 1.25rem;
            font-weight: 600;
            color: var(--text-primary);
            letter-spacing: -0.025em;
        }

        /* ========== 卡片网格 ========== */
        .card-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
            gap: 1rem;
        }

        .card {
            background: var(--bg-surface);
            border-radius: 12px;
            padding: 1.5rem;
            box-shadow: var(--shadow-sm);
            border: 1px solid var(--border-subtle);
        }

        .card-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 1rem;
        }

        .card-title {
            font-size: 1rem;
            font-weight: 600;
            color: var(--text-primary);
        }

        .card-badge {
            font-size: 0.75rem;
            font-weight: 600;
            padding: 0.25rem 0.625rem;
            border-radius: 9999px;
            background: var(--bg-muted);
            color: var(--text-secondary);
        }

        /* ========== 用户画像卡片 ========== */
        .persona-card {
            background: var(--bg-surface);
            border-radius: 12px;
            padding: 1.5rem;
            box-shadow: var(--shadow-sm);
            border: 1px solid var(--border-subtle);
            position: relative;
            overflow: hidden;
        }

        .persona-card::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 4px;
            background: linear-gradient(90deg, var(--brand-primary), var(--brand-accent));
        }

        .persona-name {
            font-size: 1.125rem;
            font-weight: 600;
            color: var(--text-primary);
            margin-bottom: 0.25rem;
        }

        .persona-count {
            font-size: 0.875rem;
            color: var(--brand-secondary);
            font-weight: 500;
            margin-bottom: 1rem;
        }

        .persona-tags {
            display: flex;
            flex-wrap: wrap;
            gap: 0.5rem;
        }

        .persona-tag {
            font-size: 0.75rem;
            padding: 0.375rem 0.75rem;
            background: var(--bg-muted);
            border-radius: 6px;
            color: var(--text-secondary);
            font-weight: 500;
        }

        /* ========== 图表容器 ========== */
        .chart-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
            gap: 1.5rem;
        }

        .chart-container {
            background: var(--bg-surface);
            border-radius: 12px;
            padding: 1.5rem;
            box-shadow: var(--shadow-sm);
            border: 1px solid var(--border-subtle);
        }

        .chart-title {
            font-size: 0.875rem;
            font-weight: 600;
            color: var(--text-secondary);
            margin-bottom: 1rem;
            text-transform: uppercase;
            letter-spacing: 0.05em;
        }

        /* ========== 统计条 ========== */
        .stat-bar {
            display: flex;
            align-items: center;
            gap: 1rem;
            margin-bottom: 0.875rem;
        }

        .stat-label {
            min-width: 100px;
            font-size: 0.875rem;
            color: var(--text-secondary);
            font-weight: 500;
        }

        .stat-track {
            flex: 1;
            height: 28px;
            background: var(--bg-muted);
            border-radius: 6px;
            overflow: hidden;
        }

        .stat-fill {
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: flex-end;
            padding-right: 0.75rem;
            font-size: 0.75rem;
            font-weight: 600;
            color: var(--text-inverse);
            border-radius: 6px;
            transition: width 0.6s ease;
        }

        .stat-fill.blue { background: linear-gradient(90deg, var(--brand-primary), var(--brand-secondary)); }
        .stat-fill.green { background: linear-gradient(90deg, var(--data-success), #34d399); }
        .stat-fill.orange { background: linear-gradient(90deg, var(--data-warning), #fbbf24); }
        .stat-fill.red { background: linear-gradient(90deg, var(--data-danger), #f87171); }

        .stat-value {
            min-width: 60px;
            text-align: right;
            font-size: 0.875rem;
            font-weight: 600;
            color: var(--text-primary);
        }

        /* ========== VOC卡片 ========== */
        .voc-card {
            background: var(--bg-surface);
            border-radius: 12px;
            padding: 1.5rem;
            box-shadow: var(--shadow-sm);
            border: 1px solid var(--border-subtle);
            margin-bottom: 1rem;
        }

        .voc-card.positive { border-left: 4px solid var(--data-success); }
        .voc-card.negative { border-left: 4px solid var(--data-danger); }
        .voc-card.neutral { border-left: 4px solid var(--text-tertiary); }

        .voc-quote {
            font-style: italic;
            color: var(--text-primary);
            font-size: 0.9375rem;
            margin-bottom: 1rem;
            padding-left: 1rem;
            border-left: 3px solid var(--border-medium);
            line-height: 1.7;
        }

        .voc-analysis {
            font-size: 0.875rem;
            color: var(--text-secondary);
        }

        .voc-analysis strong {
            color: var(--text-primary);
            font-weight: 600;
        }

        /* ========== 战略卡片 ========== */
        .strategy-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
            gap: 1rem;
        }

        .strategy-card {
            background: var(--bg-surface);
            border-radius: 12px;
            padding: 1.5rem;
            box-shadow: var(--shadow-sm);
            border: 1px solid var(--border-subtle);
        }

        .strategy-card.urgent { border-top: 4px solid var(--data-danger); }
        .strategy-card.short-term { border-top: 4px solid var(--data-warning); }
        .strategy-card.long-term { border-top: 4px solid var(--data-info); }

        .strategy-header {
            display: flex;
            justify-content: space-between;
            align-items: start;
            margin-bottom: 0.75rem;
        }

        .strategy-title {
            font-weight: 600;
            color: var(--text-primary);
            font-size: 0.9375rem;
        }

        .strategy-urgency {
            font-size: 0.6875rem;
            font-weight: 700;
            text-transform: uppercase;
            letter-spacing: 0.075em;
            padding: 0.25rem 0.5rem;
            border-radius: 4px;
        }

        .strategy-urgency.immediate { background: var(--data-danger-light); color: #991b1b; }
        .strategy-urgency.short-term { background: var(--data-warning-light); color: #92400e; }
        .strategy-urgency.long-term { background: var(--data-info-light); color: #1e40af; }

        .strategy-desc {
            font-size: 0.875rem;
            color: var(--text-secondary);
            line-height: 1.6;
        }

        .strategy-roi {
            margin-top: 1rem;
            padding-top: 0.75rem;
            border-top: 1px dashed var(--border-subtle);
            font-size: 0.75rem;
            color: var(--data-success);
            font-weight: 500;
        }

        /* ========== 洞察内容 ========== */
        .insights-content {
            background: var(--bg-surface);
            border-radius: 12px;
            padding: 2rem;
            box-shadow: var(--shadow-sm);
            border: 1px solid var(--border-subtle);
        }

        .insights-content h1,
        .insights-content h2,
        .insights-content h3,
        .insights-content h4 {
            color: var(--text-primary);
            margin-top: 1.5rem;
            margin-bottom: 0.75rem;
            font-weight: 600;
        }

        .insights-content h1 { font-size: 1.5rem; }
        .insights-content h2 { font-size: 1.25rem; }
        .insights-content h3 { font-size: 1.125rem; }

        .insights-content p {
            margin-bottom: 1rem;
            color: var(--text-secondary);
            line-height: 1.7;
        }

        .insights-content ul,
        .insights-content ol {
            margin-left: 1.5rem;
            margin-bottom: 1rem;
            color: var(--text-secondary);
        }

        .insights-content li {
            margin-bottom: 0.5rem;
        }

        .insights-content strong {
            color: var(--text-primary);
            font-weight: 600;
        }

        .insights-content code {
            background: var(--bg-muted);
            padding: 0.125rem 0.375rem;
            border-radius: 4px;
            font-size: 0.875em;
            color: var(--brand-primary);
        }

        /* ========== 页脚 ========== */
        .footer {
            margin-top: 3rem;
            padding: 2rem;
            background: var(--bg-surface);
            border-radius: 12px;
            text-align: center;
            box-shadow: var(--shadow-sm);
            border: 1px solid var(--border-subtle);
        }

        .footer-logo {
            font-size: 1.125rem;
            font-weight: 700;
            color: var(--brand-primary);
            margin-bottom: 0.5rem;
        }

        .footer p {
            font-size: 0.875rem;
            color: var(--text-tertiary);
        }

        /* ========== 响应式 ========== */
        @media (max-width: 768px) {
            body { padding: 1rem; }
            .header { padding: 1.5rem; }
            .metrics-grid { grid-template-columns: 1fr; }
            .chart-grid { grid-template-columns: 1fr; }
            .card-grid { grid-template-columns: 1fr; }
            .header-top { flex-direction: column; gap: 1rem; }
            .header-meta { flex-direction: column; gap: 0.5rem; }
        }

        /* ========== 打印样式 ========== */
        @media print {
            body {
                background: white;
                padding: 0;
            }
            .card,
            .chart-container,
            .persona-card,
            .voc-card,
            .strategy-card,
            .insights-content,
            .metric-card,
            .header,
            .footer {
                box-shadow: none;
                border: 1px solid #e2e8f0;
                page-break-inside: avoid;
            }
            .section {
                page-break-inside: avoid;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <!-- 头部 -->
        <header class="header">
            <div class="header-top">
                <div class="header-title">
                    <h1>{{PRODUCT_NAME}}</h1>
                </div>
                <div class="header-meta">
                    <div class="header-meta-item">ASIN: {{ASIN}}</div>
                    <div class="header-meta-item">{{ANALYSIS_DATE}}</div>
                    <div class="header-meta-item">AI分析</div>
                </div>
            </div>
            <p class="header-subtitle">
                基于 {{TOTAL_REVIEWS}} 条评论的深度分析 · 22维度标签体系 · 用户画像洞察
            </p>
        </header>

        <!-- 关键指标 -->
        <section class="metrics-grid">
            <div class="metric-card primary">
                <div class="metric-label">Total Reviews</div>
                <div class="metric-value">{{TOTAL_REVIEWS}}<span class="metric-unit">条</span></div>
            </div>
            <div class="metric-card success">
                <div class="metric-label">Positive Rate</div>
                <div class="metric-value">{{POSITIVE_RATE}}<span class="metric-unit">%</span></div>
                <div class="metric-delta positive">↑ 高于平均</div>
            </div>
            <div class="metric-card primary">
                <div class="metric-label">Avg Rating</div>
                <div class="metric-value">{{AVG_RATING}}<span class="metric-unit">/5</span></div>
            </div>
            <div class="metric-card warning">
                <div class="metric-label">User Personas</div>
                <div class="metric-value">{{PERSONA_COUNT}}<span class="metric-unit">个</span></div>
            </div>
        </section>

        <!-- 核心用户画像 -->
        <section class="section">
            <div class="section-header">
                <span class="section-number">01</span>
                <h2 class="section-title">核心用户画像</h2>
            </div>
            <div class="card-grid">
                {{PERSONAS_HTML}}
            </div>
        </section>

        <!-- 情感分布 -->
        <section class="section">
            <div class="section-header">
                <span class="section-number">02</span>
                <h2 class="section-title">情感分布分析</h2>
            </div>
            <div class="chart-grid">
                <div class="chart-container">
                    <div class="chart-title">评价等级分布</div>
                    <canvas id="ratingChart"></canvas>
                </div>
                <div class="chart-container">
                    <div class="chart-title">用户满意度情感分析</div>
                    <canvas id="sentimentChart"></canvas>
                </div>
            </div>
        </section>

        <!-- 标签维度统计 -->
        <section class="section">
            <div class="section-header">
                <span class="section-number">03</span>
                <h2 class="section-title">标签维度统计</h2>
            </div>
            <div class="chart-grid">
                <div class="chart-container">
                    <div class="chart-title">使用场景分布</div>
                    <canvas id="scenarioChart"></canvas>
                </div>
                <div class="chart-container">
                    <div class="chart-title">功能满意度分布</div>
                    <canvas id="satisfactionChart"></canvas>
                </div>
            </div>
        </section>

        <!-- 价格感知与复购 -->
        <section class="section">
            <div class="section-header">
                <span class="section-number">04</span>
                <h2 class="section-title">价格感知与复购意愿</h2>
            </div>
            <div class="chart-grid">
                <div class="chart-container">
                    <div class="chart-title">价格感知分布</div>
                    <div id="pricePerception"></div>
                </div>
                <div class="chart-container">
                    <div class="chart-title">复购意愿分布</div>
                    <canvas id="repurchaseChart"></canvas>
                </div>
            </div>
        </section>

        <!-- VOC用户之声 -->
        <section class="section">
            <div class="section-header">
                <span class="section-number">05</span>
                <h2 class="section-title">用户之声 (VOC)</h2>
            </div>
            <div id="vocSection">
                {{VOC_HTML}}
            </div>
        </section>

        <!-- 战略执行矩阵 -->
        <section class="section">
            <div class="section-header">
                <span class="section-number">06</span>
                <h2 class="section-title">战略执行矩阵</h2>
            </div>
            <div class="strategy-grid">
                {{STRATEGY_HTML}}
            </div>
        </section>

        <!-- 深度洞察 -->
        <section class="section">
            <div class="section-header">
                <span class="section-number">07</span>
                <h2 class="section-title">深度洞察分析</h2>
            </div>
            <div class="insights-content">
                {{INSIGHTS_HTML}}
            </div>
        </section>

        <!-- 页脚 -->
        <footer class="footer">
            <div class="footer-logo">Review Analytics Pro</div>
            <p>Generated by AI · {{ANALYSIS_DATE}}</p>
            <p style="margin-top: 0.5rem; font-size: 0.75rem; color: var(--text-tertiary);">
                Powered by Claude AI · 22-Dimension Tag System · User Persona Analysis
            </p>
        </footer>
    </div>

    <script>
        // Chart.js 配置
        Chart.defaults.color = '#475569';
        Chart.defaults.borderColor = '#e2e8f0';
        Chart.defaults.font.family = 'Inter, -apple-system, sans-serif';

        // 专业调色板（色盲友好）
        const chartColors = {
            blue: '#3b82f6',
            green: '#10b981',
            orange: '#f59e0b',
            red: '#ef4444',
            purple: '#8b5cf6',
            cyan: '#06b6d4',
            pink: '#ec4899',
            lime: '#84cc16',
            gray: '#94a3b8'
        };

        // 图表数据配置
        const chartData = {{CHART_DATA}};

        // 1. 评价等级分布
        new Chart(document.getElementById('ratingChart'), {
            type: 'bar',
            data: {
                labels: ['5星', '4星', '3星', '2星', '1星'],
                datasets: [{
                    label: '评价数量',
                    data: chartData.ratingDistribution || [0, 0, 0, 0, 0],
                    backgroundColor: [chartColors.green, chartColors.blue, chartColors.gray, chartColors.orange, chartColors.red],
                    borderRadius: 6,
                    borderSkipped: false
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: { display: false }
                },
                scales: {
                    y: {
                        beginAtZero: true,
                        ticks: { stepSize: 1 }
                    }
                }
            }
        });

        // 2. 用户满意度情感分析
        new Chart(document.getElementById('sentimentChart'), {
            type: 'doughnut',
            data: {
                labels: ['强烈推荐', '推荐', '中立', '不推荐', '强烈不推荐'],
                datasets: [{
                    data: chartData.sentimentDistribution || [0, 0, 0, 0, 0],
                    backgroundColor: [chartColors.green, chartColors.blue, chartColors.gray, chartColors.orange, chartColors.red],
                    borderWidth: 2,
                    borderColor: '#ffffff'
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        position: 'right',
                        labels: { padding: 15, usePointStyle: true }
                    }
                }
            }
        });

        // 3. 使用场景分布
        new Chart(document.getElementById('scenarioChart'), {
            type: 'pie',
            data: {
                labels: chartData.scenarioLabels || [],
                datasets: [{
                    data: chartData.scenarioData || [],
                    backgroundColor: [chartColors.blue, chartColors.orange, chartColors.purple, chartColors.gray],
                    borderWidth: 2,
                    borderColor: '#ffffff'
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        position: 'right',
                        labels: { padding: 15, usePointStyle: true }
                    }
                }
            }
        });

        // 4. 功能满意度分布
        new Chart(document.getElementById('satisfactionChart'), {
            type: 'polarArea',
            data: {
                labels: ['超出预期', '符合预期', '低于预期'],
                datasets: [{
                    data: chartData.satisfactionData || [0, 0, 0],
                    backgroundColor: [
                        'rgba(16, 185, 129, 0.8)',
                        'rgba(59, 130, 246, 0.8)',
                        'rgba(239, 68, 68, 0.8)'
                    ],
                    borderWidth: 0
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        position: 'right',
                        labels: { padding: 15, usePointStyle: true }
                    }
                },
                scales: {
                    r: {
                        ticks: { display: false }
                    }
                }
            }
        });

        // 5. 复购意愿分布
        new Chart(document.getElementById('repurchaseChart'), {
            type: 'bar',
            data: {
                labels: ['会复购', '可能', '不会', '未提及'],
                datasets: [{
                    label: '用户数量',
                    data: chartData.repurchaseData || [0, 0, 0, 0],
                    backgroundColor: [chartColors.green, chartColors.blue, chartColors.red, chartColors.gray],
                    borderRadius: 6
                }]
            },
            options: {
                responsive: true,
                indexAxis: 'y',
                plugins: {
                    legend: { display: false }
                },
                scales: {
                    x: {
                        beginAtZero: true,
                        ticks: { stepSize: 1 }
                    }
                }
            }
        });
    </script>
</body>
</html>

path: prompts/insights.txt
content: # Role
你是一位拥有15年经验的"消费者行为学家"和"首席跨境电商数据分析师"。你擅长通过处理结构化的标签数据和非结构化的文本内容，挖掘市场真相。你的行文风格专业、客观、逻辑严密。

# Input Data

## 数据范围
- 评论总量：{total} 条
- 有效打标：{tagged} 条
- ASIN：{asin}

## 用户画像分析（{personas_count} 个）
{personas_details}

## 统计摘要

### 情感分布
{sentiment_distribution}

### 高频标签 Top 15
{top_tags}

## 黄金样本（按用户画像筛选，共 {samples_count} 条）
{golden_samples}

# Analysis Framework
请严格按照以下 7 大章节进行深度洞察分析，生成完整的 Markdown 报告：

## 0. 数据统计
- 核心要求：对所有打标数据进行全盘的定量汇总与统计
- 格式要求：[标签维度]：[类别A]（X人，X%），[类别B]（Y人，Y%），[类别C]（Z人，Z%）...
- 语义归并原则：发挥AI的语义识别能力，将表达有差异但实际属于同个维度的标签进行合并统计
- 长尾折叠原则：对于"职业"、"使用场景"、"具体功能"等分类可能极其繁杂的维度，仅详细列出占比最高的 Top 10 类别。Top 10 之外的所有长尾数据，请统一合并记录为"其他/未知"
- 统计范围：必须涵盖性别、年龄段、职业、场景、功能满意度、质量维度、体验维度以及总体评价等核心标签

## 1. 用户画像与主流场景
- 核心用户群体有哪些？（基于性别、年龄、职业的交叉分析）
- 典型使用场景有哪些？
- **必须引用评论原话支撑**

## 2. 核心卖点与价值验证
- 哪些功能/价值被普遍认可？
- 在"功能满意度"为"超出预期"或"总体评价"为"强烈推荐"的数据中，出现频率最高的标签是什么？
- **必须引用评论原话支撑**

## 3. 核心痛点与负面归因
- 用户最不满意的是什么？
- 根源追溯：不要只说"耐用性差"，要指出具体是哪个部件/功能出了问题
- 严重性评估：区分导致退货的致命伤 vs 可忍受的小瑕疵
- **必须引用评论原话支撑**

## 4. 改进建议与优先级
- 基于痛点分析，提出 3 条具体的改进建议
- 按照问题出现的频率和对购买决策的影响程度进行优先级排序（高/中/低）

## 5. 潜在机会与差异化
- 竞品情报：用户提到了哪些竞品？他们认为我们好在哪里、差在哪里？
- 蓝海发现：是否存在小众但满意度极高的人群？
- **必须引用评论原话支撑**

## 6. 典型用户深度解析
- 从高价值评论样本中提取 1-2 个占比最高或最具代表性的"核心用户画像"
- 针对每个画像，抽取 1-2 条评论进行具体解析，总共输出不少于 4 个评论内容的解析
- 必须包含正面和负面评价，保持分布均匀
- 严格按照以下格式输出本章节内容：

**典型画像**：[年龄段] [性别] [职业] [核心需求]

**评价原文**："[保留评论核心内容，过长可适当截断]"

**评论解析**：[1-2句话简评。指出该用户的核心痛点、爽点或具体建议]

## 7. 关键洞察总结
- 约精炼的文字（约150字左右）对整份报告进行总结
- 核心聚焦：基于你的专业判断，指出报告中最值得关注的"亮点"或"风险点"

# Output Format
请输出完整的 Markdown 报告，结构如下：

# {{ product_name }} 评论深度洞察报告

> ASIN: {{ asin }} | 分析时间: {{ date }} | 样本量: {{ total }} 条

## 洞察总览
[不少于 300 字的全局宏观总结，指出品牌当前所处的阶段与最核心的机会/危机]

## 数据统计
[结构化罗列人群、场景、满意度等核心分布数据]
- 对性别、年龄段、职业、场景、功能满意度、质量维度、体验维度以及总体评价等核心标签进行统计
- 应用语义归并原则，合并同类型标签
- 对职业、场景、具体功能等维度只显示 Top 10，其余合并为"其他/未知"

## 一、核心用户画像与场景
1. 定量统计与交叉洞察（基于性别、年龄、职业等维度的深度碰撞）
2. 典型画像描述（刻画 2 个极其具象的真实使用场景。注意：严禁使用整段大篇幅文字，必须使用加粗标题+简短列表的结构化形式排版，确保移动端及大屏阅读体验）

## 二、核心卖点与价值验证
1. 满意度归因（用户为什么买？为什么给好评？）
2. 语义挖掘（用户在赞美时的关键词及其背后的心理溢价）

## 三、主要痛点与负面归因
1. 问题分布（按严重程度排序）
2. 根源追溯（是产品设计缺陷、质量控制问题、还是用户预期管理不当？）
3. 严重性评估（哪些问题会导致致命的退货和品牌崩塌？）

## 四、改进建议与优先级
1. 硬件/功能迭代建议（优先级：高/中/低）
2. 软件/服务/预期引导建议
3. 详细执行动作与预期 ROI 分析

## 五、潜在机会与差异化
1. 竞品情报分析（基于评论中出现的竞品对比）
2. 蓝海细分场景发现

## 六、典型用户深度解析 (VOC)
请选取 4 个最具代表性的用户，进行"原文 + 深度背景解析 + 核心需求探测"的连线分析。

对于每个用户，请根据评论风格从以下六类中选出一个最契合的头像类型：
- `male_young` (青年/中年男性)
- `female_young` (青年/中年女性)
- `male_elderly` (老年男性)
- `female_elderly` (老年女性)
- `child_boy` (少男/男童)
- `child_girl` (少女/女童)

**典型画像**：[如：26-35岁 男性 IT程序员]
**头像类型**：[从指定的六个枚举值中选择其一]
**核心需求**：[一句话概括，如：缓解长期久坐的腰部酸痛]
**评价原文**："[评论内容 - 保留核心原话，不得仅显示ID，控制在100字以内]"
**评论解析**：[深入分析该用户的爽点、痛点及隐性商业需求]

[重复上述格式，至少4个评论]

## 关键洞察总结
[约150字，聚焦最值得关注的亮点或风险点]

# Strategic Data Output (System Only)
为了确保可视化看板的准确性，请在 Markdown 报告的最后，强制输出一个 `<strategic_json>` 块。
该块必须是合法的 JSON，严禁包含任何 Markdown 格式。
**数据密度要求:**
- `moat` 和 `vulnerability` 必须分别包含至少 3 项。
- `desc` 字段必须包含具体的逻辑推导或证据，不少于 50 字。
- `execution_matrix` 必须针对三个时间维度给出具体的、可落地的业务指令。

格式如下：
<strategic_json>
{{
  "moat": [
    {{"title": "护城河1标题", "desc": "非常详尽的背景、数据支撑及优势描述..."}},
    {{"title": "护城河2标题", "desc": "..."}},
    {{"title": "护城河3标题", "desc": "..."}}
  ],
  "vulnerability": [
    {{"title": "软肋1标题", "desc": "深度解析该软肋对品牌的杀伤力..."}},
    {{"title": "软肋2标题", "desc": "..."}},
    {{"title": "软肋3标题", "desc": "..."}}
  ],
  "execution_matrix": [
    {{"urgency": "Immediate", "directive": "指令1", "details": "极其详尽的动作拆解...", "roi": "量化的预期回报..."}},
    {{"urgency": "Short-Term", "directive": "指令2", "details": "...", "roi": "..."}},
    {{"urgency": "Long-Term", "directive": "指令3", "details": "...", "roi": "..."}}
  ]
}}
</strategic_json>


path: prompts/tagging.txt
content: 你是一个专业电商评论分析AI。请分析以下评论，提取22个维度的标签。

# 输入数据
【评论ID】：{review_id}
【评论标题】：{title}
【评论内容】：{body}
【星级】：{rating}星

# 任务要求
严格按以下JSON格式返回（不要markdown代码块，不要其他文字）：

{{
  "review_id": "{review_id}",
  "sentiment": "强烈推荐/推荐/中立/不推荐/强烈不推荐",
  "info_score": <信息密度评分1-20>,
  "tags": {{
    "人群_性别": "男性/女性/不明",
    "人群_年龄段": "18-25/26-35/36-45/46-55/55+/不明",
    "人群_职业": "根据评论内容推断，如：医疗工作者/体力劳动者/办公室职员/学生/退休人员/教师/司机/其他/不明",
    "人群_购买角色": "自用/礼物/商用/不明",
    "场景_使用场景": "根据评论内容推断，如：家用/办公/户外/运动/开车/旅行/其他",
    "功能_满意度": "超出预期/符合预期/低于预期/未提及",
    "功能_具体功能": "简述用户提到的具体功能点，如：续航能力/防水功能/保暖效果/降噪功能等",
    "质量_材质": "优秀/良好/一般/差/未提及",
    "质量_做工": "精细/一般/粗糙/未提及",
    "质量_耐用性": "耐用/一般/易坏/未提及",
    "服务_发货速度": "快/正常/慢/未提及",
    "服务_包装质量": "完好/一般/破损/未提及",
    "服务_客服响应": "及时/一般/迟缓/未提及",
    "服务_退换货": "顺畅/一般/困难/未提及",
    "服务_保修": "有保修/无保修/未提及",
    "体验_舒适度": "舒适/一般/不适/未提及",
    "体验_易用性": "简单/适中/困难/未提及",
    "体验_外观设计": "满意/一般/不满意/未提及",
    "体验_价格感知": "超值/合理/偏贵/未提及",
    "竞品_竞品对比": "用户提及的竞品品牌名称，如：品牌A/品牌B/品牌C等，如无则填'无'",
    "复购_复购意愿": "会复购/可能/不会/未提及",
    "情感_总体评价": "强烈推荐/推荐/中立/不推荐/强烈不推荐"
  }}
}}

# 评分规则 (info_score)
- 基础分：评论字数 > 50字得1分，> 200字再加2分
- 标签分：每个有效标签（非"未提及"）得1分
- 加分项：
  - 提及竞品：+5分
  - 明确复购意愿：+3分
  - 描述使用场景：+2分

# 注意事项
1. 无依据的标签填"不明"或"未提及"
2. 职业标签、使用场景均需从评论内容中推断，如无明确线索则填"不明"
3. info_score 范围 1-20，反映评论的信息价值密度
4. 只返回纯JSON，不要任何解释文字


path: prompts/tagging_batch.txt
content: 你是一个专业电商评论分析AI。请分析以下批量评论，为每条提取22个维度的标签。

# 输入数据（共 {batch_size} 条评论）
{reviews_json}

# 任务要求
对每条评论进行分析，返回JSON数组格式，每条评论包含：
- review_id: 原始评论ID
- sentiment: 情感倾向 (强烈推荐/推荐/中立/不推荐/强烈不推荐)
- info_score: 信息密度评分 (1-20)
- tags: 完整22维度标签对象

# 标签体系

人群维度:
  - 人群_性别: 男性/女性/不明
  - 人群_年龄段: 18-25/26-35/36-45/46-55/55+/不明
  - 人群_职业: 根据评论推断职业（医疗工作者/体力劳动者/办公室职员/学生/退休人员/教师/司机/其他/不明）
  - 人群_购买角色: 自用/礼物/商用/不明

场景维度:
  - 场景_使用场景: 根据评论推断场景，如家用/办公/户外/运动/开车/旅行/其他

功能维度:
  - 功能_满意度: 超出预期/符合预期/低于预期/未提及
  - 功能_具体功能: 具体功能点描述

质量维度:
  - 质量_材质: 优秀/良好/一般/差/未提及
  - 质量_做工: 精细/一般/粗糙/未提及
  - 质量_耐用性: 耐用/一般/易坏/未提及

服务维度:
  - 服务_发货速度: 快/正常/慢/未提及
  - 服务_包装质量: 完好/一般/破损/未提及
  - 服务_客服响应: 及时/一般/迟缓/未提及
  - 服务_退换货: 顺畅/一般/困难/未提及
  - 服务_保修: 有保修/无保修/未提及

体验维度:
  - 体验_舒适度: 舒适/一般/不适/未提及
  - 体验_易用性: 简单/适中/困难/未提及
  - 体验_外观设计: 满意/一般/不满意/未提及
  - 体验_价格感知: 超值/合理/偏贵/未提及

市场维度:
  - 竞品_竞品对比: 竞品品牌或'无'
  - 复购_复购意愿: 会复购/可能/不会/未提及

情感维度:
  - 情感_总体评价: 强烈推荐/推荐/中立/不推荐/强烈不推荐

# 输出格式
返回纯JSON数组，不要markdown代码块：
[
  {{"review_id": "...", "sentiment": "...", "info_score": 10, "tags": {{...}}}},
  {{"review_id": "...", "sentiment": "...", "info_score": 8, "tags": {{...}}}},
  ...
]

# 评分规则 (info_score)
- 基础分：评论字数 > 50字得1分，> 200字再加2分
- 标签分：每个有效标签（非"未提及"）得1分
- 加分项：提及竞品(+5分)、明确复购意愿(+3分)、描述使用场景(+2分)

# 注意事项
1. 无依据的标签填"不明"或"未提及"
2. 只返回纯JSON数组，不要任何解释文字


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