Skill: lgmsmile/citic-creditcard-auto-apply-skill-v3 (version 1.0.0)

--- name: citic-creditcard-auto-apply description: 基于中信银行信用卡官网进行卡种推荐，并使用 OpenClaw browser 工具辅助预填官方申请表；默认停在最终提交前，需客户本人确认。 version: 3.0.0 metadata: openclaw: requires: anyBins: - python3 - python emoji: "💳" homepage: "https://creditcard.ecitic.com/" --- # 中信银行信用卡自动申请（OpenClaw 原生改写版） ## 什么时候使用 当用户希望： - 根据年龄、收入、消费习惯、出行偏好、权益诉求推荐中信信用卡； - 查看官网当前仍可申请的卡种； - 读取自己在 OpenClaw 工作区中**已授权**的申请资料； - 用 OpenClaw 的 **browser** 工具打开官网申请页并预填资料； 就使用本 skill。 ## 这版 skill 的实现方式 这不是一个“YAML steps 自动执行器”。 - `SKILL.md`：告诉 agent 何时调用哪些工具。 - `config/workflow.template.yaml`：只是流程模板和字段配置，不会被 OpenClaw 直接执行。 - `scripts/profile_extractor.py`：从工作区资料中提取候选申请字段。 - `scripts/build_application_plan.py`：把候选字段、推荐结果和预填边界整理成一个可执行计划 JSON。 - `browser` 工具：真正执行打开网页、快照、点击、输入。 ## 硬规则 1. **只用中信银行信用卡官网和官网可达申请页作为产品与申请依据。** 2. **推荐与申请分两阶段进行。** 先推荐，再预填，不要一上来直接填表。 3. **默认只做预填，不做最终提交。** 4. **只有在用户明确授权时，才读取 `profiles/applicant_profile.json`、`USER.md`、`MEMORY.md`、`memory/*.md`。** 5. **从工作区读取到的内容都只是候选值。** 必须展示来源并让用户确认。 6. **不要猜测或伪造敏感信息。** 包括身份证号、手机号、收入、单位、地址等。 7. **验证码、短信 OTP、人脸识别、征信授权、协议勾选、最终提交，一律交给用户本人完成。** 8. **如果官网表单与本地数据不一致，以官网实时页面为准。** 9. **优先使用 OpenClaw 托管浏览器 profile `openclaw`。** 只有用户明确要求复用当前登录态时，才考虑 `user` profile。 ## 推荐阶段 ### 1）准备客户画像 优先收集： - 年龄 - 月收入 / 年收入 - 是否有稳定单位与职业信息 - 是否常做餐饮、网购、航旅、酒店、车主、境外消费 - 航司 / 酒店偏好 - 所需权益（积分、里程、酒店、航延险、车主权益、境外返现等） - 年费容忍度 ### 2）先看本地数据与模板 优先读取： - `data/card_catalog.json` - `profiles/applicant_profile.json`（若存在） - `profiles/applicant_profile.template.json` ### 3）推荐命令 ```bash python3 scripts/citic_cc_advisor.py recommend --profile profiles/applicant_profile.template.json ``` 如果要产出结构化结果： ```bash python3 scripts/citic_cc_advisor.py recommend --profile profiles/applicant_profile.template.json --format json ``` ### 4）推荐输出要求 必须包含： - 推荐 TOP 3 - 每张卡的适配原因 - 风险提示 / 限制条件 - 官网详情页 / 申请页 - 明确写出“最终批核、额度、权益解释以官网和审批结果为准” ## 申请阶段（browser 工具执行） ### A. 先生成候选资料 ```bash python3 scripts/profile_extractor.py --workspace . --json ``` 或生成完整执行计划： ```bash python3 scripts/build_application_plan.py --workspace . --workflow config/workflow.template.yaml --out output/application_plan.json ``` ### B. 先向用户确认候选字段 必须展示： - 字段名 - 脱敏预览 - 来源文件 - 置信度 只有用户确认后的字段才能用于填表。 ### C. browser 工具的推荐顺序 1. 打开官网或目标卡的申请页。 2. 先做 `snapshot`，拿到页面 ref。 3. 通过 ref 点击“立即申请/申请/办卡”等按钮。 4. 再 `snapshot` 新页面，识别表单字段。 5. 仅填写已经确认的字段。 6. 遇到验证码、OTP、协议勾选、最终提交时停止。 ### D. browser 填表原则 - 尽量用页面 ref，而不是脆弱的文本选择器。 - 每进入新页面或发生跳转后，都重新 snapshot。 - 动态表单或 iframe 先确认上下文，再填值。 - 对于官网未明确显示的字段，不要强行猜测映射。 - 若必填字段缺失，立即停止并提示用户补充。 ## 建议的对话/执行流程 1. 收集客户画像。 2. 生成推荐结果并让用户选定卡片。 3. 用户授权读取本地资料。 4. 生成候选字段和预填计划。 5. 使用 browser 工具进入官网申请页。 6. 逐页预填并停在最终提交前。 7. 提示用户本人核对并完成最终提交。 ## 文件说明 - `config/workflow.template.yaml`：把你原先的 YAML 改写成“流程模板”，供脚本或人工读取。 - `scripts/build_application_plan.py`：把 workflow、推荐结果、候选字段组装成 JSON 计划。 - `scripts/profile_extractor.py`：抽取待确认字段。 - `scripts/citic_cc_advisor.py`：推荐卡种，支持 markdown / json 两种格式。 ## 不要这样做 - 不要把 `workflow.template.yaml` 当成 OpenClaw 原生可执行步骤。 - 不要静默读取并外发用户全部 memory。 - 不要自动勾选授权、征信、协议条款。 - 不要自动最终提交信用卡申请。

path: README.md
content: # citic-creditcard-auto-apply

这是把原先“声明式 YAML steps”改写成 **OpenClaw 更适合落地的结构** 的版本：

- `SKILL.md`：告诉 agent 何时使用 browser 和本地脚本
- `config/workflow.template.yaml`：流程模板，只用于配置和说明
- `scripts/profile_extractor.py`：从工作区资料中提取候选字段
- `scripts/build_application_plan.py`：生成可执行计划 JSON
- `scripts/citic_cc_advisor.py`：输出卡种推荐（markdown / json）

## 为什么要这样改

OpenClaw 的 skill 负责“教 agent 怎么做”，不是原生 step-runner。
所以：

- 你的 YAML 仍然保留，但转成模板文件
- 真正执行网页操作的，是 OpenClaw 的 `browser` 工具
- 真正组织数据的，是本地脚本输出的 JSON

## 推荐用法

### 1. 准备客户资料

复制并填写：

```bash
cp profiles/applicant_profile.template.json profiles/applicant_profile.json
```

### 2. 生成推荐结果

```bash
python3 scripts/citic_cc_advisor.py recommend --profile profiles/applicant_profile.json
```

或：

```bash
python3 scripts/citic_cc_advisor.py recommend --profile profiles/applicant_profile.json --format json
```

### 3. 提取候选申请字段

```bash
python3 scripts/profile_extractor.py --workspace . --json --out output/candidates.json
```

### 4. 生成完整预填计划

