import os
import sys
import re
import random
import argparse
import platform
import subprocess
import urllib.request
from datetime import datetime, timezone, timedelta
import json
import time
import threading
import socketserver
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs, unquote
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from qiniu import Auth, put_file_v2, CdnManager, BucketManager

# 确保同目录的共享模块可导入
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from artifacts_schema import ARTIFACTS_FILENAME, create_empty_artifacts, now_iso as _now_iso

# 跨平台获取 openclaw 目录
def get_openclaw_path():
    if platform.system() == 'Windows' or os.path.exists('/root/.openclaw'):
        return '/root/.openclaw'
    return os.path.expanduser('~/.openclaw')

claw = os.environ.get("CLAW_NAME", "claw")

BASE_URL = f"https://cdn.yiranlaoshi.com/{claw}"

QINIU_KEY = "T3tgxM7EMx1j4VESw4m4PIfFXoOvBo-wQEOQewXX"
QINIU_TOKEN = "PVZvlKVOjX2RqlV2ILMg-QwpNMssOlpVbaEzypz0"


QINIU_BUCKET = "yiran1"

access_key = QINIU_KEY
secret_key = QINIU_TOKEN
bucket_name = QINIU_BUCKET
q = Auth(access_key, secret_key)
cdn_manager = CdnManager(q)
bucket_manager = BucketManager(q)

# 是否将 __MY_ARTIFACTS__.json 同步到 OSS
# True  = 上传到七牛（旧行为）
# False = 不上传，且主动从七牛删除已有的（新行为）
SYNC_ARTIFACTS_JSON_TO_OSS = False


# 忽略的目录名（路径中只要包含这些目录段就跳过）
IGNORE_DIR_PATTERNS = {'.git', 'node_modules', '__pycache__', '.myclaw'}

# 忽略的文件名
IGNORE_FILE_PATTERNS = {'.myclaw-sync.pid', '.DS_Store', '.gitignore', '.gitkeep'}


def _path_has_ignored_dir(filepath):
    """检查路径中是否包含需要忽略的目录段"""
    parts = os.path.normpath(filepath).split(os.sep)
    return any(part in IGNORE_DIR_PATTERNS for part in parts)


class MyHandler(FileSystemEventHandler):

    def _is_file_event(self, event):
        return not getattr(event, "is_directory", False)

    def _should_ignore(self, filepath):
        basename = os.path.basename(filepath)
        if basename in IGNORE_FILE_PATTERNS:
            return True
        if _path_has_ignored_dir(filepath):
            return True
        # 忽略 .openclaw 根目录下的隐藏文件（非 workspace 目录内的）
        if basename.startswith('.') and 'workspace' not in filepath:
            return True
        return False

    # def on_created(self, event):
    #     if not self._is_file_event(event):
    #         return
    #     print(f"🟢 新建: {event.src_path}")
    #     file_gen(event.src_path, "add")

    def on_modified(self, event):
        if not self._is_file_event(event):
            return
        if self._should_ignore(event.src_path):
            return
        print(f"🟡 修改: {event.src_path}")
        file_gen(event.src_path, "add")

    def on_deleted(self, event):
        if not self._is_file_event(event):
            return
        if self._should_ignore(event.src_path):
            return
        print(f"🔴 删除: {event.src_path}")
        file_gen(event.src_path, "delete")

    def on_moved(self, event):
        if not self._is_file_event(event):
            return
        if self._should_ignore(event.src_path):
            return
        print(f"🔵 移动: {event.src_path} -> {getattr(event, 'dest_path', '')}")
        file_gen(event.src_path, "delete")
        if getattr(event, 'dest_path', ''):
            file_gen(event.dest_path, "add")


def now_iso():
    return _now_iso()


def gen_id():
    return f"asset-{int(datetime.now().timestamp() * 1000)}"


def get_type(file_path):
    return file_path.split(".")[-1] if "." in file_path else "unknown"


