#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
销售工作台 · 本地数据服务（仅依赖 Python 标准库）

用法：
  1. 把本文件放在同时包含「工具」和「数据」两个文件夹的目录里
  2. python3 销售工作台服务.py          （默认端口 8000）
     python3 销售工作台服务.py 8080     （指定端口）

功能：
  - 静态文件服务（替代原先的静态服务器，原有页面/JSON 路径不变）
  - GET  /api/todos   读取共享待办（存于 数据/todos.json）
  - POST /api/todos   写入共享待办，action 支持：
        save    {user, todos}   整体覆盖某用户的待办列表
        close   {item}          追加一条团队已关闭记录（按 id 去重，最多保留 200 条）
        unclose {id}            按 id 移除已关闭记录（重新打开/删除时调用）
  - 所有 .json 响应自动附加 Cache-Control: no-store，避免浏览器缓存旧数据
"""
import json
import os
import sys
import threading
import time
from datetime import datetime
import urllib.request
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # 脚本/ → 父目录
DATA_DIR = os.path.join(ROOT, '数据')
TODOS_FILE = os.path.join(DATA_DIR, 'todos.json')
FEISHU_DATA_FILE = os.path.join(DATA_DIR, 'customer_feishu_data.json')
LOCK = threading.Lock()
MAX_CLOSED = 200

# MCP 配置
MCP_TOKEN = 'm-abfb29e8-3104-434f-9944-8d0bb592f8cd'
MCP_API = 'https://project.feishu.cn/mcp_server/v1'
MCP_PK = '6593cd71471290e3cc6be6e6'
ACTIVITY_TYPE_KEY = '65ae1e5d44338dbe7c39a29a'

# MCP 数据缓存（TTL 秒）
MCP_CACHE = {}
MCP_CACHE_TTL = {
    'customers': 1800,   # 30分钟
    'orders': 300,       # 5分钟
    'complaints': 900,   # 15分钟
    'activities': 300,   # 5分钟
    'opportunities': 300 # 5分钟
}
MCP_CACHE_LOCK = threading.Lock()

def mcp_pull_all(mql_query, max_pages=50):
    """拉取全量 MCP 数据，自动翻页，展开 field_list 为扁平 JSON"""
    all_items = []
    session_id = None
    for page in range(1, max_pages + 1):
        args = {'project_key': MCP_PK, 'mql': mql_query}
        if session_id:
            args['session_id'] = session_id
        args['group_pagination_list'] = [{'page_num': page, 'group_id': 'default'}]
        result = mcp_call('search_by_mql', args, timeout=60)
        if not result:
            break
        # 从 data dict 中提取 items
        data_dict = result.get('data', {})
        if not data_dict:
            break
        for gid, items in data_dict.items():
            for item in items:
                flat = {}
                for f in (item.get('moql_field_list') or item.get('field_list') or []):
                    k = f.get('key', '')
                    v = f.get('value', None)
                    # 展开 key_label_value 格式
                    if isinstance(v, list):
                        # 数组类型（multi-select, multi-user, workitem_related_multi_select）
                        flat[k] = []
                        for elem in v:
                            if isinstance(elem, dict) and 'key_label_value' in elem:
                                flat[k].append({'key': elem['key_label_value'].get('key',''), 'label': elem['key_label_value'].get('label','')})
                            elif isinstance(elem, dict) and len(elem) == 1:
                                flat[k].append(list(elem.values())[0])
                            else:
                                flat[k].append(elem)
                    elif isinstance(v, dict) and 'key_label_value' in v:
                        flat[k] = v['key_label_value'].get('label', v['key_label_value'].get('key',''))
                    elif isinstance(v, dict) and len(v) == 1:
                        flat[k] = list(v.values())[0]
                    else:
                        flat[k] = v
                all_items.append(flat)
            if len(items) < 50:
                return all_items  # 最后一页
        if not session_id:
            session_id = result.get('session_id', '')
        # 检查总数
        group_infos = result.get('list', [])
        for gi in group_infos:
            total = gi.get('count', 0)
            if len(all_items) >= total:
                return all_items
    return all_items

def get_mcp_cache(cache_key):
    """获取缓存，过期返回 None"""
    with MCP_CACHE_LOCK:
        entry = MCP_CACHE.get(cache_key)
        if entry and time.time() - entry['time'] < MCP_CACHE_TTL.get(cache_key, 300):
            return entry['data']
    return None

def set_mcp_cache(cache_key, data):
    """写入缓存 + 写入 JSON 文件作为降级备份"""
    with MCP_CACHE_LOCK:
        MCP_CACHE[cache_key] = {'time': time.time(), 'data': data}
    # 异步写 JSON 备份
    backup_path = os.path.join(DATA_DIR, f'mcp_{cache_key}.json')
    try:
        with open(backup_path + '.tmp', 'w', encoding='utf-8') as f:
            json.dump({'updated_at': time.strftime('%Y-%m-%d %H:%M:%S'), 'customers' if cache_key == 'customers' else 'items': data}, f, ensure_ascii=False)
        os.replace(backup_path + '.tmp', backup_path)
    except Exception:
        pass

def refresh_mcp_cache(cache_key, mql_query):
    """强制刷新缓存"""
    try:
        data = mcp_pull_all(mql_query)
        sys.stderr.write(f'[MCP] refresh {cache_key}: got {len(data)} items\n')
        sys.stderr.flush()
        set_mcp_cache(cache_key, data)
        return len(data)
    except Exception as e:
        sys.stderr.write(f'[MCP] refresh {cache_key} ERROR: {e}\n')
        sys.stderr.flush()
        # 尝试从备份文件恢复
        backup_path = os.path.join(DATA_DIR, f'mcp_{cache_key}.json')
        if os.path.exists(backup_path):
            try:
                with open(backup_path, encoding='utf-8') as f:
                    old = json.load(f)
                return len(old.get('data', []))
            except Exception:
                pass
        raise e

def mcp_call(method, args, timeout=20):
    """调用飞书 MCP API"""
    req = urllib.request.Request(
        MCP_API,
        data=json.dumps({
            'jsonrpc': '2.0', 'id': 1,
            'method': 'tools/call',
            'params': {'name': method, 'arguments': args}
        }).encode('utf-8'),
        headers={'X-Mcp-Token': MCP_TOKEN, 'Content-Type': 'application/json'},
        method='POST'
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as r:
            d = json.loads(r.read())
            if 'error' in d:
                return {'ok': False, 'error': d['error']}
            for c in d['result']['content']:
                t = c.get('text', '')
                if 'log_id' in t: continue
                return json.loads(t)
    except Exception as e:
        return {'ok': False, 'error': str(e)}
    return {'ok': False, 'error': 'no content'}

# IMA 凭证
IMA_CLIENT_ID = os.environ.get('IMA_OPENAPI_CLIENTID') or open(os.path.expanduser('~/.config/ima/client_id')).read().strip()
IMA_API_KEY = os.environ.get('IMA_OPENAPI_APIKEY') or open(os.path.expanduser('~/.config/ima/api_key')).read().strip()
IMA_KB_DIAG = 'fwEO41E0Ef0CfulmtQHYgYQy_jBsbl6tXE5x6VYLIoA='  # 宝锐诊断原料销售

def ima_api(path, body):
    """调用 IMA OpenAPI"""
    req = urllib.request.Request(
        f'https://ima.qq.com/{path}',
        data=json.dumps(body).encode('utf-8'),
        headers={
            'ima-openapi-clientid': IMA_CLIENT_ID,
            'ima-openapi-apikey': IMA_API_KEY,
            'Content-Type': 'application/json'
        },
        method='POST'
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode('utf-8'))


def read_store():
    if not os.path.exists(TODOS_FILE):
        return {'users': {}, 'closed': [], 'updated_at': None}
    try:
        with open(TODOS_FILE, encoding='utf-8') as f:
            d = json.load(f)
        d.setdefault('users', {})
        d.setdefault('closed', [])
        return d
    except Exception:
        return {'users': {}, 'closed': [], 'updated_at': None}


def write_store(d):
    os.makedirs(DATA_DIR, exist_ok=True)
    d['updated_at'] = time.strftime('%Y-%m-%d %H:%M:%S')
    tmp = TODOS_FILE + '.tmp'
    with open(tmp, 'w', encoding='utf-8') as f:
        json.dump(d, f, ensure_ascii=False, indent=2)
    os.replace(tmp, TODOS_FILE)


# 默认用户表（邮箱前缀 → 完整用户信息）
DEFAULT_USERS = {
    'admin': {'name': '超级管理员', 'role': 'admin', 'dept': '诊断原料销售部'},
    'liuxinyuan@biori.com': {'name': '刘新元', 'role': 'admin', 'dept': '诊断原料销售部'},
    'liuxin@biori.com': {'name': '刘欣', 'role': 'admin', 'dept': '诊断原料销售拓展部'},
    'zhangliya@biori.com': {'name': '张立娅', 'role': 'manager', 'dept': '诊断原料大客户销售部'},
    'zhaoyunhao@biori.com': {'name': '赵云浩', 'role': 'sales', 'dept': '诊断原料大客户销售部'},
    'wuyun@biori.com': {'name': '吴云', 'role': 'manager', 'dept': '生命科学-浙江'},
    'hanyuanhuai@biori.com': {'name': '韩远怀', 'role': 'manager', 'dept': '生命科学-广东'},
}

class Handler(SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=ROOT, **kwargs)

    def _find_user(self, email, password):
        """验证用户：先查默认表，密码用 biori2026"""
        email = email.strip().lower()
        user = DEFAULT_USERS.get(email)
        if not user:
            return None
        valid = password == 'biori2026'
        if not valid:
            return None
        return dict(user)

    def log_message(self, fmt, *args):
        sys.stderr.write('%s - %s\n' % (self.address_string(), fmt % args))

    def end_headers(self):
        path = self.path.split('?')[0]
        if path.endswith('.json') or path.startswith('/api/') or path.endswith('.html'):
            self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
        super().end_headers()

    def _send_json(self, obj, code=200):
        body = json.dumps(obj, ensure_ascii=False).encode('utf-8')
        self.send_response(code)
        self.send_header('Content-Type', 'application/json; charset=utf-8')
        self.send_header('Content-Length', str(len(body)))
        self.send_header('Access-Control-Allow-Origin', '*')
        self.end_headers()
        self.wfile.write(body)

    def do_OPTIONS(self):
        """处理 CORS 预检请求"""
        self.send_response(204)
        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.end_headers()

    def do_GET(self):
        path = self.path.split('?')[0]
        if path == '/api/feishu/customer_data':
            # 读取飞书缓存数据
            qs = {}
            if '?' in self.path:
                from urllib.parse import parse_qs
                qs = {k: v[0] for k, v in parse_qs(self.path.split('?')[1]).items()}
            cid = qs.get('customer_id', '')
            try:
                with open(FEISHU_DATA_FILE, encoding='utf-8') as f:
                    all_data = json.load(f)
                customer_data = all_data.get(str(cid))
                if customer_data:
                    self._send_json({'ok': True, 'data': customer_data})
                else:
                    self._send_json({'ok': False, 'error': 'no data for this customer, please ask Hermes to generate it'}, 404)
            except FileNotFoundError:
                self._send_json({'ok': False, 'error': 'no cache file yet'}, 404)
            except Exception as e:
                self._send_json({'ok': False, 'error': str(e)}, 500)
        elif path == '/api/todos':
            with LOCK:
                self._send_json(read_store())
        elif path.startswith('/api/mcp/'):
            # ── MCP 实时数据 API ──
            from urllib.parse import parse_qs
            qs = {k: v[0] for k, v in parse_qs(self.path.split('?')[1]).items()} if '?' in self.path else {}
            force = qs.get('refresh') == '1'
            
            MQL_MAP = {
                'customers':     'SELECT `name`,`work_item_id`,`field_17186c`,`field_6415cf`,`field_5ed7ab`,`field_2d5b6a`,`field_e62869`,`field_c3224b`,`field_c8e80d`,`field_a1ff52`,`field_79aa0b`,`owner`,`start_time`,`business`,`current_status_operator` FROM `6593cd71471290e3cc6be6e6`.`65ae1e403c87b152f3365ca6`',
                'orders':        'SELECT `name`,`work_item_id`,`field_e1001d`,`field_a6e501`,`field_5ed7ab`,`field_2d6367`,`field_e0cce5`,`field_2e095b`,`field_4a1b47`,`field_29d947`,`field_3a3ccb`,`field_8c4b7b`,`field_5b2b3b`,`field_dbd4ab`,`field_a98458`,`field_8d9153`,`owner`,`start_time`,`business`,`current_status_operator`,`work_item_status` FROM `6593cd71471290e3cc6be6e6`.`662612aea6bb7089fea044ef`',
                'complaints':    'SELECT `name`,`work_item_id`,`start_time`,`work_item_status`,`field_9e2144`,`field_6fb810`,`field_3ce9fe`,`field_d107b0`,`field_502a85`,`field_ac5caf`,`owner`,`business`,`current_status_operator`,`finish_time`,`finish_status` FROM `658288abfb8bd616b17025f1`.`6669433056a98249604376de`',
                'activities':    'SELECT `name`,`work_item_id`,`field_b54da4`,`field_b99055`,`field_76654e`,`field_a2b3f6`,`field_5f20fc`,`field_5ed7ab`,`field_d6ee93`,`owner`,`start_time`,`business`,`current_status_operator`,`work_item_status` FROM `6593cd71471290e3cc6be6e6`.`65ae1e5d44338dbe7c39a29a`',
                'opportunities': 'SELECT `name`,`work_item_id`,`owner`,`start_time`,`business`,`current_status_operator`,`work_item_status`,`description` FROM `6593cd71471290e3cc6be6e6`.`story`',
            }
            
            cache_key = path.replace('/api/mcp/', '')
            if cache_key == 'dashboard':
                self._send_json({'ok': False, 'error': 'use POST /api/mcp/refresh or GET with ?refresh=1 to compute'}, 400)
                return
            
            if cache_key in MQL_MAP:
                try:
                    if force:
                        count = refresh_mcp_cache(cache_key, MQL_MAP[cache_key])
                        data = get_mcp_cache(cache_key)
                    else:
                        data = get_mcp_cache(cache_key)
                        if data is None:
                            count = refresh_mcp_cache(cache_key, MQL_MAP[cache_key])
                            data = get_mcp_cache(cache_key)
                    self._send_json({'ok': True, 'type': cache_key, 'total': len(data), 'items': data})
                except Exception as e:
                    self._send_json({'ok': False, 'error': str(e), 'type': cache_key}, 500)
            else:
                self._send_json({'ok': False, 'error': f'unknown type: {cache_key}'}, 400)
        else:
            super().do_GET()

    def do_POST(self):
        path = self.path.split('?')[0]

        # ── 登录 API（无需 CloudBase 前端 SDK）──
        if path == '/api/login':
            try:
                length = int(self.headers.get('Content-Length') or 0)
                payload = json.loads(self.rfile.read(length) or b'{}')
            except Exception:
                self._send_json({'ok': False, 'error': '请求格式错误'}, 400)
                return
            email = (payload.get('email') or '').strip()
            pwd = payload.get('password', '')
            if not email:
                self._send_json({'ok': False, 'error': '请输入邮箱'}, 400)
                return
            user = self._find_user(email, pwd)
            if not user:
                self._send_json({'ok': False, 'error': '邮箱未注册或密码错误'}, 401)
                return
            self._send_json({
                'ok': True,
                'user': {
                    'name': user.get('name', email.split('@')[0]),
                    'role': user.get('role', 'sales'),
                    'dept': user.get('dept', ''),
                    'email': email,
                    'tabs': user.get('tabs', {})
                }
            })
            return

        # ── IMA 知识库 API ──
        if path == '/api/ima':
            try:
                length = int(self.headers.get('Content-Length') or 0)
                payload = json.loads(self.rfile.read(length) or b'{}')
            except Exception:
                self._send_json({'ok': False, 'error': 'bad json'}, 400)
                return
            
            action = payload.get('action', '')
            try:
                if action == 'search':
                    # 搜索知识库
                    query = payload.get('query', '')
                    kb_id = payload.get('kb_id', IMA_KB_DIAG)
                    result = ima_api('openapi/wiki/v1/search_knowledge', {
                        'knowledge_base_id': kb_id,
                        'query': query,
                        'cursor': ''
                    })
                    self._send_json({'ok': True, 'data': result})
                    
                elif action == 'create_note':
                    # 创建笔记并添加到知识库
                    title = payload.get('title', '未命名经验')
                    content = payload.get('content', '')
                    
                    # Step 1: 创建笔记
                    note = ima_api('openapi/v1/note/import_doc', {
                        'title': title,
                        'content': content,
                        'content_format': 1  # markdown
                    })
                    
                    doc_id = note.get('data',{}).get('doc_id') or note.get('doc_id')
                    if not doc_id:
                        self._send_json({'ok': False, 'error': '创建笔记失败', 'detail': note}, 500)
                        return
                    
                    # Step 2: 添加到知识库
                    kb = ima_api('openapi/wiki/v1/add_knowledge', {
                        'knowledge_base_id': IMA_KB_DIAG,
                        'media_type': 11,  # 笔记
                        'note_info': {'content_id': doc_id},
                        'title': title
                    })
                    
                    self._send_json({'ok': True, 'doc_id': doc_id, 'note': note, 'kb': kb})
                    
                else:
                    self._send_json({'ok': False, 'error': f'unknown action: {action}'}, 400)
                    
            except Exception as e:
                self._send_json({'ok': False, 'error': str(e)}, 500)
            return
        
        # ── 飞书免登 API ──
        if path == '/api/feishu/auth':
            try:
                length = int(self.headers.get('Content-Length') or 0)
                payload = json.loads(self.rfile.read(length) or b'{}')
            except Exception:
                self._send_json({'ok': False, 'error': 'bad json'}, 400)
                return

            code = payload.get('code', '')
            if not code:
                self._send_json({'ok': False, 'error': 'missing code'}, 400)
                return

            # 用 app_access_token 换用户身份
            try:
                # Step 1: 获取 app_access_token
                req = urllib.request.Request(
                    'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal',
                    data=json.dumps({'app_id': 'cli_aaee6a2f90b89bc1', 'app_secret': 'arht5fd8wyppZysFvOjUcb2bV0UMVVYF'}).encode(),
                    headers={'Content-Type': 'application/json'}, method='POST'
                )
                with urllib.request.urlopen(req, timeout=10) as r:
                    token_res = json.loads(r.read())
                access_token = token_res.get('app_access_token', '')

                if not access_token:
                    self._send_json({'ok': False, 'error': 'no access token'}, 500)
                    return

                # Step 2: 用 code 换用户身份
                req2 = urllib.request.Request(
                    'https://open.feishu.cn/open-apis/authen/v1/oidc/access_token',
                    data=json.dumps({'grant_type': 'authorization_code', 'code': code}).encode(),
                    headers={'Content-Type': 'application/json', 'Authorization': f'Bearer {access_token}'},
                    method='POST'
                )
                with urllib.request.urlopen(req2, timeout=10) as r2:
                    auth_res = json.loads(r2.read())

                if auth_res.get('code') != 0:
                    self._send_json({'ok': False, 'error': 'auth failed', 'detail': str(auth_res)[:200]}, 500)
                    return

                name = (auth_res.get('data') or {}).get('name', '')
                self._send_json({'ok': True, 'name': name})

            except Exception as e:
                self._send_json({'ok': False, 'error': str(e)}, 500)
            return

        # ── 活动创建 API ──
        if path == '/api/create_activity':
            try:
                length = int(self.headers.get('Content-Length') or 0)
                payload = json.loads(self.rfile.read(length) or b'{}')
            except Exception:
                self._send_json({'ok': False, 'error': 'bad json'}, 400)
                return

            name = payload.get('name', '拜访纪要')
            customer_id = payload.get('customer_id')
            activity_type = payload.get('activity_type', '线下拜访')
            description = payload.get('description', '')
            start_date = payload.get('start_date', time.strftime('%Y-%m-%d'))
            department = payload.get('department')

            # 活动类型映射
            type_map = {'线下拜访': 'v4zlj721e', '线上跟进': '5vezmmm1y', '陌拜': 'wg15dd75b', '协同拜访': 'wsxnxsd60'}

            fields = [
                {'field_key': 'name', 'field_value': name},
                {'field_key': 'field_76654e', 'field_value': type_map.get(activity_type, 'v4zlj721e')},
                {'field_key': 'field_a2b3f6', 'field_value': int(time.mktime(time.strptime(start_date, '%Y-%m-%d'))) * 1000 if start_date else int(time.time() * 1000)},
            ]
            if description:
                fields.append({'field_key': 'field_b99055', 'field_value': description})
            if customer_id:
                fields.append({'field_key': 'field_5f20fc', 'field_value': str(customer_id)})
            if department:
                fields.append({'field_key': 'field_5ed7ab', 'field_value': department})

            result = mcp_call('create_workitem', {
                'project_key': MCP_PK,
                'work_item_type': ACTIVITY_TYPE_KEY,
                'fields': fields,
                'template_id': '700373'
            })

            if result.get('ok') == False:
                self._send_json({'ok': False, 'error': result.get('error', 'MCP call failed')}, 500)
                return

            # 提取工作项 ID
            wid = result.get('id') or result.get('work_item_id') or result.get('data', {}).get('id')
            feishu_url = f'https://project.feishu.cn/baoyu/workitem/{wid}' if wid else ''
            self._send_json({'ok': True, 'work_item_id': wid, 'feishu_url': feishu_url, 'mcp_result': result})
            return

        # ── MCP 数据刷新 API ──
        if path == '/api/mcp/refresh':
            try:
                length = int(self.headers.get('Content-Length') or 0)
                payload = json.loads(self.rfile.read(length) or b'{}')
            except Exception:
                self._send_json({'ok': False, 'error': 'bad json'}, 400)
                return

            rtype = payload.get('type', 'all')
            MQL_MAP = {
                'customers':     'SELECT `name`,`work_item_id`,`field_17186c`,`field_6415cf`,`field_5ed7ab`,`field_2d5b6a`,`field_e62869`,`field_c3224b`,`field_c8e80d`,`field_a1ff52`,`field_79aa0b`,`owner`,`start_time`,`business`,`current_status_operator` FROM `6593cd71471290e3cc6be6e6`.`65ae1e403c87b152f3365ca6`',
                'orders':        'SELECT `name`,`work_item_id`,`field_e1001d`,`field_a6e501`,`field_5ed7ab`,`field_2d6367`,`field_e0cce5`,`field_2e095b`,`field_4a1b47`,`field_29d947`,`field_3a3ccb`,`field_8c4b7b`,`field_5b2b3b`,`field_dbd4ab`,`field_a98458`,`field_8d9153`,`owner`,`start_time`,`business`,`current_status_operator`,`work_item_status` FROM `6593cd71471290e3cc6be6e6`.`662612aea6bb7089fea044ef`',
                'complaints':    'SELECT `name`,`work_item_id`,`start_time`,`work_item_status`,`field_9e2144`,`field_6fb810`,`field_3ce9fe`,`field_d107b0`,`field_502a85`,`field_ac5caf`,`owner`,`business`,`current_status_operator`,`finish_time`,`finish_status` FROM `658288abfb8bd616b17025f1`.`6669433056a98249604376de`',
                'activities':    'SELECT `name`,`work_item_id`,`field_b54da4`,`field_b99055`,`field_76654e`,`field_a2b3f6`,`field_5f20fc`,`field_5ed7ab`,`field_d6ee93`,`owner`,`start_time`,`business`,`current_status_operator`,`work_item_status` FROM `6593cd71471290e3cc6be6e6`.`65ae1e5d44338dbe7c39a29a`',
                'opportunities': 'SELECT `name`,`work_item_id`,`owner`,`start_time`,`business`,`current_status_operator`,`work_item_status`,`description` FROM `6593cd71471290e3cc6be6e6`.`story`',
            }
            
            if rtype == 'all':
                results = {}
                for key, mql in MQL_MAP.items():
                    try:
                        results[key] = refresh_mcp_cache(key, mql)
                    except Exception as e:
                        results[key] = f'error: {e}'
                self._send_json({'ok': True, 'results': results})
            elif rtype in MQL_MAP:
                try:
                    count = refresh_mcp_cache(rtype, MQL_MAP[rtype])
                    self._send_json({'ok': True, 'type': rtype, 'count': count})
                except Exception as e:
                    self._send_json({'ok': False, 'error': str(e)}, 500)
            else:
                self._send_json({'ok': False, 'error': f'unknown type: {rtype}, valid: all, {", ".join(MQL_MAP.keys())}'}, 400)
            return

        # ── 拜访卡生成（精简版：写请求队列）──
        if path == '/api/visit-card':
            try:
                length = int(self.headers.get('Content-Length') or 0)
                payload = json.loads(self.rfile.read(length) or b'{}')
            except Exception:
                self._send_json({'ok': False, 'error': 'bad json'}, 400)
                return
            customer = (payload.get('customer') or '').strip()
            if not customer:
                self._send_json({'ok': False, 'error': 'customer required'}, 400)
                return
            
            # 保存请求队列
            try:
                queue_dir = os.path.join(ROOT, '数据', 'visit_requests')
                os.makedirs(queue_dir, exist_ok=True)
                ts = datetime.now().strftime('%Y%m%d_%H%M%S')
                cn = customer[:10].replace('/', '_')
                queue_file = os.path.join(queue_dir, f'{ts}_{cn}.json')
                with open(queue_file, 'w') as qf:
                    json.dump({
                        'customer': customer,
                        'salesperson': payload.get('salesperson', ''),
                        'dept': payload.get('dept', ''),
                        'visitDate': payload.get('visitDate', ''),
                        'note': payload.get('note', ''),
                        'requirements': payload.get('requirements', ''),
                        'types': payload.get('types', []),
                        'requested_at': datetime.now().isoformat()
                    }, qf, ensure_ascii=False)
            except Exception as e:
                print(f'[visit-card] Queue error: {e}')
            
            self._send_json({
                'ok': True,
                'queued': True,
                'customer': customer,
                'generated_at': datetime.now().isoformat()
            })
            return

        # ── 原有 todos API ──
        # ── 原有 todos API ──
        if path != '/api/todos':
            self._send_json({'ok': False, 'error': 'not found'}, 404)
            return
        try:
            length = int(self.headers.get('Content-Length') or 0)
            payload = json.loads(self.rfile.read(length) or b'{}')
        except Exception:
            self._send_json({'ok': False, 'error': 'bad json'}, 400)
            return

        action = payload.get('action')
        with LOCK:
            store = read_store()
            if action == 'save':
                user = str(payload.get('user') or '')
                todos = payload.get('todos')
                if not user or not isinstance(todos, list):
                    self._send_json({'ok': False, 'error': 'bad payload'}, 400)
                    return
                store['users'][user] = todos
            elif action == 'close':
                item = payload.get('item')
                if not isinstance(item, dict):
                    self._send_json({'ok': False, 'error': 'bad payload'}, 400)
                    return
                store['closed'] = [x for x in store['closed'] if x.get('id') != item.get('id')]
                store['closed'].insert(0, item)
                store['closed'] = store['closed'][:MAX_CLOSED]
            elif action == 'unclose':
                tid = payload.get('id')
                store['closed'] = [x for x in store['closed'] if x.get('id') != tid]
            else:
                self._send_json({'ok': False, 'error': 'unknown action'}, 400)
                return
            write_store(store)
        self._send_json({'ok': True})


if __name__ == '__main__':
    port = int(sys.argv[1]) if len(sys.argv) > 1 else 8000
    print('销售工作台服务已启动：http://0.0.0.0:%d  （根目录：%s）' % (port, ROOT))
    print('按 Ctrl+C 停止')
    ThreadingHTTPServer(('0.0.0.0', port), Handler).serve_forever()
