#!/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 subprocess
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': '生命科学-广东'},
    'songxuewei@biori.com': {'name': '宋学伟', 'role': 'sales', 'dept': '诊断原料销售拓展部'},
}

# ============ 数据质检 ============
def run_quality_check(filepath, ext):
    """质检上传文件：检测类型、列名、数据完整性"""
    result = {'pass': False, 'type': 'unknown', 'columns': [], 'issues': [], 'summary': ''}

    if ext not in ('.xlsx', '.xls', '.csv'):
        result['summary'] = '不支持的文件格式，仅支持 .xlsx/.xls/.csv 数据文件'
        return result

    try:
        if ext == '.csv':
            import csv
            with open(filepath, 'r', encoding='utf-8-sig') as f:
                reader = csv.reader(f)
                headers = [h.strip() for h in next(reader)]
        else:
            import openpyxl
            wb = openpyxl.load_workbook(filepath, data_only=True)
            ws = wb.active
            first_row = next(ws.iter_rows(min_row=1, max_row=1, values_only=True))
            headers = [str(c).strip() if c else '' for c in first_row]
            wb.close()

        result['columns'] = headers
        has_order = any('单据编号' in h for h in headers)
        has_tax = any('价税合计' in h or '税合计' in h for h in headers)
        has_customer = any('购货单位' in h or '客户' in h for h in headers)
        has_activity = any('活动' in h for h in headers)
        has_creator = any('创建者' in h or '创建人' in h for h in headers)

        if has_order and has_tax:
            result['type'] = 'erp_order'
            result['summary'] = f'识别为 ERP 销售订单导出，{len(headers)} 列'
        elif has_activity or has_creator:
            result['type'] = 'feishu_activity'
            result['summary'] = f'识别为 飞书活动导出，{len(headers)} 列'
        else:
            result['summary'] = f'未识别，{len(headers)} 列'
            result['issues'].append('无法匹配处理管线')
            return result

        if result['type'] == 'erp_order':
            for c in ['单据编号', '价税合计']:
                if not any(c in h for h in headers):
                    result['issues'].append(f'缺少关键列: {c}')
        if not result['issues']:
            result['pass'] = True
            result['summary'] += '，质检通过'
    except Exception as e:
        result['issues'].append(str(e))
        result['summary'] = f'质检失败: {e}'
    return result