def init_config(workspace_id, file_path, method="add"):
    base_path = get_openclaw_path()
    file = f"{base_path}/{workspace_id}/.myclaw/{ARTIFACTS_FILENAME}"

    # 确保目录存在
    os.makedirs(os.path.dirname(file), exist_ok=True)

    now = now_iso()

    # 如果文件不存在，先初始化
    if not os.path.exists(file):
        data = create_empty_artifacts(workspace_id, BASE_URL)
    else:
        with open(file, "r", encoding="utf-8") as f:
            try:
                data = json.load(f)
            except:
                data = create_empty_artifacts(workspace_id, BASE_URL)
        # 确保根级字段存在且最新
        if "created_at" not in data:
            data["created_at"] = now
        data["updated_at"] = now
        data["base_url"] = BASE_URL  # 始终同步为当前 CDN 前缀

    if method == "delete":
        # 删除逻辑：将匹配 path 的项过滤掉，并更新根级更新时间
        data["assets"] = [asset for asset in data.get(
            "assets", []) if asset.get("path") != file_path]
        data["updated_at"] = now
    else:
        found = False
        # 查找是否已存在该 path
        for asset in data.get("assets", []):
            if asset.get("path") == file_path:
                # 保证已有 created_at，不被覆盖；更新 updated_at 和 type（以防扩展名变化）
                asset.setdefault("created_at", now)
                asset["updated_at"] = now
                asset["type"] = get_type(file_path)
                found = True
                break

        # 不存在则新增（asset 已包含创建/更改时间）
        if not found:
            new_asset = {
                "id": gen_id(),
                "type": get_type(file_path),
                "path": file_path,
                "created_at": now,
                "updated_at": now
            }
            data["assets"].append(new_asset)

        # 更新根级更新时间
        data["updated_at"] = now

    # 写回文件
    with open(file, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def file_gen(path, method):
    # 规范化路径，兼容 Windows 和 *nix
    norm_path = os.path.normpath(path)
    parts = norm_path.split(os.sep)

    # 查找包含 workspace 标识的分段（例如 workspace 或 workspace-xxx）
    space_idx = -1
    for i, p in enumerate(parts):
        if "workspace" in p:
            space_idx = i
            break

    if space_idx == -1:
        # 路径中不包含 workspace，不处理
        return

    space_id = parts[space_idx]
    # 相对路径：workspace 后面的所有段，使用 '/' 作为存储格式（保证跨平台一致）
    relative_parts = parts[space_idx + 1:]
    if not relative_parts:
        # 没有文件名，可能是目录事件，忽略
        return
    relative_path = "/".join(relative_parts)

    # 上传 key 保留 workspace 段（用于 CDN 存储），但写入 JSON 时只保存相对路径
    path_url = f"{space_id}/{relative_path}"
    key = f"{claw}/{path_url}"

    if method == "delete":
        # 删除时不需要上传，只清理配置文件记录，传入的 file_path 不包含 space_id（relative_path）
        init_config(space_id, relative_path, method="delete")
        print(f"🗑️ 已删除配置记录: {relative_path}")
    else:
        # 添加或修改时进行上传
        try:
            token = q.upload_token(bucket_name, key, 3600)
            # 直接使用 put_file_v2 上传文件路径，避免读取到空内容时导致 SDK 报错缺少 data 参数
            ret, info = put_file_v2(token, key, path)

            # 刷新 CDN
            cdn_url = f"https://cdn.yiranlaoshi.com/{key}"
            cdn_ret, cdn_info = cdn_manager.refresh_urls([cdn_url])

            # 如果是 __MY_ARTIFACTS__.json 配置文件，只上传不写入（防止循环触发）
            if os.path.basename(path) == ARTIFACTS_FILENAME:
                print(f"🚀 已上传配置文件: {relative_path}")
                print(f"风 CDN刷新: {cdn_url} - {cdn_info.status_code if info else 'unknown'}")
            else:
                # 记录时使用不包含 workspace 的相对路径
                init_config(space_id, relative_path, method="add")
                print(f"🚀 已上传并记录: {relative_path}")
                print(f"风 CDN刷新: {cdn_url} - {cdn_info.status_code if info else 'unknown'}")
        except Exception as e:
            print(f"❌ 上传/读取文件失败: {e}")


def sync_all(workspace_id):
    """全量同步指定 workspace 下所有文件"""
    base_path = get_openclaw_path()
    workspace_path = f"{base_path}/{workspace_id}"

    if not os.path.exists(workspace_path):
        print(f"[错误] workspace 不存在: {workspace_path}")
        return False

    print(f"[全量同步] workspace: {workspace_id}")
    print(f"[全量同步] 目录: {workspace_path}")
    print("-" * 50)

    file_count = 0
    success_count = 0

    for root, dirs, files in os.walk(workspace_path):
        # 剪枝：跳过忽略目录，避免递归进入
        dirs[:] = [d for d in dirs if d not in IGNORE_DIR_PATTERNS]

        for filename in files:
            # 排除配置文件自身（防止循环）
            if filename == ARTIFACTS_FILENAME:
                continue
            if filename in IGNORE_FILE_PATTERNS:
                continue

            local_path = os.path.join(root, filename)
            rel_path = os.path.relpath(local_path, workspace_path)
            relative_path = rel_path.replace(os.sep, "/")

            file_count += 1

            # 上传到七牛云
            path_url = f"{workspace_id}/{relative_path}"
            key = f"{claw}/{path_url}"

            try:
                token = q.upload_token(bucket_name, key, 3600)
                ret, info = put_file_v2(token, key, local_path)
                cdn_url = f"https://cdn.yiranlaoshi.com/{key}"
                cdn_manager.refresh_urls([cdn_url])
                print(f"  ✅ {relative_path}")
                success_count += 1
            except Exception as e:
                print(f"  ❌ {relative_path}: {e}")

            # 写入配置记录
            init_config(workspace_id, relative_path, method="add")

    print("-" * 50)
    print(f"[完成] 共 {file_count} 个文件，成功 {success_count} 个")

    # 反向查对：JSON 中有记录但磁盘上不存在的，剔除
    config_file = f"{workspace_path}/.myclaw/{ARTIFACTS_FILENAME}"
    if os.path.exists(config_file):
        with open(config_file, "r", encoding="utf-8") as f:
            try:
                data = json.load(f)
            except:
                data = None

        if data and data.get("assets"):
            before = len(data["assets"])
            data["assets"] = [
                asset for asset in data["assets"]
                if os.path.exists(os.path.join(workspace_path, asset.get("path", "")))
            ]
            removed = before - len(data["assets"])
            if removed > 0:
                data["updated_at"] = now_iso()
                with open(config_file, "w", encoding="utf-8") as f:
                    json.dump(data, f, ensure_ascii=False, indent=2)
                print(f"[清理] 剔除 {removed} 条已不存在的记录")

    # 最后处理配置文件的 OSS 同步
    key = f"{claw}/{workspace_id}/.myclaw/{ARTIFACTS_FILENAME}"
    if SYNC_ARTIFACTS_JSON_TO_OSS:
        config_path = f"{workspace_path}/.myclaw/{ARTIFACTS_FILENAME}"
        if os.path.exists(config_path):
            print(f"[配置] 上传配置文件...")
            try:
                token = q.upload_token(bucket_name, key, 3600)
                ret, info = put_file_v2(token, key, config_path)
                cdn_url = f"https://cdn.yiranlaoshi.com/{key}"
                cdn_manager.refresh_urls([cdn_url])
                print(f"  ✅ __MY_ARTIFACTS__.json")
            except Exception as e:
                print(f"  ❌ __MY_ARTIFACTS__.json: {e}")
    else:
        print(f"[配置] 从 OSS 删除配置文件...")
        try:
            ret, info = bucket_manager.delete(bucket_name, key)
            if info.status_code == 200 or info.status_code == 612:
                # 612 = 文件不存在，视为成功
                print(f"  ✅ 已从 OSS 删除 __MY_ARTIFACTS__.json")
            else:
                print(f"  ❌ 删除失败: {info.status_code}")
        except Exception as e:
            print(f"  ❌ 删除失败: {e}")

    return True


# ═══════════════════════════════════════════════════════════════
# Fork：从 CDN 克隆他人 workspace 到本地
# ═══════════════════════════════════════════════════════════════

# CDN 根域名（不含 scheme）
CDN_HOST = "cdn.yiranlaoshi.com"

# Fork URL 固定前缀：https://cdn.yiranlaoshi.com/myclaw/show/{claw}/{workspace}/{version}/...
FORK_CDN_PREFIX = "myclaw/show"

# 异步 job 状态表 { job_id -> {"status": "running"|"done"|"failed", ...} }
_fork_jobs = {}


def _list_files_from_qiniu(prefix):
    """按前缀列出七牛 OSS 上的所有文件"""
    marker = None
    files = []
    while True:
        ret, eof, info = bucket_manager.list(
            bucket_name, prefix=prefix, marker=marker, limit=1000)
        if ret is not None:
            files.extend(ret.get('items', []))
            marker = ret.get('marker')
            if eof:
                break
        else:
            break
    return files


def _download_from_cdn(key, local_path):
    """从公共 CDN 下载单个文件"""
    from urllib.parse import quote
    # key 可能含中文，需要对路径部分做 percent-encode（保留 / 分隔符）
    encoded_key = quote(key, safe='/')
    url = f"https://cdn.yiranlaoshi.com/{encoded_key}"
    os.makedirs(os.path.dirname(local_path), exist_ok=True)
    try:
        import ssl
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        with urllib.request.urlopen(url, context=ctx) as resp, open(local_path, 'wb') as f:
            f.write(resp.read())
        return True
    except Exception as e:
        print(f"  ❌ 下载失败 {key}: {e}")
        return False


def _do_fork(job_id, src_clawname, src_workspace, src_version, entry_rel_path=None):
    """在后台线程中执行 fork"""
    try:
        rand_id = str(random.randint(100, 999))

        # create_agent.js 会自动加 "workspace-" 前缀，所以这里只传去掉前缀的名字
        # 例：src_workspace="workspace-fangshunhe" → agent_name="fangshunhe-v1-fork-475"
        #     mc new 创建目录 → workspace-fangshunhe-v1-fork-475  ✓
        base_name = src_workspace
        if base_name.startswith("workspace-"):
            base_name = base_name[len("workspace-"):]
        agent_name = f"{base_name}-v{src_version}-fork-{rand_id}"
        new_workspace = f"workspace-{agent_name}"

        print(f"[Fork] 开始: {src_workspace} v{src_version} → {new_workspace}")
        if entry_rel_path:
            print(f"[Fork] 入口文件: {entry_rel_path}")

        # mc new 创建 workspace（不带首条消息，等文件下载完再发）
        mc_result = subprocess.run(
            ["mc", "new", agent_name],
            capture_output=True, text=True
        )
        if mc_result.returncode != 0:
            _fork_jobs[job_id] = {
                "status": "failed",
                "error": f"mc new 失败: {mc_result.stderr.strip()}"
            }
            return

        base_path = get_openclaw_path()
        # create_agent.js: workspaceDir = openclawDir + "/workspace-" + agentId
        target_dir = os.path.join(base_path, new_workspace)

        # 列出源文件
        prefix = f"{FORK_CDN_PREFIX}/{src_clawname}/{src_workspace}/{src_version}/"
        print(f"[Fork] 列出文件: {prefix}")
        files = _list_files_from_qiniu(prefix)

        if not files:
            _fork_jobs[job_id] = {"status": "failed", "error": "CDN 上未找到任何文件"}
            return

        print(f"[Fork] 找到 {len(files)} 个文件，开始下载...")
        success = 0
        for item in files:
            key = item['key']
            # 去掉前缀，得到相对路径（CDN key 是 URL 编码过的，decode 成实际文件名）
            rel_path = unquote(key[len(prefix):])
            if not rel_path:
                continue
            local_path = os.path.join(target_dir, rel_path)
            if _download_from_cdn(key, local_path):
                success += 1
                print(f"  ✓ {rel_path}")

        print(f"[Fork] 完成: 成功 {success}/{len(files)} 个文件")

        # 更新入口文件 mtime，使其在列表中排最前
        # entry_rel_path 可能是 URL 编码（含中文），需 decode 为本地文件名
        if entry_rel_path:
            entry_local = os.path.join(target_dir, unquote(entry_rel_path))
            if os.path.exists(entry_local):
                os.utime(entry_local, None)
                print(f"[Fork] 已更新入口文件 mtime: {entry_rel_path}")
            else:
                print(f"[Fork] 警告: 入口文件不存在，跳过 mtime 更新: {entry_rel_path}")

        # 文件到位后再发分析消息，确保 AI 能读到 fork 的内容
        fork_message = (
            "你好！请你仔细阅读我 workspace 下的所有文件，"
            "简单介绍一下这个项目有哪些资源、大概是做什么用的，"
            "几句话说清楚就好，不用太长。"
        )
        try:
            subprocess.Popen(
                ["openclaw", "agent", "--agent", agent_name, "--message", fork_message, "--json"],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL
            )
            print(f"[Fork] 已发送分析消息 → {agent_name}")
        except Exception as e:
            print(f"[Fork] 发送消息失败（非致命）: {e}")

        _fork_jobs[job_id] = {
            "status": "done",
            "workspace": new_workspace,
            "files": success,
            "total": len(files)
        }

    except Exception as e:
        print(f"[Fork] 异常: {e}")
        _fork_jobs[job_id] = {"status": "failed", "error": str(e)}


def start_fork(remote_url):
    """
    解析 remote_url，生成 job_id，在后台线程执行 fork。
    URL 格式: https://cdn.yiranlaoshi.com/myclaw/show/{clawname}/{workspace}/{version}/...
    """
    parsed = urlparse(remote_url)
    # path: /myclaw/show/{clawname}/{workspace}/{version}/...
    parts = parsed.path.strip('/').split('/')
    # parts: ['myclaw', 'show', clawname, workspace, version, ...]
    if len(parts) < 5 or parts[0] != 'myclaw' or parts[1] != 'show':
        return None, "URL 格式不正确，期望 .../myclaw/show/{claw}/{workspace}/{version}/..."

    src_clawname = parts[2]
    src_workspace = parts[3]
    src_version = parts[4]
    # parts[5:] 是入口文件相对路径（URL 中 version 之后的部分）
    entry_rel_path = '/'.join(parts[5:]) if len(parts) > 5 else None

    if not src_workspace.startswith('workspace'):
        return None, f"无法识别 workspace: {src_workspace}"

    job_id = f"fork-{int(time.time() * 1000)}"
    _fork_jobs[job_id] = {"status": "running"}

    t = threading.Thread(
        target=_do_fork,
        args=(job_id, src_clawname, src_workspace, src_version, entry_rel_path),
        daemon=True
    )
    t.start()

    return job_id, None


# ═══════════════════════════════════════════════════════════════
# 内嵌 HTTP API 服务
# ═══════════════════════════════════════════════════════════════

API_PORT = int(os.environ.get("MYCLAW_API_PORT", "8080"))


class MyclawAPIHandler(BaseHTTPRequestHandler):
    """轻量 HTTP API，供前端获取作品列表和配置"""
    # 使用默认 HTTP/1.0 — 天然 connection close，无 keep-alive 阻塞问题

    def _cors_headers(self):
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        self.send_header('Access-Control-Allow-Private-Network', 'true')
        self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')

    def _send_json(self, data, status=200):
        """统一 JSON 响应：自动设 Content-Type / Content-Length / CORS"""
        body = json.dumps(data, ensure_ascii=False).encode('utf-8')
        self.send_response(status)
        self._cors_headers()
        self.send_header('Content-Type', 'application/json; charset=utf-8')
        self.send_header('Content-Length', str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _send_json_raw(self, raw_bytes, status=200):
        """发送已编码的 JSON 字节（跳过二次序列化）"""
        self.send_response(status)
        self._cors_headers()
        self.send_header('Content-Type', 'application/json; charset=utf-8')
        self.send_header('Content-Length', str(len(raw_bytes)))
        self.end_headers()
        self.wfile.write(raw_bytes)

    def do_OPTIONS(self):
        self.send_response(200)
        self._cors_headers()
        self.send_header('Content-Length', '0')
        self.end_headers()

    def _parse_path(self):
        """解析并标准化请求路径，兼容 nginx /sync 前缀反代"""
        parsed = urlparse(self.path)
        path = parsed.path.rstrip('/')
        # nginx: location /sync → proxy_pass http://127.0.0.1:8080/sync
        # 请求路径会带 /sync 前缀，剥掉后统一路由
        if path.startswith('/sync'):
            path = path[len('/sync'):]
        if not path:
            path = '/'
        params = parse_qs(parsed.query)
        return path, params

    def do_POST(self):
        path, params = self._parse_path()

        if path == '/api/resync':
            return self._handle_resync(params)
        elif path == '/api/fork':
            return self._handle_fork()
        else:
            self._send_json({"error": "not found"}, 404)

    def do_GET(self):
        path, params = self._parse_path()

        if path == '/api/config':
            return self._handle_config()
        elif path == '/api/artifacts':
            return self._handle_artifacts(params)
        elif path == '/api/fork/status':
            return self._handle_fork_status(params)
        else:
            self._send_json({"error": "not found"}, 404)

    def _handle_config(self):
        """GET /api/config → 返回 claw 名称和 CDN base_url"""
        self._send_json({"claw": claw, "base_url": BASE_URL})

    def _handle_artifacts(self, params):
        """GET /api/artifacts?workspace=xxx → 直接从磁盘读取 __MY_ARTIFACTS__.json"""
        workspace = params.get('workspace', [''])[0]
        if not workspace:
            self._send_json({"error": "missing workspace param"}, 400)
            return

        base_path = get_openclaw_path()
        json_path = os.path.join(base_path, workspace, '.myclaw', ARTIFACTS_FILENAME)

        if not os.path.exists(json_path):
            self._send_json(create_empty_artifacts(workspace, BASE_URL))
            return

        try:
            with open(json_path, 'r', encoding='utf-8') as f:
                content = f.read()
            self._send_json_raw(content.encode('utf-8'))
        except Exception as e:
            self._send_json({"error": str(e)}, 500)

    def _handle_resync(self, params):
        """POST /api/resync?workspace=xxx → 触发全量同步"""
        workspace = params.get('workspace', [''])[0]
        if not workspace:
            self._send_json({"error": "missing workspace param"}, 400)
            return

        print(f"[API] 收到 resync 请求: {workspace}")
        try:
            result = sync_all(workspace)
            self._send_json({"ok": result, "workspace": workspace})
        except Exception as e:
            self._send_json({"ok": False, "error": str(e)}, 500)

    def _handle_fork(self):
        """POST /api/fork  Body: {"url": "https://cdn.yiranlaoshi.com/myclaw/show/..."}"""
        try:
            length = int(self.headers.get('Content-Length', 0))
            body = json.loads(self.rfile.read(length).decode('utf-8')) if length else {}
            remote_url = body.get('url', '')
        except Exception:
            self._send_json({"error": "请求体解析失败"}, 400)
            return

        if not remote_url:
            self._send_json({"error": "缺少 url 参数"}, 400)
            return

        job_id, err = start_fork(remote_url)
        if err:
            self._send_json({"error": err}, 400)
            return

        self._send_json({"job_id": job_id})

    def _handle_fork_status(self, params):
        """GET /api/fork/status?job_id=xxx"""
        job_id = params.get('job_id', [''])[0]
        if not job_id or job_id not in _fork_jobs:
            self._send_json({"error": "job 不存在"}, 404)
            return
        self._send_json(_fork_jobs[job_id])

    def log_message(self, format, *args):
        # 静默日志，避免轮询刷屏
        pass


def cleanup_ignored_assets(workspace_id):
    """启动时清理 __MY_ARTIFACTS__.json 中属于忽略目录的旧记录"""
    base_path = get_openclaw_path()
    file = f"{base_path}/{workspace_id}/.myclaw/{ARTIFACTS_FILENAME}"
    if not os.path.exists(file):
        return
    try:
        with open(file, "r", encoding="utf-8") as f:
            data = json.load(f)
    except Exception:
        return

    before = len(data.get("assets", []))
    data["assets"] = [
        asset for asset in data.get("assets", [])
        if not _path_has_ignored_dir(asset.get("path", ""))
        and os.path.basename(asset.get("path", "")) not in IGNORE_FILE_PATTERNS
    ]
    after = len(data["assets"])

    if before != after:
        data["updated_at"] = now_iso()
        with open(file, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"[cleanup] 清理忽略文件记录: {before - after} 条（{workspace_id}）")
    else:
        print(f"[cleanup] 无需清理（{workspace_id}）")


def start_api_server():
    """在后台线程启动 HTTP API 服务（多线程，避免 resync 阻塞轮询）"""
    class ThreadedHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
        allow_reuse_address = True
        daemon_threads = True

    server = ThreadedHTTPServer(('0.0.0.0', API_PORT), MyclawAPIHandler)
    thread = threading.Thread(target=server.serve_forever, daemon=True)
    thread.start()
    print(f"[myclaw-api] ✅ HTTP API 服务已启动: http://127.0.0.1:{API_PORT}")
    print(f"[myclaw-api]   GET /api/config     → claw 配置")
    print(f"[myclaw-api]   GET /api/artifacts   → 作品列表")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="文件同步服务")
    parser.add_argument("--agent", help="启动前先全量同步指定 workspace")
    parser.add_argument("--port", type=int, default=API_PORT, help="API 服务端口 (默认 18800)")
    args = parser.parse_args()

    API_PORT = args.port

    base_path = get_openclaw_path()
    path = base_path

    # 启动时清理所有 workspace 中的忽略文件记录
    for entry in os.scandir(base_path):
        if entry.is_dir() and "workspace" in entry.name:
            cleanup_ignored_assets(entry.name)

    # 如果指定了 --agent，先全量同步
    if args.agent:
        if not sync_all(args.agent):
            sys.exit(1)
        print("")

    # 启动 HTTP API 服务
    start_api_server()

    event_handler = MyHandler()
    observer = Observer()
    observer.schedule(event_handler, path, recursive=True)

    observer.start()
    print(f"开始监听目录: {path}")

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()

    observer.join()