```bash
python3 scripts/build_application_plan.py   --workspace .   --workflow config/workflow.template.yaml   --out output/application_plan.json
```

### 5. 让 OpenClaw 按 SKILL.md + browser 工具执行

推荐流程：

1. 先推荐卡种
2. 用户选定卡片
3. 用户授权读取资料
4. 生成并确认候选字段
5. 用 browser 打开官网申请页
6. 预填资料
7. 停在最终提交前

## 文件结构

- `SKILL.md`
- `config/workflow.template.yaml`
- `data/card_catalog.json`
- `scripts/citic_cc_advisor.py`
- `scripts/profile_extractor.py`
- `scripts/build_application_plan.py`
- `profiles/applicant_profile.template.json`
- `docs/browser_workflow.md`
- `docs/security_notes.md`

## 依赖

```bash
pip install -r requirements.txt
```

## 部署

将整个文件夹放到：

- `<workspace>/skills/citic-creditcard-auto-apply/`
- 或 `~/.openclaw/skills/citic-creditcard-auto-apply/`

然后重新开启 OpenClaw session。


path: requirements.txt
content: # No third-party dependencies required for the included helper scripts.
PyYAML>=6.0


path: scripts/build_application_plan.py
content: #!/usr/bin/env python3
"""Build a safe prefill execution plan for the CITIC credit-card auto-apply skill.

This script does not control the browser. It prepares a JSON plan for the agent:
- candidate fields extracted from workspace files
- recommended card and official apply URL
- confirmed/required/blocked actions
- stop conditions before final submission
"""
from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any, Dict, List

try:
    import yaml  # type: ignore
except Exception as exc:  # pragma: no cover
    raise SystemExit("缺少 PyYAML，请先安装 requirements.txt") from exc

from profile_extractor import build_candidates  # type: ignore
from citic_cc_advisor import recommend_json  # type: ignore

ROOT = Path(__file__).resolve().parents[1]


def load_profile(workspace: Path) -> Dict[str, Any]:
    profile_path = workspace / 'profiles' / 'applicant_profile.json'
    if not profile_path.exists():
        profile_path = workspace / 'profiles' / 'applicant_profile.template.json'
    if not profile_path.exists():
        return {}
    return json.loads(profile_path.read_text(encoding='utf-8'))


def load_workflow(path: Path) -> Dict[str, Any]:
    return yaml.safe_load(path.read_text(encoding='utf-8')) or {}