def process_uploaded_file(filepath, ftype, saved_name):
    """处理上传文件：智能识别月份 → 路由到正确数据文件 → 合并部署"""
    import re
    result = {'ok': False, 'summary': '', 'updated': []}
    try:
        ext = os.path.splitext(saved_name)[1].lower()
        if ext == '.csv':
            import csv
            with open(filepath, 'r', encoding='utf-8-sig') as f:
                reader = csv.DictReader(f)
                headers = reader.fieldnames
                rows_list = list(reader)
        else:
            import openpyxl
            wb = openpyxl.load_workbook(filepath, data_only=True)
            ws = wb.active
            headers = [str(c).strip() if c else '' for c in next(ws.iter_rows(min_row=1, max_row=1, values_only=True))]
            rows_list = []
            for row in ws.iter_rows(min_row=2, values_only=True):
                if not row: continue
                rows_list.append({headers[i]: row[i] for i in range(min(len(headers), len(row)))})
            wb.close()

        if ftype == 'erp_order':
            from collections import defaultdict, Counter
            cols = headers
            bn_col = amt_col = sp_col = None
            for c in cols:
                if '单据编号' in c: bn_col = c
                if '价税合计' in c or '税合计' in c: amt_col = c
                if '销售员' in c or '下单人' in c: sp_col = c

            # ── 智能月份检测 ──
            month_counter = Counter()
            for row in rows_list[:100]:
                bn = str(row.get(bn_col, '') or '').strip() if bn_col else ''
                if bn.startswith('ZHBR') and len(bn) >= 10:
                    ym = '20' + bn[4:6] + '-' + bn[6:8]
                    month_counter[ym] += 1
            m = re.search(r'(2026\d{2})', saved_name)
            file_ym = m.group(1)[:4] + '-' + m.group(1)[4:6] if m else None
            detected_month = month_counter.most_common(1)[0][0] if month_counter else (file_ym or datetime.now().strftime('%Y-%m'))
            current_month = datetime.now().strftime('%Y-%m')

            # ── 数据汇总 ──
            by_sp = defaultdict(float)
            total = rows = 0
            for row in rows_list:
                bn = str(row.get(bn_col, '') or '').strip() if bn_col else ''
                if not bn or bn == 'None' or '合计' in bn: continue
                amt = 0
                try: amt = float(row.get(amt_col, 0) or 0) if amt_col else 0
                except: continue
                total += amt
                sp = str(row.get(sp_col, '') or '').strip() if sp_col else ''
                if sp and amt > 0: by_sp[sp] += amt
                rows += 1

            now_str = datetime.now().strftime('%Y-%m-%d %H:%M')
            people = [{'name': k, 'amount': round(v,2)} for k, v in sorted(by_sp.items(), key=lambda x: -x[1])]

            # ── 按月份路由 ──
            if detected_month == current_month:
                kpi_data = {'month': detected_month, 'updated': now_str,
                            'source': f'上传: {saved_name}', 'total': round(total,2),
                            'orders': rows, 'people': [{'name': k, 'current': round(v,2), 'ytd': round(v,2)} for k,v in sorted(by_sp.items(), key=lambda x: -x[1])]}
                with open(os.path.join(DATA_DIR, 'kpi_progress.json'), 'w', encoding='utf-8') as f:
                    json.dump(kpi_data, f, ensure_ascii=False, indent=2)
                result['updated'].append('kpi_progress.json')
                result['summary'] = f'{detected_month} KPI: {rows}条, {total/1e4:.1f}万, {len(people)}人'
            else:
                # 历史月 → three_year_monthly + kpi_dashboard
                month_num = int(detected_month.split('-')[1])
                ty_path = os.path.join(DATA_DIR, 'three_year_monthly.json')
                with open(ty_path, 'r', encoding='utf-8') as f:
                    ty = json.load(f)
                ty['2026'][str(month_num)] = round(total, 2)
                with open(ty_path, 'w', encoding='utf-8') as f:
                    json.dump(ty, f, ensure_ascii=False, indent=2)
                result['updated'].append('three_year_monthly.json')

                kd_path = os.path.join(DATA_DIR, 'kpi_dashboard.json')
                with open(kd_path, 'r', encoding='utf-8') as f:
                    kd = json.load(f)
                kd['total_h2_actual'] = round(kd.get('total_h2_actual', 0) + total, 2)
                kd['total_h1_h2'] = round(kd.get('total_h1', 0) + kd['total_h2_actual'], 2)
                kd['updated_at'] = now_str
                with open(kd_path, 'w', encoding='utf-8') as f:
                    json.dump(kd, f, ensure_ascii=False, indent=2)
                result['updated'].append('kpi_dashboard.json')
                result['summary'] = f'{detected_month} 校准: {rows}条, {total/1e4:.1f}万, {len(people)}人'

            subprocess.run(['python3', os.path.join(ROOT, '脚本', 'merge_workbench_data.py')], capture_output=True, timeout=30)
            result['updated'].append('workbench_data.json → CloudBase')
            subprocess.run(['bash', os.path.join(ROOT, '脚本', 'cb_sync.sh')], capture_output=True, timeout=60)
            result['ok'] = True
        else:
            result['summary'] = f'类型 {ftype} 暂不支持自动处理'
    except Exception as e:
        result['summary'] = f'处理失败: {e}'
    return result


class Handler(SimpleHTTPRequestHandler):

    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 == '/api/uploads':
            # 返回上传日志
            log_file = os.path.join(DATA_DIR, '上传', 'upload_log.json')
            if os.path.exists(log_file):
                try:
                    with open(log_file, 'r', encoding='utf-8') as lf:
                        log = json.load(lf)
                    self._send_json({'ok': True, 'uploads': log})
                except Exception as e:
                    self._send_json({'ok': False, 'error': str(e)}, 500)
            else:
                self._send_json({'ok': True, 'uploads': []})
        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

        # ── 文件上传 API ──
        if path == '/api/upload':
            try:
                content_type = self.headers.get('Content-Type', '')
                if 'multipart/form-data' not in content_type:
                    self._send_json({'ok': False, 'error': '需要 multipart/form-data'}, 400)
                    return
                # 解析 multipart
                boundary = content_type.split('boundary=')[1].strip()
                if boundary.startswith('"') and boundary.endswith('"'):
                    boundary = boundary[1:-1]
                length = int(self.headers.get('Content-Length') or 0)
                raw = self.rfile.read(length)

                # 简单 multipart 解析
                boundary_bytes = ('--' + boundary).encode('utf-8')
                end_boundary = ('--' + boundary + '--').encode('utf-8')
                parts = raw.split(boundary_bytes)
                uploaded_files = []

                for part in parts[1:]:
                    if part.startswith(b'--'):
                        continue
                    if part.startswith(b'\r\n'):
                        part = part[2:]
                    if part.endswith(b'\r\n'):
                        part = part[:-2]

                    header_end = part.find(b'\r\n\r\n')
                    if header_end < 0:
                        continue
                    headers_raw = part[:header_end].decode('utf-8', errors='replace')
                    body = part[header_end + 4:]

                    # 提取 filename
                    import re as _re
                    fn_match = _re.search(r'filename="([^"]*)"', headers_raw)
                    if not fn_match:
                        continue
                    filename = fn_match.group(1)

                    # 保存文件
                    upload_dir = os.path.join(DATA_DIR, '上传')
                    os.makedirs(upload_dir, exist_ok=True)
                    ts = datetime.now().strftime('%Y%m%d_%H%M%S')
                    safe_name = f'{ts}_{filename}'
                    filepath = os.path.join(upload_dir, safe_name)
                    with open(filepath, 'wb') as f:
                        f.write(body)

                    # 自动分类
                    ext = os.path.splitext(filename)[1].lower()
                    ftype_map = {'.xlsx':'ERP/飞书导出', '.xls':'ERP/飞书导出', '.csv':'ERP/飞书导出',
                                 '.docx':'文档', '.doc':'文档', '.pdf':'PDF', '.png':'图片', '.jpg':'图片'}
                    ftype = ftype_map.get(ext, '其他')

                    uploaded_files.append({
                        'original': filename,
                        'saved': safe_name,
                        'size': len(body),
                        'type': ftype,
                        'path': f'数据/上传/{safe_name}'
                    })

                # 记录上传日志
                log_file = os.path.join(DATA_DIR, '上传', 'upload_log.json')
                log = []
                if os.path.exists(log_file):
                    try:
                        with open(log_file, 'r', encoding='utf-8') as lf:
                            log = json.load(lf)
                    except:
                        log = []
                for uf in uploaded_files:
                    log.insert(0, {'time': datetime.now().isoformat(), **uf})
                log = log[:100]  # 保留最近 100 条
                with open(log_file, 'w', encoding='utf-8') as lf:
                    json.dump(log, lf, ensure_ascii=False, indent=2)

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

        # ── 文件处理 API（质检 + 更新 + 归档）──
        if path == '/api/upload/process':
            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
            
            saved_name = payload.get('file', '')
            if not saved_name:
                self._send_json({'ok': False, 'error': 'missing file param'}, 400)
                return
            
            upload_dir = os.path.join(DATA_DIR, '上传')
            filepath = os.path.join(upload_dir, saved_name)
            if not os.path.exists(filepath):
                self._send_json({'ok': False, 'error': f'文件不存在: {saved_name}'}, 404)
                return
            
            ext = os.path.splitext(saved_name)[1].lower()
            result = {'ok': True, 'file': saved_name, 'steps': []}
            
            # Step 1: 质检
            qc = run_quality_check(filepath, ext)
            result['qc'] = qc
            result['steps'].append({'step': '质检', 'ok': qc.get('pass', False), 'summary': qc.get('summary', '')})
            
            if not qc.get('pass'):
                self._send_json(result)
                return
            
            # Step 2: 数据处理
            ftype = qc.get('type', 'unknown')
            process_result = process_uploaded_file(filepath, ftype, saved_name)
            result['process'] = process_result
            result['steps'].append({'step': '数据更新', 'ok': process_result.get('ok', False), 'summary': process_result.get('summary', '')})
            
            # Step 3: 归档（仅处理成功时归档）
            if process_result.get('ok'):
                archive_dir = os.path.join(DATA_DIR, '上传', '_已处理归档')
                os.makedirs(archive_dir, exist_ok=True)
                ts = datetime.now().strftime('%Y%m%d_%H%M%S')
                archive_name = f'{ts}_{saved_name}'
                import shutil
                shutil.move(filepath, os.path.join(archive_dir, archive_name))
                result['archive'] = archive_name
                result['steps'].append({'step': '归档', 'ok': True, 'summary': f'已归档至 数据/上传/_已处理归档/{archive_name}'})
            else:
                result['steps'].append({'step': '归档', 'ok': False, 'summary': '处理未成功，文件保留在上传目录'})
            
            self._send_json(result)
            return

        # ── 原有 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()