def main() -> None:
    parser = argparse.ArgumentParser(description='Build a safe browser prefill plan JSON')
    parser.add_argument('--workspace', default='.', help='Workspace root')
    parser.add_argument('--workflow', default='config/workflow.template.yaml', help='Workflow YAML path')
    parser.add_argument('--out', default='output/application_plan.json', help='Output JSON path')
    args = parser.parse_args()

    workspace = Path(args.workspace).resolve()
    workflow_path = workspace / args.workflow if not Path(args.workflow).is_absolute() else Path(args.workflow)
    out_path = workspace / args.out if not Path(args.out).is_absolute() else Path(args.out)

    workflow = load_workflow(workflow_path)
    profile = load_profile(workspace)
    recommendation = recommend_json(profile) if profile else {"top_cards": [], "recommended_card_slug": None}
    candidates = [c.__dict__ for c in build_candidates(workspace)]

    top_cards: List[Dict[str, Any]] = recommendation.get('top_cards', [])
    recommended = top_cards[0] if top_cards else {}

    plan = {
        'workflow_name': workflow.get('name'),
        'workspace': str(workspace),
        'browser_profile_default': (workflow.get('profiles') or {}).get('browser_profile_default', 'openclaw'),
        'browser_profile_existing_session': (workflow.get('profiles') or {}).get('browser_profile_existing_session', 'user'),
        'recommended_card': recommended,
        'backup_cards': top_cards[1:3],
        'candidate_fields': candidates,
        'confirmed_fields_required': [
            'full_name', 'phone', 'id_type', 'city', 'employer_name', 'monthly_income'
        ],
        'safe_prefill_fields': [
            'full_name', 'gender', 'birth_date', 'phone', 'email', 'city', 'residential_address',
            'mailing_address', 'employer_name', 'employer_city', 'job_title', 'education', 'monthly_income',
            'housing_status'
        ],
        'never_autofill_without_user_live_confirmation': [
            'id_number', 'sms_code', 'otp', 'face_verification', 'signature', 'agreement_consent', 'credit_authorization'
        ],
        'browser_sequence': [
            'open official page',
            'snapshot page refs',
            'open official apply page',
            'snapshot form refs',
            'type/select only confirmed fields',
            'stop before OTP / agreement / final submit'
        ],
        'stop_conditions': [
            'captcha shown',
            'otp or sms verification shown',
            'credit authorization or agreement shown',
            'final submit button shown',
            'required field missing or conflicting'
        ],
        'warning': 'This plan is for browser-assisted prefill only. The customer must review and complete the final submission personally.'
    }

    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_text(json.dumps(plan, ensure_ascii=False, indent=2), encoding='utf-8')
    print(json.dumps(plan, ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()


path: scripts/citic_cc_advisor.py
content: #!/usr/bin/env python3
"""CITIC credit card recommender and application assistant.

No third-party dependencies.
"""
from __future__ import annotations

import argparse
import json
from pathlib import Path
from typing import Any, Dict, List, Tuple

ROOT = Path(__file__).resolve().parents[1]
CATALOG_PATH = ROOT / "data" / "card_catalog.json"


def load_catalog() -> List[Dict[str, Any]]:
    return json.loads(CATALOG_PATH.read_text(encoding="utf-8"))


def load_profile(args: argparse.Namespace) -> Dict[str, Any]:
    if args.json:
        return json.loads(args.json)
    if args.profile:
        return json.loads(Path(args.profile).read_text(encoding="utf-8"))
    raise SystemExit("请通过 --json 或 --profile 提供客户画像。")


def normalize_texts(profile: Dict[str, Any]) -> List[str]:
    buckets: List[str] = []
    for key in ("interests", "spend_categories", "needs"):
        value = profile.get(key, []) or []
        if isinstance(value, str):
            buckets.append(value.lower())
        else:
            buckets.extend(str(v).lower() for v in value)
    for key in ("occupation", "city", "hotel_preference", "airline_preference"):
        if profile.get(key):
            buckets.append(str(profile[key]).lower())
    return buckets


def annual_fee_level(profile: Dict[str, Any]) -> str:
    value = str(profile.get("annual_fee_tolerance", "")).strip().lower()
    mapping = {
        "none": "none",
        "low": "low",
        "medium": "medium",
        "high": "high",
        "不接受": "none",
        "低": "low",
        "中": "medium",
        "高": "high",
    }
    return mapping.get(value, value or "low")


def explain_add(reasons: List[str], text: str) -> None:
    if text not in reasons:
        reasons.append(text)


def score_card(card: Dict[str, Any], profile: Dict[str, Any], terms: List[str]) -> Tuple[int, List[str], List[str]]:
    score = 0
    reasons: List[str] = []
    cautions: List[str] = []

    age = int(profile.get("age") or 0)
    monthly_income = float(profile.get("monthly_income") or 0)
    assets = float(profile.get("assets_with_citic") or 0)
    annual_fee_pref = annual_fee_level(profile)
    travel = str(profile.get("travel_frequency", "")).lower()
    intl = bool(profile.get("international_travel"))
    abroad = bool(profile.get("student_abroad"))
    car_owner = bool(profile.get("car_owner"))
    ev_owner = bool(profile.get("ev_owner"))
    hotel_pref = str(profile.get("hotel_preference", "")).lower()
    airline_pref = str(profile.get("airline_preference", "")).lower()

    joined = " ".join(terms)
    wants_points = any(k in joined for k in ["积分", "points", "咖啡", "网购", "线上", "返现"])
    wants_miles = any(k in joined for k in ["里程", "航司", "国航", "凤凰知音", "机票"])
    wants_hotel = any(k in joined for k in ["酒店", "住宿", "万豪", "bonvoy"])
    wants_delay = any(k in joined for k in ["航延", "延误", "机场", "贵宾厅", "安检"])
    wants_car = any(k in joined for k in ["车", "加油", "充电", "洗车", "停车"])
    wants_overseas = any(k in joined for k in ["出国", "境外", "留学", "外币", "海外", "国际"])
    likes_theme = any(k in joined for k in ["颜值", "主题", "好看", "设计"])

    slug = card["slug"]

    if age and not (18 <= age <= 60):
        cautions.append("中信官网主卡申请年龄一般为18至60周岁，当前年龄需人工复核。")
        score -= 50

    if slug == "yan-standard-platinum":
        score += 30
        if 22 <= age <= 35:
            score += 8
            explain_add(reasons, "年龄段更贴近年轻客群/入门白金定位。")
        if annual_fee_pref in {"none", "low"}:
            score += 8
            explain_add(reasons, "首年免年费、次年刷卡达标可免年费，适合低年费偏好。")
        if likes_theme or any(k in joined for k in ["餐饮", "日常", "通用"]):
            score += 6
            explain_add(reasons, "适合日常消费和颜值偏好。")
        if wants_hotel or wants_miles or wants_car:
            score -= 3
            cautions.append("这张卡更偏通用消费，不是酒店/车主/里程强联名卡。")

    elif slug == "i-platinum":
        score += 36
        if monthly_income >= 5000:
            score += 8
            explain_add(reasons, "月收入达到官网建议申请参考线。")
        if 25 <= age <= 35:
            score += 6
            explain_add(reasons, "年龄接近官网快捷申请参考年龄。")
        if wants_points:
            score += 10
            explain_add(reasons, "1.5倍积分和网络交易积分更适合线上消费/积分需求。")
        if wants_delay or travel in {"occasional", "frequent"}:
            score += 9
            explain_add(reasons, "有2小时航延险和500万航意险，适合兼顾出行保障。")
        if annual_fee_pref == "none":
            score -= 3
            cautions.append("虽有减免机制，但并非绝对零年费卡。")

    elif slug == "yueka-gold":
        score += 32
        if wants_points:
            score += 12
            explain_add(reasons, "核心卖点是多倍积分。")
        if assets >= 50000:
            score += 12
            explain_add(reasons, "在中信有5万元以上资产时，悦卡积分价值更明显。")
        elif assets > 0:
            score += 5
            explain_add(reasons, "已有中信资产沉淀，可更好发挥悦卡积分规则。")
        else:
            cautions.append("如无中信资产、且不关注积分，悦卡优势会下降。")
        if annual_fee_pref in {"none", "low"}:
            score += 2
        if wants_hotel or wants_car:
            score -= 2

    elif slug == "yueka-platinum":
        score += 30
        if wants_points:
            score += 12
            explain_add(reasons, "白金悦卡更适合把积分回报做大。")
        if assets >= 200000:
            score += 15
            explain_add(reasons, "中信资产20万元以上时，多倍积分潜力更强。")
        elif assets >= 50000:
            score += 8
            explain_add(reasons, "已有一定资产基础，能部分发挥悦卡价值。")
        if annual_fee_pref in {"none", "low"}:
            score -= 12
            cautions.append("白金悦卡首年刚性年费，年费敏感客户需谨慎。")
        else:
            explain_add(reasons, "客户对年费容忍度较高，可接受白金权益结构。")

    elif slug == "marriott-premium":
        score += 22
        if wants_hotel or "万豪" in hotel_pref or "marriott" in hotel_pref:
            score += 24
            explain_add(reasons, "客户明确偏好万豪/酒店权益，这张卡匹配度很高。")
        if travel == "frequent":
            score += 10
            explain_add(reasons, "高频商旅更能用到会籍、房晚和免费房晚。")
        if annual_fee_pref in {"none", "low"}:
            score -= 10
            cautions.append("年费980元，不适合完全不接受年费的客户。")
        if monthly_income < 8000:
            cautions.append("如果只是普通日常消费，980元年费的性价比未必合适。")

    elif slug == "iche-pro":
        score += 18
        if car_owner or ev_owner or wants_car:
            score += 28
            explain_add(reasons, "客户存在明显车主/充电/加油/洗车需求。")
        else:
            score -= 10
            cautions.append("无车客户通常不建议优先推荐车主卡。")
        if annual_fee_pref in {"none", "low"}:
            score += 5
            explain_add(reasons, "刷卡达标免年费，年费压力相对可控。")

    elif slug == "visa-xiaoyao":
        score += 22
        if intl or abroad or wants_overseas:
            score += 28
            explain_add(reasons, "客户有境外/留学/国际出行需求，匹配这张卡的核心权益。")
        if wants_points:
            score += 4
            explain_add(reasons, "境外交易2倍积分对海外消费有帮助。")
        if not (intl or abroad or wants_overseas):
            cautions.append("如果客户几乎没有境外需求，这张卡不会是最高优先级。")

    elif slug == "feichangzhun":
        score += 24
        if travel == "frequent" or wants_delay:
            score += 24
            explain_add(reasons, "高频飞行/航延保障需求与飞常准联名卡高度匹配。")
        if annual_fee_pref in {"none", "low"}:
            score += 6
            explain_add(reasons, "刷卡达标免年费，适合控制用卡成本。")
        if any(k in joined for k in ["机场", "贵宾厅", "快速安检"]):
            score += 5
            explain_add(reasons, "客户重视机场体验，可用到贵宾厅和快速安检类权益。")

    elif slug == "guohang-ctrip":
        score += 18
        if wants_miles or any(k in airline_pref for k in ["国航", "凤凰知音", "air china"]):
            score += 30
            explain_add(reasons, "客户明确偏好国航/凤凰知音体系，里程匹配度高。")
        if any(k in joined for k in ["携程", "差旅", "机票"]):
            score += 8
            explain_add(reasons, "机票/携程/差旅场景较多，可发挥联名价值。")
        if annual_fee_pref in {"none", "low"}:
            explain_add(reasons, "可优先考虑金卡版本，刷卡免年费。")
        if not wants_miles and travel == "rare":
            cautions.append("若并非国航用户、也不积里程，这张卡优势有限。")

    return score, reasons, cautions


def rank_cards(profile: Dict[str, Any]) -> List[Dict[str, Any]]:
    catalog = load_catalog()
    terms = normalize_texts(profile)
    ranked: List[Dict[str, Any]] = []
    for card in catalog:
        if card.get("status") != "active":
            continue
        score, reasons, cautions = score_card(card, profile, terms)
        ranked.append({
            "score": score,
            "card": card,
            "reasons": reasons,
            "cautions": cautions,
        })
    ranked.sort(key=lambda x: x["score"], reverse=True)
    return ranked


def recommend_markdown(profile: Dict[str, Any]) -> str:
    top = rank_cards(profile)[:3]
    lines: List[str] = []
    lines.append("# 中信银行信用卡推荐结果\n")
    lines.append(
        f"客户年龄：{profile.get('age', '未提供')}；月收入：{profile.get('monthly_income', '未提供')}；年费容忍度：{profile.get('annual_fee_tolerance', '未提供')}\n"
    )
    lines.append("## 推荐TOP 3\n")
    for idx, item in enumerate(top, 1):
        score = item["score"]
        card = item["card"]
        reasons = item["reasons"]
        cautions = item["cautions"]
        lines.append(f"### {idx}. {card['name']}  （匹配分：{score}）")
        lines.append(f"- 卡级：{card['tier']}")
        lines.append(f"- 年费：{card['annual_fee']}")
        lines.append(f"- 适合场景：{'、'.join(card['fit_tags'])}")
        lines.append(f"- 亮点：{'；'.join(card['highlights'])}")
        if reasons:
            lines.append(f"- 为什么推荐：{'；'.join(reasons[:4])}")
        if cautions:
            lines.append(f"- 提醒：{'；'.join(cautions[:3])}")
        lines.append(f"- 详情页：{card['detail_url']}")
        lines.append(f"- 申请页：{card.get('apply_url', card['detail_url'])}\n")

    lines.append("## 申请协助建议\n")
    lines.append("1. 先确认主卡申请年龄通常为18-60周岁。")
    lines.append("2. 网上申请通常需要填写工作单位信息；如无单位信息，可携带资料到网点尝试纸质申请。")
    lines.append("3. 柜台申请常见材料包括：身份证明、工作证明、财力证明。")
    lines.append("4. 进度查询可通过官网、短信 SQJD+证件号码 发送至106980095558、或官方微信/动卡空间办理。")
    lines.append("5. 最终卡种、额度、是否批核，以中信银行官方审批结果为准。")
    return "\n".join(lines)


def recommend_json(profile: Dict[str, Any]) -> Dict[str, Any]:
    ranked = rank_cards(profile)
    top = ranked[:3]
    return {
        "profile_summary": {
            "age": profile.get("age"),
            "monthly_income": profile.get("monthly_income"),
            "annual_fee_tolerance": profile.get("annual_fee_tolerance"),
        },
        "top_cards": [
            {
                "rank": idx,
                "score": item["score"],
                "slug": item["card"]["slug"],
                "name": item["card"]["name"],
                "tier": item["card"]["tier"],
                "annual_fee": item["card"]["annual_fee"],
                "fit_tags": item["card"]["fit_tags"],
                "detail_url": item["card"]["detail_url"],
                "apply_url": item["card"].get("apply_url", item["card"]["detail_url"]),
                "reasons": item["reasons"],
                "cautions": item["cautions"],
            }
            for idx, item in enumerate(top, 1)
        ],
        "recommended_card_slug": top[0]["card"]["slug"] if top else None,
    }


def assist(card_slug: str) -> str:
    catalog = load_catalog()
    matches = [c for c in catalog if c["slug"] == card_slug]
    if not matches:
        raise SystemExit(f"未找到卡片：{card_slug}")
    card = matches[0]
    lines = [f"# {card['name']} 申请协助", ""]
    lines.append(f"- 状态：{card['status']}")
    lines.append(f"- 卡级：{card['tier']}")
    lines.append(f"- 年费：{card['annual_fee']}")
    lines.append(f"- 详情页：{card['detail_url']}")
    if card.get("apply_url"):
        lines.append(f"- 申请页：{card['apply_url']}")
    if card.get("alternate_apply_url"):
        lines.append(f"- 备选申请页：{card['alternate_apply_url']}")
    lines.append("")
    lines.append("## 办理步骤")
    lines.append("1. 先确认年龄是否在18-60周岁，并准备身份证明。")
    lines.append("2. 如走线上申请，按官网页面填写个人信息、工作单位信息、联系方式等。")
    lines.append("3. 如页面或短信要求补充身份确认/面签/柜台办理，按官方提示办理。")
    lines.append("4. 如申请成功，按短信或寄卡指引激活电子卡/实体卡。")
    lines.append("")
    lines.append("## 常见材料")
    lines.append("- 身份证明：身份证、临时身份证、户口本等官方认可材料。")
    lines.append("- 工作证明：工作证复印件或单位开具的工作证明。")
    lines.append("- 财力证明：收入证明、工资流水、存款证明、房产证明等。")
    lines.append("")
    lines.append("## 进度跟踪")
    lines.append("- 官网办卡进度查询")
    lines.append("- 短信：SQJD+证件号码，发送到 106980095558")
    lines.append("- 微信公众号：中信银行信用卡（zxyhxyk）")
    lines.append("- 动卡空间 APP")
    lines.append("")
    lines.append("## 风险提示")
    lines.append("- 最终卡种、额度、年费和权益以官方申请页、条款细则和实际审批结果为准。")
    lines.append("- 对于已经停止发行的卡片，不要再推荐给新客户。")
    return "\n".join(lines)


def main() -> None:
    parser = argparse.ArgumentParser(description="CITIC credit card advisor")
    sub = parser.add_subparsers(dest="cmd", required=True)

    p1 = sub.add_parser("recommend", help="生成推荐结果")
    p1.add_argument("--profile", help="客户画像 JSON 文件")
    p1.add_argument("--json", help="客户画像 JSON 字符串")
    p1.add_argument("--format", choices=["markdown", "json"], default="markdown", help="输出格式")

    p2 = sub.add_parser("assist", help="输出单卡申请协助说明")
    p2.add_argument("--card", required=True, help="卡片 slug")

    args = parser.parse_args()
    if args.cmd == "recommend":
        profile = load_profile(args)
        if args.format == "json":
            print(json.dumps(recommend_json(profile), ensure_ascii=False, indent=2))
        else:
            print(recommend_markdown(profile))
    elif args.cmd == "assist":
        print(assist(args.card))


if __name__ == "__main__":
    main()


path: scripts/profile_extractor.py
content: #!/usr/bin/env python3
"""Extract candidate credit-card application fields from an OpenClaw workspace.

Safety model:
- Prefer structured JSON in profiles/applicant_profile.json
- Then read USER.md / MEMORY.md / memory/*.md for candidate values
- Treat extracted values as candidates only; never auto-submit based on them
- Avoid guessing sensitive values that are not explicitly present
"""
from __future__ import annotations

import argparse
import json
import re
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Dict, List, Optional

FIELD_PATTERNS = {
    "full_name": [r"(?:姓名|名字|name)[:：]\s*(.+)"],
    "gender": [r"(?:性别|gender)[:：]\s*(男|女|male|female)"],
    "birth_date": [r"(?:出生日期|生日|birth(?:\s+date)?)[:：]\s*([0-9]{4}[-/.年][0-9]{1,2}[-/.月][0-9]{1,2}日?)"],
    "phone": [r"(?:手机号|手机|联系电话|phone)[:：]\s*(1[3-9]\d{9})"],
    "email": [r"(?:邮箱|电子邮箱|email)[:：]\s*([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})"],
    "city": [r"(?:城市|所在城市|city)[:：]\s*(.+)"],
    "residential_address": [r"(?:居住地址|住宅地址|家庭住址|address)[:：]\s*(.+)"],
    "mailing_address": [r"(?:邮寄地址|通讯地址)[:：]\s*(.+)"],
    "employer_name": [r"(?:工作单位|单位名称|公司名称|employer)[:：]\s*(.+)"],
    "employer_city": [r"(?:单位城市|工作城市)[:：]\s*(.+)"],
    "job_title": [r"(?:职位|岗位|职务|job\s*title)[:：]\s*(.+)"],
    "education": [r"(?:学历|education)[:：]\s*(.+)"],
    "monthly_income": [r"(?:月收入|税前月薪|income)[:：]\s*(.+)"],
    "housing_status": [r"(?:住房情况|居住情况)[:：]\s*(.+)"],
    # Full ID number is intentionally only accepted from structured JSON, not free-text memory.
    "id_last4": [r"(?:身份证后四位|证件号后四位)[:：]\s*(\d{4}|[0-9Xx]{4})"],
}

PREFERRED_JSON = Path('profiles/applicant_profile.json')
TEMPLATE_JSON = Path('profiles/applicant_profile.template.json')

@dataclass
class Candidate:
    field: str
    value: str
    source: str
    confidence: str
    masked_preview: str


def mask_value(field: str, value: str) -> str:
    v = value.strip()
    if not v:
        return v
    if field == 'phone' and len(v) >= 7:
        return v[:3] + '****' + v[-4:]
    if field == 'email' and '@' in v:
        name, domain = v.split('@', 1)
        head = name[:2] if len(name) >= 2 else name[:1]
        return head + '***@' + domain
    if field in {'residential_address', 'mailing_address'} and len(v) > 8:
        return v[:6] + '***' + v[-2:]
    if field == 'full_name' and len(v) >= 2:
        return v[0] + '*' * (len(v)-1)
    return v


def load_json_profile(workspace: Path) -> Dict[str, str]:
    for rel in (PREFERRED_JSON, TEMPLATE_JSON):
        p = workspace / rel
        if p.exists():
            try:
                data = json.loads(p.read_text(encoding='utf-8'))
                if isinstance(data, dict):
                    return data
            except Exception:
                pass
    return {}


def scan_text_file(path: Path) -> str:
    try:
        return path.read_text(encoding='utf-8', errors='ignore')
    except Exception:
        return ''


def collect_text_sources(workspace: Path) -> List[tuple[str, str]]:
    files: List[tuple[str, str]] = []
    for rel in ('USER.md', 'MEMORY.md'):
        p = workspace / rel
        if p.exists():
            files.append((rel, scan_text_file(p)))
    mem_dir = workspace / 'memory'
    if mem_dir.exists():
        for p in sorted(mem_dir.glob('*.md')):
            files.append((str(p.relative_to(workspace)), scan_text_file(p)))
    return files


def extract_from_text(field: str, text: str) -> Optional[str]:
    for pattern in FIELD_PATTERNS.get(field, []):
        m = re.search(pattern, text, flags=re.IGNORECASE)
        if m:
            val = m.group(1).strip()
            val = re.sub(r'\s+$', '', val)
            return val
    return None


def build_candidates(workspace: Path) -> List[Candidate]:
    candidates: List[Candidate] = []
    json_profile = load_json_profile(workspace)

    # Structured JSON is preferred and can include high-sensitivity fields explicitly provided by the user.
    for field, value in json_profile.items():
        if value in ('', None, [], {}):
            continue
        if field.startswith('consent_') or field == 'notes':
            continue
        if isinstance(value, (list, dict, bool, int, float)):
            value_str = json.dumps(value, ensure_ascii=False)
        else:
            value_str = str(value)
        candidates.append(Candidate(
            field=field,
            value=value_str,
            source='profiles/applicant_profile.json or template',
            confidence='high',
            masked_preview=mask_value(field, value_str),
        ))

    existing_fields = {c.field for c in candidates}
    for source_name, text in collect_text_sources(workspace):
        for field in FIELD_PATTERNS:
            if field in existing_fields:
                continue
            extracted = extract_from_text(field, text)
            if extracted:
                candidates.append(Candidate(
                    field=field,
                    value=extracted,
                    source=source_name,
                    confidence='medium',
                    masked_preview=mask_value(field, extracted),
                ))
                existing_fields.add(field)
    return candidates


def main() -> None:
    parser = argparse.ArgumentParser(description='Extract candidate applicant profile fields from OpenClaw workspace files.')
    parser.add_argument('--workspace', default='.', help='Path to the OpenClaw workspace root')
    parser.add_argument('--json', action='store_true', help='Output JSON instead of markdown-like text')
    parser.add_argument('--out', help='Optional path to write the JSON payload')
    args = parser.parse_args()

    workspace = Path(args.workspace).resolve()
    candidates = build_candidates(workspace)
    payload = {
        'workspace': str(workspace),
        'warning': 'Candidates only. Confirm every field with the user before filling the official application form. Do not auto-submit.',
        'candidates': [asdict(c) for c in candidates],
    }

    if args.out:
        out_path = Path(args.out)
        out_path.parent.mkdir(parents=True, exist_ok=True)
        out_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding='utf-8')

    if args.json:
        print(json.dumps(payload, ensure_ascii=False, indent=2))
        return

    print('# 候选申请资料（待确认）')
    print()
    print(payload['warning'])
    print()
    for item in payload['candidates']:
        print(f"- {item['field']}: {item['masked_preview']}  [source={item['source']}; confidence={item['confidence']}]")


if __name__ == '__main__':
    main()


path: profiles/applicant_profile.template.json
content: {
  "full_name": "",
  "gender": "",
  "birth_date": "",
  "phone": "",
  "email": "",
  "id_type": "中华人民共和国居民身份证",
  "id_number": "",
  "city": "",
  "residential_address": "",
  "mailing_address": "",
  "employer_name": "",
  "employer_city": "",
  "job_title": "",
  "education": "",
  "monthly_income": "",
  "housing_status": "",
  "assets_with_citic": 0,
  "car_owner": false,
  "ev_owner": false,
  "international_travel": false,
  "student_abroad": false,
  "travel_frequency": "rare",
  "hotel_preference": "",
  "airline_preference": "",
  "annual_fee_tolerance": "low",
  "interests": [],
  "spend_categories": [],
  "needs": [],
  "consent_to_prefill": false,
  "consent_to_browser_fill": false,
  "consent_to_final_submit": false,
  "notes": "默认建议保持 consent_to_final_submit = false，由用户本人在官网完成最终提交。"
}

path: output/application_plan.json
content: {
  "workflow_name": "citic_creditcard_browser_prefill",
  "workspace": "/mnt/data/citic_rewrite/citic-creditcard-auto-apply",
  "browser_profile_default": "openclaw",
  "browser_profile_existing_session": "user",
  "recommended_card": {
    "rank": 1,
    "score": 38,
    "slug": "yan-standard-platinum",
    "name": "中信银行颜卡标准白金卡",
    "tier": "白金卡",
    "annual_fee": "480元/年；首年免年费，次年刷卡达标免",
    "fit_tags": [
      "日常消费",
      "入门白金",
      "年轻客群",
      "低年费偏好",
      "颜值卡",
      "通用型"
    ],
    "detail_url": "https://creditcard.ecitic.com/h5/shenqing/240515/index.html?sid=SJWAPHD3",
    "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?pid=CS0602&sid=YDKHD",
    "reasons": [
      "首年免年费、次年刷卡达标可免年费，适合低年费偏好。"
    ],
    "cautions": []
  },
  "backup_cards": [
    {
      "rank": 2,
      "score": 36,
      "slug": "i-platinum",
      "name": "中信银行i白金信用卡",
      "tier": "白金卡",
      "annual_fee": "480元/年；首年减免，账单年内刷卡12次免次年年费",
      "fit_tags": [
        "线上消费",
        "积分党",
        "经常出差",
        "年轻白领",
        "航延保障"
      ],
      "detail_url": "https://creditcard.ecitic.com/platinum/ibaijin.html",
      "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?pid=CS0083&sid=YDKHD",
      "reasons": [],
      "cautions": []
    },
    {
      "rank": 3,
      "score": 34,
      "slug": "yueka-gold",
      "name": "中信银行悦卡（金卡）",
      "tier": "金卡",
      "annual_fee": "以申请页实时展示为准",
      "fit_tags": [
        "积分党",
        "有中信资产",
        "日常高频消费",
        "金卡偏好",
        "生日月积分"
      ],
      "detail_url": "https://creditcard.ecitic.com/h5/shenqing/yueka_g.html",
      "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?pid=CS0076&sid=SJWAPPT#/baseInfo&pid=CS0076&sid=SJWAPPT",
      "reasons": [],
      "cautions": [
        "如无中信资产、且不关注积分，悦卡优势会下降。"
      ]
    }
  ],
  "candidate_fields": [
    {
      "field": "id_type",
      "value": "中华人民共和国居民身份证",
      "source": "profiles/applicant_profile.json or template",
      "confidence": "high",
      "masked_preview": "中华人民共和国居民身份证"
    },
    {
      "field": "assets_with_citic",
      "value": "0",
      "source": "profiles/applicant_profile.json or template",
      "confidence": "high",
      "masked_preview": "0"
    },
    {
      "field": "car_owner",
      "value": "false",
      "source": "profiles/applicant_profile.json or template",
      "confidence": "high",
      "masked_preview": "false"
    },
    {
      "field": "ev_owner",
      "value": "false",
      "source": "profiles/applicant_profile.json or template",
      "confidence": "high",
      "masked_preview": "false"
    },
    {
      "field": "international_travel",
      "value": "false",
      "source": "profiles/applicant_profile.json or template",
      "confidence": "high",
      "masked_preview": "false"
    },
    {
      "field": "student_abroad",
      "value": "false",
      "source": "profiles/applicant_profile.json or template",
      "confidence": "high",
      "masked_preview": "false"
    },
    {
      "field": "travel_frequency",
      "value": "rare",
      "source": "profiles/applicant_profile.json or template",
      "confidence": "high",
      "masked_preview": "rare"
    },
    {
      "field": "annual_fee_tolerance",
      "value": "low",
      "source": "profiles/applicant_profile.json or template",
      "confidence": "high",
      "masked_preview": "low"
    }
  ],
  "confirmed_fields_required": [
    "full_name",
    "phone",
    "id_type",
    "city",
    "employer_name",
    "monthly_income"
  ],
  "safe_prefill_fields": [
    "full_name",
    "gender",
    "birth_date",
    "phone",
    "email",
    "city",
    "residential_address",
    "mailing_address",
    "employer_name",
    "employer_city",
    "job_title",
    "education",
    "monthly_income",
    "housing_status"
  ],
  "never_autofill_without_user_live_confirmation": [
    "id_number",
    "sms_code",
    "otp",
    "face_verification",
    "signature",
    "agreement_consent",
    "credit_authorization"
  ],
  "browser_sequence": [
    "open official page",
    "snapshot page refs",
    "open official apply page",
    "snapshot form refs",
    "type/select only confirmed fields",
    "stop before OTP / agreement / final submit"
  ],
  "stop_conditions": [
    "captcha shown",
    "otp or sms verification shown",
    "credit authorization or agreement shown",
    "final submit button shown",
    "required field missing or conflicting"
  ],
  "warning": "This plan is for browser-assisted prefill only. The customer must review and complete the final submission personally."
}

path: docs/browser_workflow.md
content: # 浏览器辅助预填建议流程

1. 先用中信信用卡官网首页或“信用卡申请”页确认卡片仍在售。
2. 打开目标卡的官方申请页。
3. 读取并整理候选资料，但先回显给用户确认。
4. 使用 OpenClaw 浏览器逐项填写：
   - 姓名
   - 性别
   - 出生日期
   - 手机号
   - 邮箱
   - 证件类型 / 证件号
   - 所在城市
   - 住宅地址 / 通讯地址
   - 工作单位 / 工作城市 / 职位
   - 学历
   - 月收入 / 收入区间
5. 如果官网表单出现：
   - 滑块验证码
   - 短信验证码
   - 人脸识别
   - 电子签名
   - 授权征信查询
   - 最终“提交申请”按钮
   立即停止自动化并交给用户本人完成。
6. 如果页面报错：
   - 所在地区不可申请
   - 必填单位信息缺失
   - 证件类型不支持
   - 申请卡片已下线
   立即向用户说明，并建议改走网点 / 纸质申请 / 其他卡种。

## 推荐的操作口径

- “我已经帮你打开官方申请页，并预填了已确认的信息。”
- “请你本人核对身份证号、单位、收入和地址是否准确。”
- “验证码、授权勾选和最终提交请你自己完成。”

## profile 建议

- 默认使用 `openclaw` 托管 profile
- 只有用户明确要求复用当前浏览器会话时，才改用 `user` existing-session profile


path: docs/security_notes.md
content: # 安全与合规提示

1. 信用卡申请是正式金融业务，涉及高敏感个人信息与授权。
2. OpenClaw 的 memory 是工作区磁盘文件，不应把旧记录默认当作当前准确资料。
3. 对于身份证号、住址、收入、单位等字段，必须逐项让用户确认。
4. 不建议在 skill 中静默扫描全部记忆内容并直接写入官网表单。
5. 不建议用 `user` 浏览器 profile 处理无关站点，因为 existing-session 模式会继承该浏览器会话的登录态。
6. 最终提交、验证码、短信 OTP、人脸识别、电子签名必须由用户本人完成。
7. 推荐结论和自动化预填都不能替代银行实际审批。


path: data/card_catalog.json
content: [
  {
    "slug": "yan-standard-platinum",
    "name": "中信银行颜卡标准白金卡",
    "status": "active",
    "tier": "白金卡",
    "detail_url": "https://creditcard.ecitic.com/h5/shenqing/240515/index.html?sid=SJWAPHD3",
    "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?pid=CS0602&sid=YDKHD",
    "annual_fee": "480元/年；首年免年费，次年刷卡达标免",
    "income_hint": "以审批结果为准",
    "highlights": [
      "最长50天免息还款期",
      "2小时最高1000元航班延误险",
      "首年免年费",
      "次年刷卡达标免480元年费"
    ],
    "fit_tags": ["日常消费", "入门白金", "年轻客群", "低年费偏好", "颜值卡", "通用型"],
    "avoid_tags": ["酒店会籍刚需", "航空里程刚需", "车主权益刚需"],
    "notes": [
      "适合作为低门槛、日常使用的入门白金卡。",
      "更偏通用消费和基础出行保障，不是酒店/车主/航司强联名卡。"
    ]
  },
  {
    "slug": "i-platinum",
    "name": "中信银行i白金信用卡",
    "status": "active",
    "tier": "白金卡",
    "detail_url": "https://creditcard.ecitic.com/platinum/ibaijin.html",
    "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?pid=CS0083&sid=YDKHD",
    "annual_fee": "480元/年；首年减免，账单年内刷卡12次免次年年费",
    "income_hint": "官网建议税前月薪5000元以上；快捷申请参考年龄25-35岁",
    "highlights": [
      "交易1.5倍积分",
      "网络交易可计双倍积分（以官网细则为准）",
      "2小时最高1000元航班延误险",
      "500万元航空意外伤害险"
    ],
    "fit_tags": ["线上消费", "积分党", "经常出差", "年轻白领", "航延保障"],
    "avoid_tags": ["零年费绝对偏好", "酒店会籍刚需", "车主权益刚需"],
    "notes": [
      "适合线上消费较多、又希望兼顾基础商旅保障的客户。",
      "推荐时要明确首年减免、次年需刷卡达标免年费。"
    ]
  },
  {
    "slug": "yueka-gold",
    "name": "中信银行悦卡（金卡）",
    "status": "active",
    "tier": "金卡",
    "detail_url": "https://creditcard.ecitic.com/h5/shenqing/yueka_g.html",
    "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?pid=CS0076&sid=SJWAPPT#/baseInfo&pid=CS0076&sid=SJWAPPT",
    "annual_fee": "以申请页实时展示为准",
    "income_hint": "以审批结果为准",
    "highlights": [
      "依据在中信名下资产总额，消费最高可享8倍积分回馈",
      "新客户前三个自然月可享最高8倍积分回馈",
      "金卡航班延误4小时最高1000元",
      "最长50天免息还款期"
    ],
    "fit_tags": ["积分党", "有中信资产", "日常高频消费", "金卡偏好", "生日月积分"],
    "avoid_tags": ["完全不看积分", "只看境外权益", "只看酒店会籍"],
    "notes": [
      "核心价值在于多倍积分，特别适合在中信已有一定资产沉淀的客户。",
      "若客户没有资产、也不关注积分，推荐优先级会下降。"
    ]
  },
  {
    "slug": "yueka-platinum",
    "name": "中信银行悦卡（白金卡）",
    "status": "active",
    "tier": "白金卡",
    "detail_url": "https://creditcard.ecitic.com/h5/shenqing/yueka_p.html",
    "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?pid=CS0098&sid=SJWAPPT#/baseInfo&pid=CS0098&sid=SJWAPPT",
    "annual_fee": "白金卡分层：精英480元/年、精逸980元/年、尊贵2000元/年；首年刚性收取，次年可积分兑换或自行缴纳",
    "income_hint": "以审批结果为准；若白金级别未达标，官网说明可能自动更换为更适合的等级",
    "highlights": [
      "依据在中信名下资产总额，消费最高可享8倍积分回馈",
      "新客户前三个自然月可享最高8倍积分回馈",
      "白金卡航班延误2小时最高1000元",
      "最长50天免息还款期"
    ],
    "fit_tags": ["高积分需求", "中信资产客户", "愿意承担年费", "白金客群"],
    "avoid_tags": ["完全不接受年费", "低频消费", "只看航司/酒店联名"],
    "notes": [
      "适合重视积分回报、且对年费容忍度较高或已有较高资产的客户。",
      "推荐时必须把年费和可能降级的规则说清楚。"
    ]
  },
  {
    "slug": "marriott-premium",
    "name": "中信银行万豪旅享家精逸白金卡",
    "status": "active",
    "tier": "精逸白金卡",
    "detail_url": "https://creditcard.ecitic.com/shenqing/wanhao/index.shtml",
    "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?pid=CS0852&sid=YDKHD",
    "annual_fee": "980元/年",
    "income_hint": "以审批结果为准",
    "highlights": [
      "万豪旅享家尊贵金卡会籍",
      "赠送10个累计尊贵会籍有效房晚",
      "赠送1个免费房晚",
      "合格消费累计万豪积分10:1",
      "万豪住宿9折、餐饮8折起"
    ],
    "fit_tags": ["酒店常旅客", "万豪偏好", "商旅出行", "高净值权益", "愿意付年费"],
    "avoid_tags": ["不住酒店", "完全不接受年费", "纯日常小额消费"],
    "notes": [
      "这是酒店联名强需求客户的优先卡，不适合只做普通日常消费的客户。",
      "若客户明确偏好万豪体系或住宿权益，应显著提高优先级。"
    ]
  },
  {
    "slug": "iche-pro",
    "name": "中信银行i车Pro银联i白金信用卡",
    "status": "active",
    "tier": "i白金卡",
    "detail_url": "https://creditcard.ecitic.com/h5/shenqing/240515/index.html?sid=SJWAPHD3",
    "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?cls=SJWAPICNEWBJ02&pid=CS1001&sid=YDKHD",
    "annual_fee": "刷卡达标免年费",
    "income_hint": "以审批结果为准",
    "highlights": [
      "新户达标最高享1年1100元优惠",
      "刷卡达标免年费",
      "激活并绑微信支付送20元券",
      "官网首页显示含充电5折、加油立减、0元洗车等车主导向权益"
    ],
    "fit_tags": ["车主", "新能源车", "加油充电", "洗车权益", "通勤人群"],
    "avoid_tags": ["无车客户", "酒店会籍刚需", "航空里程刚需"],
    "notes": [
      "适合明确有车、尤其关注加油、充电、洗车等场景权益的客户。",
      "如果客户没有车，通常不建议放在前三。"
    ]
  },
  {
    "slug": "visa-xiaoyao",
    "name": "中信银行VISA逍遥白金信用卡",
    "status": "active",
    "tier": "白金卡",
    "detail_url": "https://creditcard.ecitic.com/h5/youhui/221122/index.html",
    "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?cls=wx&code=031wnQ0w3XwUv03qRH2w3O7CzX1wnQ0s&pid=CS0736&sid=SJWXBKDT1&state=1",
    "annual_fee": "当前官网展示为免年费/新户免费申请类表述；以申请页实时展示为准",
    "income_hint": "以审批结果为准",
    "highlights": [
      "100万美元国际救援",
      "留学/境外消费返现活动（需报名，以规则为准）",
      "500万元航空意外保险",
      "境外交易2倍积分",
      "全球全币种免货币转换费"
    ],
    "fit_tags": ["留学", "境外消费", "国际出行", "海外医疗法律协助", "外币消费"],
    "avoid_tags": ["几乎不出境", "不关心境外权益", "纯本地车主场景"],
    "notes": [
      "适合留学生、家长、长期境外消费或频繁国际出行客户。",
      "若客户需求主要在境内日常积分，通常不是第一优先。"
    ]
  },
  {
    "slug": "feichangzhun",
    "name": "中信银行飞常准联名卡（银联白金卡）",
    "status": "active",
    "tier": "银联白金卡",
    "detail_url": "https://creditcard.ecitic.com/h5/shenqing/feichangzhun/index.html",
    "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?code=001JPA000VNO4S1DIe100teQqg1JPA0l&pid=CS0199&sid=SJFCZQD1&state=1",
    "annual_fee": "刷卡达标免年费",
    "income_hint": "以审批结果为准",
    "highlights": [
      "首次申请并开卡后，次月可获12个月飞常准超级会员（标准版）",
      "双重延误保障：飞常准端延误保障 + 中信端2小时最高1000元机票款赔付",
      "页面展示航班延误赔付最高可达1200元",
      "500万元航空意外险",
      "机场贵宾厅和快速安检等会员权益"
    ],
    "fit_tags": ["高频飞行", "航延保障", "机场权益", "差旅人群", "免年费偏好"],
    "avoid_tags": ["几乎不坐飞机", "酒店会籍刚需", "有车场景权益刚需"],
    "notes": [
      "适合非常看重航班延误、机场服务、飞行出差效率的客户。",
      "相比普通通用卡，它更偏出行效率和飞行场景。"
    ]
  },
  {
    "slug": "guohang-ctrip",
    "name": "中信银行国航携程联名卡",
    "status": "active",
    "tier": "金卡/白金卡",
    "detail_url": "https://creditcard.ecitic.com/h5/shenqing/guohangxiecheng/index.html",
    "apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?cls=SJWAPGHXC01&pid=CS0481&sid=YDKHD",
    "alternate_apply_url": "https://cs.creditcard.ecitic.com/citiccard/cardshopcloud/standardcard-h5/index.html?cls=SJWAPGHXCBJ01&pid=CS0478&sid=YDKHD",
    "annual_fee": "金卡刷卡免年费；白金卡页面展示新客户首年免2000元年费",
    "income_hint": "以审批结果为准",
    "highlights": [
      "金卡：计里程交易18元=1凤凰知音里程，航延4小时最高1000元",
      "白金卡：计里程交易10元=1凤凰知音里程，航延2小时最高1000元",
      "白金卡可匹配凤凰知音银卡会籍",
      "金卡90天内飞行10次可升级凤凰知音银卡会籍",
      "乘机返现等活动以官网规则为准"
    ],
    "fit_tags": ["国航偏好", "里程党", "携程出票", "国内商旅", "航司联名"],
    "avoid_tags": ["不坐飞机", "不关心里程", "纯本地生活消费"],
    "notes": [
      "适合明确偏好国航或凤凰知音体系的客户。",
      "推荐时要区分金卡与白金卡的年费、里程比例和航延条件。"
    ]
  },
  {
    "slug": "pet-theme-card",
    "name": "中信银行颜卡宠物主题信用卡",
    "status": "stopped",
    "tier": "金卡",
    "detail_url": "https://creditcard.ecitic.com/shenqing/chongwu/index.shtml",
    "annual_fee": "不再作为新推荐卡",
    "income_hint": "不适用",
    "highlights": ["2024年7月31日起停止开放申请"],
    "fit_tags": [],
    "avoid_tags": ["停止发行"],
    "notes": ["该卡已停发，不应再向新客户推荐。"]
  }
]


path: config/workflow.template.yaml
content: version: 1
name: citic_creditcard_browser_prefill
notes:
  - 这是流程模板，不是 OpenClaw skill 原生执行语法。
  - OpenClaw 实际执行依赖 SKILL.md 指导 + browser 工具 + 辅助脚本。
  - 所有候选申请信息都必须先得到用户确认。

profiles:
  browser_profile_default: openclaw
  browser_profile_existing_session: user

inputs:
  applicant_profile_json: profiles/applicant_profile.json
  workspace_files:
    - USER.md
    - MEMORY.md
    - memory/*.md
  card_catalog: data/card_catalog.json

stages:
  - id: recommend_card
    goal: 基于用户画像和官网在售卡种，输出推荐卡与备选卡
    actions:
      - tool: local_script
        script: scripts/citic_cc_advisor.py
        args:
          - recommend
          - --profile
          - profiles/applicant_profile.json
          - --format
          - json
      - tool: browser
        operation: open
        url: https://creditcard.ecitic.com/
      - tool: browser
        operation: snapshot
        purpose: 核对官网首页/办卡页是否仍展示目标卡或相关入口
    outputs:
      - recommended_card_slug
      - top_cards
      - official_page_verified

  - id: extract_candidates
    goal: 从用户明确授权的工作区资料中抽取候选申请字段
    requires_user_consent: true
    actions:
      - tool: local_script
        script: scripts/profile_extractor.py
        args:
          - --workspace
          - .
          - --json
    outputs:
      - candidate_fields
      - field_sources
      - field_confidences

  - id: build_prefill_plan
    goal: 生成预填计划，明确哪些字段可填、哪些字段必须人工处理
    actions:
      - tool: local_script
        script: scripts/build_application_plan.py
        args:
          - --workspace
          - .
          - --workflow
          - config/workflow.template.yaml
          - --out
          - output/application_plan.json
    outputs:
      - output/application_plan.json

  - id: open_apply_page
    goal: 打开官网申请页并定位到表单
    actions:
      - tool: browser
        operation: open
        url_from: recommended_card.apply_url
      - tool: browser
        operation: snapshot
        purpose: 获取申请页 ref，并识别立即申请/下一步按钮
      - tool: browser
        operation: click_by_ref
        target: 申请按钮或继续按钮
        when: 页面出现可点击 ref 且已核对上下文无误

  - id: fill_form
    goal: 仅填写已确认字段
    actions:
      - tool: browser
        operation: snapshot
        purpose: 获取表单页 ref，避免使用脆弱文本 selector
      - tool: browser
        operation: type_by_ref
        source: confirmed_fields
        mapping:
          full_name: 姓名
          phone: 手机号
          email: 电子邮箱
          birth_date: 出生日期
          city: 所在城市
          residential_address: 居住地址
          mailing_address: 通讯地址
          employer_name: 工作单位
          employer_city: 单位城市
          job_title: 职位
          education: 学历
          monthly_income: 月收入
      - tool: browser
        operation: select_or_click_by_ref
        source: confirmed_fields
        mapping:
          gender: 性别
          housing_status: 住房情况
    stop_conditions:
      - 验证码出现
      - OTP 或短信验证出现
      - 征信授权 / 协议签署出现
      - 最终提交按钮出现
      - 出现未确认的官网必填字段

  - id: handoff_to_user
    goal: 在最终提交前交由用户本人完成
    actions:
      - tool: none
        instruction: 提示用户核对全部信息，并由本人完成验证码、签名、授权与最终提交


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