/* ============================================================
   QQ 游戏 AI 频道智能匹配 Demo - 核心逻辑
   链路：用户输入 → 意图识别 → 结构化标签 → 频道匹配 → 推荐理由
        → 用户反馈 → Bad Case 收集
   说明：V1 使用「本地规则 + 标签提取 + 可解释评分」模拟 AI 能力，
        无需 API Key，离线可运行。
   ============================================================ */

/* ------------------------------------------------------------
 * 0. AI 模型配置层（LLM 理解层，与频道匹配逻辑完全解耦）
 *    架构：用户输入 → LLM 理解层 → 结构化用户意图
 *          → 本地频道匹配策略 → 推荐结果
 *    说明：模型只负责「游戏/意图/阶段/标签」识别并输出 JSON，
 *          不直接生成推荐频道；任何失败都会自动回退本地模式。
 * ---------------------------------------------------------- */

// 提供商预设（Base URL 与模型名均可在界面中自由修改，不写死）
const PROVIDERS = {
    openai:   { name: 'OpenAI',     baseUrl: 'https://api.openai.com/v1',                          models: ['gpt-4o-mini', 'gpt-4o'] },
    deepseek: { name: 'DeepSeek',   baseUrl: 'https://api.deepseek.com',                           models: ['deepseek-chat', 'deepseek-reasoner'] },
    qwen:     { name: '通义千问',   baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',  models: ['qwen-plus', 'qwen-turbo', 'qwen-max'] },
    doubao:   { name: '豆包',       baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',           models: ['doubao-pro-32k', 'doubao-lite-32k'] },
    zhipu:    { name: '智谱',       baseUrl: 'https://open.bigmodel.cn/api/paas/v4',               models: ['glm-4-flash', 'glm-4-air'] },
    custom:   { name: '自定义模型', baseUrl: '',                                                   models: [] }
};

// 运行时配置：默认仅保存在页面内存中，刷新即失效；
// 仅当用户主动勾选「记住配置」时才写入 localStorage。
const AI_CONFIG_KEY = 'qqgame_demo_ai_config';
let aiConfig = { mode: 'local', provider: 'openai', apiKey: '', baseUrl: '', model: '' };

/**
 * 统一 LLM 调用接口：所有提供商（OpenAI / DeepSeek / 通义 / 豆包 / 智谱 /
 * 自定义 OpenAI Compatible）均通过该函数返回结构化结果。
 * 更换模型时无需修改任何频道匹配逻辑。
 * @param {{apiKey:string, baseUrl:string, model:string}} config
 * @param {string} prompt
 * @param {number} timeoutMs
 * @returns {Promise<object>} 解析后的 JSON 对象
 */
async function callLLM(config, prompt, timeoutMs = 15000) {
    const url = config.baseUrl.replace(/\/+$/, '') + '/chat/completions';
    const body = {
        model: config.model,
        messages: [{ role: 'user', content: prompt }],
        temperature: 0
    };

    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    let resp;
    try {
        resp = await fetch(url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': 'Bearer ' + config.apiKey
            },
            body: JSON.stringify({ ...body, response_format: { type: 'json_object' } }),
            signal: controller.signal
        });
        // 部分提供商不支持 response_format，400 时降级重试一次
        if (resp.status === 400) {
            resp = await fetch(url, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': 'Bearer ' + config.apiKey
                },
                body: JSON.stringify(body),
                signal: controller.signal
            });
        }
    } finally {
        clearTimeout(timer);
    }

    if (!resp.ok) {
        const err = new Error('HTTP ' + resp.status);
        err.status = resp.status;
        throw err;
    }

    const data = await resp.json();
    const content = data.choices && data.choices[0] && data.choices[0].message
        ? data.choices[0].message.content : '';

    // 容错解析：去掉 markdown 代码块，截取首个 { 到末个 }
    const cleaned = content.replace(/```(?:json)?/gi, '').trim();
    const start = cleaned.indexOf('{');
    const end = cleaned.lastIndexOf('}');
    if (start === -1 || end === -1) throw new Error('模型未返回有效 JSON');
    return JSON.parse(cleaned.slice(start, end + 1));
}

/** 构造意图理解 Prompt，强制模型只输出结构化 JSON（新版 11 类意图） */
function buildAnalysisPrompt(text) {
    return [
        '你是游戏社区的玩家需求理解助手。请分析以下玩家输入，只输出 JSON，不要输出任何其他文字。',
        `玩家输入："""${text}"""`,
        'intent 只能从以下类别中选一个：组队 / 攻略 / 游戏讨论 / 体验评价 / 游戏资讯 / BUG反馈 / 角色培养 / 氪金商业化 / 新手求助 / 活动讨论 / 社交交友 / 赛事观赛。',
        'stage 只能从以下类别中选一个：新手 / 进阶 / 长期玩家 / 未知。',
        '请严格按此 JSON 格式输出：',
        '{"game":"识别到的游戏标准名称，没有则为 null","intent":"上述意图类别之一","stage":"上述阶段类别之一","tags":["从输入中提取的关键词数组"]}'
    ].join('\n');
}

/** 将模型输出的 JSON 归一化为本地匹配算法所需的用户画像结构 */
function normalizeLLMProfile(raw) {
    // 游戏名标准化：命中别名表则使用标准名；未知名称原样保留（进入“无频道”分支）
    const game = raw.game ? standardizeGame(String(raw.game).trim()) : null;
    // 意图归一化：直接匹配 11 类意图，兼容模型输出的近义写法
    const rawIntent = String(raw.intent || '');
    const intentNames = Object.keys(SOCIAL_MAP);
    let intent = intentNames.find(n => rawIntent === n || rawIntent === n.replace('/', ''))
        || intentNames.find(n => rawIntent.includes(n.replace('/', '')) || n.includes(rawIntent) && rawIntent.length >= 2)
        || null;
    // 阶段归一化
    const rawStage = String(raw.stage || '');
    let stage = null;
    if (/新手/.test(rawStage)) stage = '新手';
    else if (/长期|常驻/.test(rawStage)) stage = '长期玩家倾向';
    else if (/进阶|老手|高玩/.test(rawStage)) stage = '进阶玩家';
    // 标签：确保包含游戏名
    const tags = Array.isArray(raw.tags) ? raw.tags.filter(t => typeof t === 'string' && t.trim()) : [];
    if (game && !tags.includes(game)) tags.unshift(game);

    return {
        game,
        intent,
        stage,
        social: intent ? (SOCIAL_MAP[intent] || null) : null,
        tags
    };
}

/* ------------------------------------------------------------
 * 1. 本地频道库（内置数据，避免浏览器 file:// 跨域问题）
 * ---------------------------------------------------------- */
const CHANNELS = [
    { id: 1,  name: '逆水寒手游｜新手组队', game: '逆水寒手游', tags: ['逆水寒手游', '新手', '组队', '副本', '社交'] },
    { id: 2,  name: '逆水寒手游｜攻略讨论', game: '逆水寒手游', tags: ['逆水寒手游', '攻略', '职业', '装备', '技能'] },
    { id: 3,  name: '王者荣耀｜开黑大厅',   game: '王者荣耀',   tags: ['王者荣耀', '组队', '开黑', '好友', '社交'] },
    { id: 4,  name: '王者荣耀｜新手交流',   game: '王者荣耀',   tags: ['王者荣耀', '新手', '入门', '攻略'] },
    { id: 5,  name: '三国杀｜排位攻略',     game: '三国杀',     tags: ['三国杀', '排位', '武将', '攻略'] },
    { id: 6,  name: '原神｜探索交流',       game: '原神',       tags: ['原神', '探索', '任务', '攻略'] },
    { id: 7,  name: '原神｜深渊组队',       game: '原神',       tags: ['原神', '深渊', '组队', '副本'] },
    { id: 8,  name: '鸣潮｜攻略交流',       game: '鸣潮',       tags: ['鸣潮', '攻略', '角色', '装备', '培养'] },
    { id: 9,  name: '英雄联盟｜组队开黑',   game: '英雄联盟',   tags: ['英雄联盟', '组队', '开黑', '排位', '社交'] },
    { id: 10, name: '蛋仔派对｜好友组队',   game: '蛋仔派对',   tags: ['蛋仔派对', '组队', '好友', '休闲', '社交'] },
    { id: 11, name: '异环｜游戏讨论',       game: '异环',       tags: ['异环', '游戏讨论', '体验评价', '玩法', '运营', 'BUG'] },
    { id: 12, name: '异环｜攻略交流',       game: '异环',       tags: ['异环', '攻略', '角色', '玩法', '培养'] },
    { id: 13, name: '异环｜BUG反馈',        game: '异环',       tags: ['异环', 'BUG', '反馈', '问题', '体验'] },
    // V1.2 新增：CS:GO 与电竞赛事频道（覆盖「去哪看职业选手打比赛」类需求）
    { id: 14, name: 'CS:GO｜赛事观赛',      game: 'CS:GO',      tags: ['CS:GO', '赛事', '观赛', '比赛', '职业选手', '直播', '资讯'] },
    { id: 15, name: 'CS:GO｜游戏讨论',      game: 'CS:GO',      tags: ['CS:GO', '游戏讨论', '枪法', '皮肤', '饰品', '攻略'] },
    { id: 16, name: 'CS:GO｜新手入门',      game: 'CS:GO',      tags: ['CS:GO', '新手', '入门', '枪法', '攻略', '组队'] },
    { id: 17, name: '英雄联盟｜赛事观赛',   game: '英雄联盟',   tags: ['英雄联盟', '赛事', '观赛', '比赛', 'LPL', '职业选手', '直播'] },
    { id: 18, name: '无畏契约｜赛事观赛',   game: '无畏契约',   tags: ['无畏契约', '赛事', '观赛', '比赛', 'VCT', '职业选手', '直播'] },
    { id: 19, name: '王者荣耀｜赛事观赛',   game: '王者荣耀',   tags: ['王者荣耀', '赛事', '观赛', '比赛', 'KPL', '职业选手', '直播'] },
    // V1.3 新增：补齐别名表游戏的频道覆盖 + 氪金/活动/社交类意图频道
    { id: 20, name: '逆水寒手游｜庄园社交', game: '逆水寒手游', tags: ['逆水寒手游', '庄园', '社交', '好友', '休闲'] },
    { id: 21, name: '逆水寒手游｜外观时装', game: '逆水寒手游', tags: ['逆水寒手游', '外观', '时装', '氪金', '商业化', '讨论'] },
    { id: 22, name: '王者荣耀｜攻略讨论',   game: '王者荣耀',   tags: ['王者荣耀', '攻略', '英雄', '装备', '铭文'] },
    { id: 23, name: '王者荣耀｜活动福利',   game: '王者荣耀',   tags: ['王者荣耀', '活动', '福利', '签到', '兑换码'] },
    { id: 24, name: '三国杀｜新手教学',     game: '三国杀',     tags: ['三国杀', '新手', '入门', '武将', '攻略'] },
    { id: 25, name: '三国杀｜开黑交友',     game: '三国杀',     tags: ['三国杀', '组队', '开黑', '好友', '社交'] },
    { id: 26, name: '原神｜角色培养',       game: '原神',       tags: ['原神', '角色', '培养', '配队', '攻略'] },
    { id: 27, name: '原神｜抽卡氪金',       game: '原神',       tags: ['原神', '抽卡', '氪金', '商业化', '讨论'] },
    { id: 28, name: '原神｜版本活动',       game: '原神',       tags: ['原神', '活动', '福利', '版本更新', '前瞻'] },
    { id: 29, name: '鸣潮｜角色养成',       game: '鸣潮',       tags: ['鸣潮', '角色', '培养', '配队', '攻略', '装备'] },
    { id: 30, name: '英雄联盟｜攻略进阶',   game: '英雄联盟',   tags: ['英雄联盟', '攻略', '排位', '上分', '英雄', '装备'] },
    { id: 31, name: '蛋仔派对｜地图工坊',   game: '蛋仔派对',   tags: ['蛋仔派对', '地图', '工坊', '攻略', '创意'] },
    { id: 32, name: '蛋仔派对｜新手入门',   game: '蛋仔派对',   tags: ['蛋仔派对', '新手', '入门', '攻略', '休闲'] },
    { id: 33, name: '和平精英｜组队吃鸡',   game: '和平精英',   tags: ['和平精英', '组队', '吃鸡', '开黑', '社交'] },
    { id: 34, name: '和平精英｜战术攻略',   game: '和平精英',   tags: ['和平精英', '攻略', '战术', '枪械', '装备'] },
    { id: 35, name: '穿越火线｜组队开黑',   game: '穿越火线',   tags: ['穿越火线', '组队', '开黑', '枪战', '社交'] },
    { id: 36, name: '穿越火线｜赛事观赛',   game: '穿越火线',   tags: ['穿越火线', '赛事', '观赛', '比赛', 'CFPL', '直播'] },
    { id: 37, name: '三角洲行动｜组队开黑', game: '三角洲行动', tags: ['三角洲行动', '组队', '开黑', '战术', '社交'] },
    { id: 38, name: '三角洲行动｜攻略交流', game: '三角洲行动', tags: ['三角洲行动', '攻略', '枪械', '装备', '玩法'] },
    { id: 39, name: '无畏契约｜组队开黑',   game: '无畏契约',   tags: ['无畏契约', '组队', '开黑', '排位', '社交'] },
    { id: 40, name: '无畏契约｜新手教学',   game: '无畏契约',   tags: ['无畏契约', '新手', '入门', '枪法', '攻略'] },
    { id: 41, name: '绝区零｜攻略交流',     game: '绝区零',     tags: ['绝区零', '攻略', '角色', '培养', '装备'] },
    { id: 42, name: '绝区零｜游戏讨论',     game: '绝区零',     tags: ['绝区零', '游戏讨论', '剧情', '玩法', '角色'] },
    { id: 43, name: '崩坏：星穹铁道｜攻略交流', game: '崩坏：星穹铁道', tags: ['崩坏：星穹铁道', '攻略', '角色', '培养', '配队'] },
    { id: 44, name: '崩坏：星穹铁道｜剧情讨论', game: '崩坏：星穹铁道', tags: ['崩坏：星穹铁道', '游戏讨论', '剧情', '角色', '讨论'] },
    { id: 45, name: '第五人格｜组队开黑',   game: '第五人格',   tags: ['第五人格', '组队', '开黑', '车队', '社交'] },
    { id: 46, name: '第五人格｜攻略教学',   game: '第五人格',   tags: ['第五人格', '攻略', '新手', '角色', '技巧'] },
    { id: 47, name: '我的世界｜生存组队',   game: '我的世界',   tags: ['我的世界', '组队', '生存', '开黑', '好友'] },
    { id: 48, name: '我的世界｜建筑交流',   game: '我的世界',   tags: ['我的世界', '建筑', '攻略', '创意', '交流'] },
    { id: 49, name: '暗区突围｜组队开黑',   game: '暗区突围',   tags: ['暗区突围', '组队', '开黑', '战术', '社交'] },
    { id: 50, name: '暗区突围｜装备攻略',   game: '暗区突围',   tags: ['暗区突围', '攻略', '装备', '枪械', '新手'] },
    { id: 51, name: '光遇｜社交交友',       game: '光遇',       tags: ['光遇', '社交', '交友', '好友', '组队'] },
    { id: 52, name: '光遇｜萌新互助',       game: '光遇',       tags: ['光遇', '新手', '入门', '攻略', '社交'] }
];

/* ------------------------------------------------------------
 * 1.1 临时扩展频道库（AI Discovery 产物）
 *    原始频道库 CHANNELS 永不被修改；AI 发现的频道画像单独存放，
 *    持久化到 localStorage，刷新可恢复，可随时清空。
 * ---------------------------------------------------------- */
const DISCOVERED_KEY = 'qq_ai_discovered_channels';
let discoveredChannels = loadDiscoveredChannels();
const initialDiscoveredCount = discoveredChannels.length; // 页面加载时已入库的扩充频道数（统计用）

function loadDiscoveredChannels() {
    try { return JSON.parse(localStorage.getItem(DISCOVERED_KEY)) || []; }
    catch { return []; }
}
function saveDiscoveredChannels() {
    localStorage.setItem(DISCOVERED_KEY, JSON.stringify(discoveredChannels));
}
/** 最终参与匹配的频道池 = 本地频道库 + AI 扩充频道 */
function getAllChannels() {
    return [...CHANNELS, ...discoveredChannels];
}

/* ------------------------------------------------------------
 * 1.2 Web Discovery（模拟公开 Web 搜索，Demo 演示用）
 *    静态页面无法直连搜索引擎；真实环境可替换为搜索 API。
 *    模拟语料中故意混入其他游戏结果，用于验证过滤逻辑。
 * ---------------------------------------------------------- */
const MOCK_WEB_CORPUS = {
    '异环': [
        { title: '异环玩家社区：首测体验与 BUG 汇总讨论', type: '游戏讨论', tags: ['体验评价', '玩法', 'BUG', '画面', '运营'] },
        { title: '异环 BUG 反馈与优化建议合集',           type: 'BUG反馈',   tags: ['BUG', '反馈', '问题', '体验'] },
        { title: '异环新手入门与角色培养攻略',           type: '攻略',     tags: ['攻略', '角色', '培养', '新手'] },
        { title: '异环值不值得长期玩？玩家评价汇总',     type: '体验评价', tags: ['体验评价', '运营', '长期游玩'] },
        // 干扰项：必须被过滤
        { title: '逆水寒手游赛季攻略汇总', type: '攻略', tags: ['攻略'] },
        { title: '王者荣耀开黑组队社区',   type: '社交', tags: ['组队'] }
    ]
};

/**
 * 模拟 Web 搜索：根据游戏 / 意图动态生成搜索词，返回公开信息片段。
 * 明显无效的游戏名（如纯 "XX"）返回空结果，用于演示「拒绝生成」兜底。
 */
function searchWeb(game, intent) {
    const queries = [
        `${game} 玩家 社区`, `${game} 游戏讨论`,
        `${game} ${intent || '玩家'} 评价`, `${game} 攻略 社区`
    ];
    if (MOCK_WEB_CORPUS[game]) {
        return { queries, results: MOCK_WEB_CORPUS[game] };
    }
    if (/^[xX\s]+$/.test(game) || game.length < 2) {
        return { queries, results: [] }; // 公开信息不足
    }
    // 未收录游戏：模拟通用公开社区信息
    return {
        queries,
        results: [
            { title: `${game}玩家交流社区`,           type: '游戏讨论', tags: ['游戏讨论', '交流', '体验评价'] },
            { title: `${game}新手攻略与玩法讨论`,     type: '攻略',     tags: ['攻略', '玩法', '新手'] },
            { title: `${game}玩家评价：值得入坑吗`,   type: '体验评价', tags: ['体验评价', '运营'] }
        ]
    };
}

/** 搜索结果过滤：只保留确实讨论目标游戏的有效内容，去广告、去其他游戏、去重 */
function filterSearchResults(game, results) {
    const seen = new Set();
    return results.filter(r => {
        if (!r.title.includes(game)) return false;              // 淘汰其他游戏（游戏实体必须一致）
        if (/广告|下载链接|充值返利/.test(r.title)) return false; // 去广告
        if (seen.has(r.title)) return false;                    // 去重
        seen.add(r.title);
        return true;
    });
}

/* ------------------------------------------------------------
 * 1.3 AI 频道画像生成
 *    不直接照搬网页标题，而是把搜索结果抽象为结构化频道画像。
 *    该画像是 Demo 生成的数据，不是真实 QQ 频道。
 * ---------------------------------------------------------- */
const INTENT_CHANNEL_THEME = {
    '体验评价': '玩家体验讨论', 'BUG反馈': 'BUG反馈交流', '组队': '玩家组队交流',
    '攻略': '攻略交流', '角色培养': '角色培养交流', '新手求助': '新手互助',
    '游戏讨论': '玩家讨论区', '社交交友': '玩家交友', '氪金/商业化': '运营讨论',
    '活动讨论': '活动讨论', '游戏资讯': '资讯讨论'
};

/** 本地生成频道画像（LLM 不可用时的默认实现） */
function buildChannelProfile(game, intent, tags, filtered) {
    const intentTags = INTENT_TAG_MAP[intent] || [];
    // 优先使用与用户意图相关的来源确定主题
    const relevant = filtered.filter(r =>
        r.type === intent || (r.tags || []).some(t => t === intent || intentTags.includes(t)));
    const themeSources = relevant.length > 0 ? relevant : filtered;

    // 聚合标签：游戏 + 意图 + 用户标签 + 来源高频标签
    const tagCount = {};
    themeSources.forEach(r => (r.tags || []).forEach(t => { tagCount[t] = (tagCount[t] || 0) + 1; }));
    const hotTags = Object.entries(tagCount).sort((a, b) => b[1] - a[1]).map(([t]) => t);
    const mergedTags = [...new Set([game, intent, ...tags, ...hotTags].filter(Boolean))].slice(0, 6);

    // 频道名：游戏｜主题；与现有频道重名时加 AI 后缀
    const theme = INTENT_CHANNEL_THEME[intent] || '玩家讨论区';
    let name = `${game}｜${theme}`;
    if (getAllChannels().some(ch => ch.name === name)) name = `${game}｜${theme}·AI`;

    const confidence = Math.min(0.95, 0.7 + filtered.length * 0.05);
    return {
        id: 'd' + Date.now(),
        name,
        game,
        tags: mergedTags,
        channelType: intent || '游戏讨论',
        targetUsers: ['普通玩家'],
        topics: hotTags.slice(0, 4),
        source: 'AI Discovery',
        confidence,
        sources: filtered.map(s => s.title),
        createdAt: new Date().toISOString()
    };
}

/**
 * 执行 AI Discovery：Web 搜索 → 结果过滤 → 生成频道画像。
 * 信息不足时返回 ok:false，拒绝生成（避免 AI 幻觉）。
 */
async function runDiscovery(profile) {
    const { queries, results } = searchWeb(profile.game, profile.intent);
    const filtered = filterSearchResults(profile.game, results);
    if (filtered.length === 0) {
        return { ok: false, queries, sources: [], channel: null };
    }
    // 真实 AI 模式下可选：让 LLM 从搜索结果中提取画像；失败回退本地生成
    let channel = null;
    const llmReady = aiConfig.mode === 'llm' && aiConfig.apiKey && aiConfig.baseUrl && aiConfig.model;
    if (llmReady) {
        try {
            const raw = await callLLM(aiConfig, buildProfilePrompt(profile, filtered), 12000);
            channel = normalizeChannelProfile(raw, profile, filtered);
        } catch { channel = null; }
    }
    if (!channel) channel = buildChannelProfile(profile.game, profile.intent, profile.tags, filtered);
    return { ok: true, queries, sources: filtered, channel };
}

/** LLM 画像提取 Prompt（真实 AI 模式可选增强） */
function buildProfilePrompt(profile, filtered) {
    return [
        '你是游戏频道生态运营助手。根据玩家需求和公开 Web 搜索结果，抽象出一个游戏频道画像，只输出 JSON。',
        `游戏：${profile.game}；玩家意图：${profile.intent || '未知'}；玩家标签：${(profile.tags || []).join('、')}`,
        '搜索结果：' + filtered.map(r => `《${r.title}》`).join('、'),
        '输出格式：{"channelName":"游戏｜主题","channelType":"频道类型","tags":["标签数组"],"topics":["主题数组"],"targetUsers":["目标用户"],"confidence":0.0到1.0}'
    ].join('\n');
}

/** 校验并补全 LLM 生成的频道画像；不合法则抛错回退本地生成 */
function normalizeChannelProfile(raw, profile, filtered) {
    if (!raw || typeof raw.channelName !== 'string' || !raw.channelName.includes(profile.game)) {
        throw new Error('LLM 画像无效');
    }
    const base = buildChannelProfile(profile.game, profile.intent, profile.tags, filtered);
    return {
        ...base,
        name: getAllChannels().some(ch => ch.name === raw.channelName) ? raw.channelName + '·AI' : raw.channelName,
        channelType: typeof raw.channelType === 'string' ? raw.channelType : base.channelType,
        tags: Array.isArray(raw.tags) && raw.tags.length ? [...new Set([profile.game, ...raw.tags])].slice(0, 6) : base.tags,
        topics: Array.isArray(raw.topics) && raw.topics.length ? raw.topics.slice(0, 4) : base.topics,
        targetUsers: Array.isArray(raw.targetUsers) && raw.targetUsers.length ? raw.targetUsers : base.targetUsers,
        confidence: typeof raw.confidence === 'number' ? Math.min(0.95, Math.max(0.5, raw.confidence)) : base.confidence
    };
}

/* ------------------------------------------------------------
 * 1.4 频道空缺检测
 *    sufficient  ：游戏存在且有高匹配频道（>=60），无需扩充
 *    insufficient：游戏存在但最高匹配 <60，主题覆盖不足，触发扩充
 *    not_found   ：频道库完全没有该游戏，触发扩充
 * ---------------------------------------------------------- */
function checkChannelCoverage(profile) {
    const pool = getAllChannels().filter(ch => ch.game === profile.game);
    if (!profile.game) {
        return { gameExists: false, matchedChannels: 0, coverage: 'not_found', shouldExpand: false };
    }
    if (pool.length === 0) {
        return { gameExists: false, matchedChannels: 0, coverage: 'not_found', shouldExpand: true };
    }
    const bestScore = Math.max(...pool.map(ch => scoreChannel(profile, ch).total));
    const sufficient = bestScore >= MATCH_THRESHOLD;
    return {
        gameExists: true,
        matchedChannels: pool.length,
        coverage: sufficient ? 'sufficient' : 'insufficient',
        shouldExpand: !sufficient
    };
}

/* ------------------------------------------------------------
 * 2. 语义识别规则库（本地轻量规则，模拟 AI 意图识别）
 * ---------------------------------------------------------- */
// 游戏实体标准化表：标准名 -> 别名
// AI / 规则识别出的游戏名必须先经 standardizeGame() 标准化，再进入匹配算法
const GAME_ALIASES = {
    '异环':       ['异环', 'ANANTA', 'Ananta', 'ananta'],
    '逆水寒手游': ['逆水寒', '逆水寒手游'],
    '王者荣耀':   ['王者', '王者荣耀', '农药'],
    '原神':       ['原神'],
    '三国杀':     ['三国杀'],
    '鸣潮':       ['鸣潮'],
    '英雄联盟':   ['英雄联盟', 'LOL', 'lol', '撸啊撸'],
    '蛋仔派对':   ['蛋仔派对', '蛋仔'],
    '和平精英':   ['和平精英', '吃鸡'],
    '三角洲行动': ['三角洲行动', '三角洲'],
    '幻塔':       ['幻塔'],
    '第五人格':   ['第五人格', '第五', 'D5'],
    '我的世界':   ['我的世界', 'MC', 'mc'],
    '穿越火线':   ['穿越火线', 'CF', 'cf'],
    '无畏契约':   ['无畏契约', '瓦罗兰特', 'VALORANT', 'valorant'],
    '绝区零':     ['绝区零', 'ZZZ', 'zzz'],
    '崩坏：星穹铁道': ['崩坏：星穹铁道', '星穹铁道', '崩铁'],
    '暗区突围':   ['暗区突围', '暗区'],
    '光遇':       ['光遇'],
    'CS:GO':      ['CS:GO', 'CSGO', 'csgo', 'CS2', 'cs2', 'CS', 'cs', 'go']
};

/** 游戏名标准化：命中别名表返回标准名；未收录名称原样保留（用于“暂无频道”场景） */
function standardizeGame(name) {
    if (!name) return null;
    for (const [standard, aliases] of Object.entries(GAME_ALIASES)) {
        if (name === standard || aliases.some(a => name.toLowerCase() === a.toLowerCase())) {
            return standard;
        }
    }
    return name;
}

/** HTML 转义：所有进入 innerHTML 的动态内容（模型输出 / 用户输入 / localStorage）必须先经过此函数，防 XSS */
function escapeHTML(s) {
    return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({
        '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
    }[c]));
}

// 否定词表：触发词前出现这些词时，该次命中不计入（处理“不是想找队友”“不想打深渊”等否定句）
const NEGATION_WORDS = ['不是', '不想', '不要', '不找', '不打算', '不需要', '不玩', '没玩', '没想', '没打算', '别'];

/** 判断文本中某触发词位置是否处于否定语境（向前看 6 个字符） */
function isNegated(text, index) {
    const prefix = text.slice(Math.max(0, index - 6), index);
    return NEGATION_WORDS.some(w => prefix.includes(w));
}

/** 统计触发词命中次数：否定语境下的命中不计入 */
function countTriggers(text, triggers) {
    let count = 0;
    for (const w of triggers) {
        const idx = text.indexOf(w);
        if (idx !== -1 && !isNegated(text, idx)) count++;
    }
    return count;
}

// 意图分类（11 类）：按触发词命中次数计票，得票最高者胜出；平票按定义顺序优先
const INTENT_RULES = [
    { intent: 'BUG反馈',     triggers: ['bug', 'BUG', 'Bug', '闪退', '卡顿', '报错', '崩溃', '掉帧'] },
    { intent: '氪金/商业化', triggers: ['氪金', '不氪', '逼氪', '抽卡', '充值', '付费', '月卡', '战令', '商业化'] },
    { intent: '角色培养',    triggers: ['培养', '养成', '练度', '突破', '命座', '升星'] },
    { intent: '新手求助',    triggers: ['刚玩', '新手', '萌新', '入门', '刚入坑', '小白', '怎么玩'] },
    { intent: '活动讨论',    triggers: ['活动', '福利', '签到', '兑换码', '前瞻', '版本更新'] },
    { intent: '游戏资讯',    triggers: ['资讯', '公告', '爆料', '什么时候上线', '公测', '开服'] },
    { intent: '组队',        triggers: ['一起玩', '组队', '开黑', '有没有人', '找人', '一起打', '队伍', '车队', '队友', '带带', '求带', '带', '有人玩', '一起', '双排', '排位赛', '五排'] },
    { intent: '攻略',        triggers: ['攻略', '怎么', '如何', '搭配', '教程', '好用', '玩法', '阵容', '加点'] },
    { intent: '体验评价',    triggers: ['不错', '好玩', '值得', '画面', '画质', '体验', '评价', '失望', '拉胯', '优化差', '推荐入手'] },
    { intent: '游戏讨论',    triggers: ['讨论', '交流', '聊聊', '怎么看', '觉得'] },
    { intent: '赛事观赛',    triggers: ['比赛', '赛事', '观赛', '职业选手', '战队', '直播', '总决赛', '淘汰赛', '回放'] },
    { intent: '社交交友',    triggers: ['聊天', '交友', '扩列', '唠嗑', 'cpdd'] }
];

// 用户阶段规则
const STAGE_RULES = {
    '新手':         ['刚玩', '新手', '萌新', '入门', '刚入坑', '小白'],
    '长期玩家倾向': ['长期游玩', '长期', '一直玩', '常驻', '养老', '玩下去'],
    '进阶玩家':     ['排位', '上分', '冲分', '深渊', '老玩家', '段位']
};

// 意图 -> 社交需求推断
const SOCIAL_MAP = {
    '组队': '组队', '攻略': '讨论', '游戏讨论': '讨论', '体验评价': '讨论',
    '游戏资讯': '关注', 'BUG反馈': '反馈', '角色培养': '讨论', '氪金/商业化': '讨论',
    '新手求助': '求助', '活动讨论': '讨论', '社交交友': '交友', '赛事观赛': '关注'
};

// 意图 -> 频道标签映射（第二阶段「意图匹配」判分依据，权重 35%）
const INTENT_TAG_MAP = {
    '组队':        ['组队', '开黑', '队友'],
    '攻略':        ['攻略'],
    '游戏讨论':    ['游戏讨论', '讨论', '交流'],
    '体验评价':    ['体验评价', '体验', '评价', '讨论'],
    '游戏资讯':    ['资讯', '公告', '活动'],
    'BUG反馈':     ['BUG', '反馈', '问题'],
    '角色培养':    ['培养', '角色'],
    '氪金/商业化': ['运营', '氪金', '商业化'],
    '新手求助':    ['新手', '入门', '求助'],
    '活动讨论':    ['活动', '福利'],
    '社交交友':    ['社交', '交友', '好友'],
    '赛事观赛':    ['赛事', '观赛', '比赛', '直播']
};

// 阶段 -> 频道标签映射（「用户阶段匹配」判分依据，权重 15%）
const STAGE_TAG_MAP = {
    '新手':         ['新手', '入门'],
    '进阶玩家':     ['排位', '深渊', '上分'],
    '长期玩家倾向': ['长期', '运营', '日常']
};

// 频道主题（频道名「｜」后缀）-> 意图亲和表（「频道主题匹配」判分依据，权重 20%）
const TOPIC_INTENT_AFFINITY = {
    '新手': ['新手求助', '组队', '攻略'],
    '组队': ['组队', '新手求助'],
    '开黑': ['组队', '社交交友'],
    '好友': ['社交交友', '组队'],
    '攻略': ['攻略', '角色培养', '新手求助'],
    '讨论': ['游戏讨论', '体验评价', '活动讨论', '氪金/商业化'],
    '交流': ['游戏讨论', '攻略', '角色培养'],
    '探索': ['攻略', '游戏讨论'],
    '深渊': ['组队', '攻略'],
    '排位': ['攻略', '组队'],
    'BUG':  ['BUG反馈', '体验评价'],
    '反馈': ['BUG反馈', '体验评价'],
    '资讯': ['游戏资讯', '活动讨论'],
    '活动': ['活动讨论', '游戏资讯'],
    '赛事': ['赛事观赛', '游戏资讯'],
    '观赛': ['赛事观赛', '游戏资讯'],
    '直播': ['赛事观赛', '游戏讨论'],
    // V1.3 新增频道主题亲和
    '教学': ['新手求助', '攻略'],
    '养成': ['角色培养', '攻略'],
    '剧情': ['游戏讨论', '攻略'],
    '战术': ['攻略', '组队'],
    '吃鸡': ['组队', '攻略'],
    '互助': ['新手求助', '组队', '社交交友'],
    '建筑': ['攻略', '游戏讨论'],
    '生存': ['组队', '攻略', '游戏讨论'],
    '庄园': ['社交交友', '游戏讨论'],
    '时装': ['氪金/商业化', '游戏讨论'],
    '工坊': ['攻略', '游戏讨论'],
    '社交': ['社交交友', '组队', '游戏讨论'],
    '交友': ['社交交友', '组队'],
    '氪金': ['氪金/商业化', '游戏讨论'],
    '抽卡': ['氪金/商业化', '游戏讨论']
};

// 补充关键词词表（命中则加入用户标签）
const EXTRA_KEYWORDS = ['副本', '深渊', '排位', '开黑', '组队', '攻略', '武将', '装备', '角色',
    '培养', '技能', '职业', '新手', '探索', '任务', '好友', '休闲', '上分', '入门',
    '画面', '画质', '玩法', '运营', 'BUG', 'bug', '反馈', '问题', '剧情', '抽卡', '氪金',
    '活动', '福利', '长期游玩', '长期', '体验', '讨论',
    '比赛', '赛事', '观赛', '职业选手', '直播', '战队',
    '吃鸡', '战术', '枪法', '皮肤', '时装', '外观', '庄园', '建筑', '生存', '配队', '工坊', '剧情'];
// 标签归一化：同义写法合并，避免重复计分
const TAG_NORMALIZE = { 'bug': 'BUG', 'Bug': 'BUG', '长期': '长期游玩' };

/* ------------------------------------------------------------
 * 3. 快捷测试案例
 * ---------------------------------------------------------- */
const EXAMPLES = [
    { label: '新手求助', text: '刚玩逆水寒手游，想找几个新手一起打副本。' },
    { label: '组队开黑', text: '王者荣耀有没有人晚上一起开黑？' },
    { label: '攻略交流', text: '三国杀排位有什么好用的武将搭配？' },
    { label: '赛事观赛', text: '去哪里看CS:GO职业选手打比赛呀' },
    { label: '游戏讨论', text: '最近在玩一个叫幻塔的游戏，想交流一下' },
    { label: 'BUG反馈',  text: '异环不错啊，画面也真实，但是玩法空间、bug多，但是凭着抽角色不氪的运营确实值得我长期游玩' },
    { label: '找运营',   text: '异环有没有新手群？刚入坑求带' }
];

/* ------------------------------------------------------------
 * 4. 匹配算法配置（两阶段匹配）
 *    第一阶段：游戏实体硬过滤（不参与打分，跨游戏频道直接出局）
 *    第二阶段：意图 35% + 标签 30% + 阶段 15% + 频道主题 20%
 *    阈值：80~100 强匹配 / 60~79 一般匹配 / <60 不推荐
 * ---------------------------------------------------------- */
const WEIGHTS = { intent: 35, tag: 30, stage: 15, topic: 20 };
const MATCH_THRESHOLD = 60;   // 最低推荐阈值：低于该分数不强行推荐
const STRONG_THRESHOLD = 80;  // 强匹配下限

/* ==================== 意图理解 ==================== */

/**
 * 分析用户输入，输出结构化用户画像
 * @returns {{game:string|null, intent:string|null, stage:string|null,
 *            social:string|null, tags:string[]}}
 */
function analyzeInput(text) {
    // 1) 游戏识别 + 标准化：别名表命中；兜底识别“一个叫 XX 的游戏”中的未收录名称
    //    （否定语境下的命中不计入，如“我不玩原神”不会被识别为原神）
    let game = null;
    outer:
    for (const [standard, aliases] of Object.entries(GAME_ALIASES)) {
        for (const alias of aliases) {
            const idx = text.indexOf(alias);
            if (idx !== -1 && !isNegated(text, idx)) { game = standard; break outer; }
        }
    }
    if (!game) {
        // 兜底提取未收录游戏名：“一个叫 XX 的游戏” / “XX 是什么游戏”
        const m = text.match(/叫[「"']?([一-龥A-Za-z0-9]{1,12}?)[」"']?的?游戏/)
            || text.match(/([一-龥A-Za-z0-9]{1,12}?)\s*是什么游戏/);
        if (m) game = standardizeGame(m[1]);
    }

    // 2) 意图识别：11 类意图按触发词命中次数计票（否定语境不计票），避免非明确需求被粗暴归入“社交”
    let intent = null;
    let bestCount = 0;
    for (const rule of INTENT_RULES) {
        const count = countTriggers(text, rule.triggers);
        if (count > bestCount) { bestCount = count; intent = rule.intent; }
    }

    // 3) 用户阶段识别（否定语境不计，如“不想打深渊”不计为进阶玩家）
    let stage = null;
    for (const [stageName, triggers] of Object.entries(STAGE_RULES)) {
        if (countTriggers(text, triggers) > 0) { stage = stageName; break; }
    }

    // 4) 社交需求：由意图推断
    const social = intent ? (SOCIAL_MAP[intent] || null) : null;

    // 5) 关键词 / 标签提取：游戏 + 意图 + 阶段（新手）+ 补充词表命中（含归一化；否定语境不计）
    const tags = new Set();
    if (game) tags.add(game);
    if (intent) tags.add(intent);
    if (stage === '新手') tags.add('新手');
    EXTRA_KEYWORDS.forEach(word => {
        const idx = text.indexOf(word);
        if (idx !== -1 && !isNegated(text, idx)) tags.add(TAG_NORMALIZE[word] || word);
    });

    return { game, intent, stage, social, tags: Array.from(tags) };
}

/* ==================== 匹配评分（第二阶段：候选排序） ==================== */

/**
 * 计算单个候选频道的匹配度（0~100）。
 * 注意：调用前必须已完成第一阶段游戏过滤，channel.game === profile.game。
 * 评分公式：意图 35% + 标签重合(Dice) 30% + 阶段 15% + 频道主题 20%
 */
function scoreChannel(profile, channel) {
    // 意图维度（35%）：频道标签命中意图核心标签
    const intentTags = INTENT_TAG_MAP[profile.intent] || [];
    const intentHit = intentTags.some(t => channel.tags.includes(t));
    const intentScore = intentHit ? WEIGHTS.intent : 0;

    // 标签维度（30%）：用户标签与频道标签的 Dice 重合系数
    const overlap = profile.tags.filter(t => channel.tags.includes(t));
    const dice = (profile.tags.length + channel.tags.length) === 0 ? 0
        : (2 * overlap.length) / (profile.tags.length + channel.tags.length);
    const tagScore = Math.round(WEIGHTS.tag * dice);

    // 阶段维度（15%）：频道标签命中阶段标签
    const stageTags = STAGE_TAG_MAP[profile.stage] || [];
    const stageHit = profile.stage !== null && stageTags.some(t => channel.tags.includes(t));
    const stageScore = stageHit ? WEIGHTS.stage : 0;

    // 频道主题维度（20%）：频道名「｜」后缀主题与用户意图的亲和度
    const topic = channel.name.split('｜')[1] || '';
    const topicHit = Object.keys(TOPIC_INTENT_AFFINITY).some(
        k => topic.includes(k) && TOPIC_INTENT_AFFINITY[k].includes(profile.intent)
    );
    const topicScore = topicHit ? WEIGHTS.topic : 0;

    return {
        total: intentScore + tagScore + stageScore + topicScore,
        detail: { intentScore, tagScore, stageScore, topicScore, intentHit, stageHit, topicHit, overlap }
    };
}

/* ==================== 两阶段匹配主入口 ==================== */

/**
 * 第一阶段：游戏实体硬过滤 + Bad Case 排除/降权规则
 * 第二阶段：候选排序 + 最低阈值判定
 * @param {object} profile 用户画像
 * @param {object|null} extraCandidate AI Discovery 待审核频道（临时参与匹配，未入库）
 */
function matchChannels(profile, extraCandidate = null) {
    const rules = getBadcaseRules();
    const all = getAllChannels();
    let pool;
    let appliedRules = 0;
    let candidateCount = null;

    if (profile.game) {
        // 第一阶段：只保留同游戏频道，跨游戏频道禁止进入排序
        let sameGame = all.filter(ch => ch.game === profile.game);
        // AI Discovery 待审核候选：必须游戏实体一致才能进入候选池
        if (extraCandidate && extraCandidate.game === profile.game) {
            sameGame = [...sameGame, extraCandidate];
        }
        candidateCount = sameGame.length;
        // Bad Case 策略：本地频道被标记后直接排除；AI 发现频道走降权（见下）
        const excluded = rules.exclude[profile.game] || [];
        appliedRules = excluded.length;
        pool = sameGame.filter(ch => ch.source === 'AI Discovery' || !excluded.includes(ch.name));
        // 频道库没有该游戏频道（或全部被排除）：不强行推荐其他游戏
        if (pool.length === 0) {
            return { status: 'no_channel', best: null, candidates: [], appliedRules, candidateCount };
        }
    } else {
        // 未识别到游戏：无法进行游戏过滤，全库参与排序，但仍受最低阈值约束
        pool = all.slice();
    }

    // 第二阶段：候选排序（AI 发现频道按 Bad Case 次数降权，多次错误标记为低置信）
    const scored = pool
        .map(ch => {
            const s = scoreChannel(profile, ch);
            const badCount = countChannelBadCases(ch.name);
            if (ch.source === 'AI Discovery' && badCount > 0) {
                s.total = Math.max(0, s.total - 15 * badCount);
                s.lowConfidence = badCount >= 2; // 多次反馈错误 → 低置信频道
            }
            return { channel: ch, ...s };
        })
        .sort((a, b) => b.total - a.total);
    const best = scored[0];

    // 最低匹配阈值：最高分低于 60 不推荐；
    // 未识别到游戏时无法做硬过滤，置信度要求更高（需达强匹配线 80 才推荐）
    const threshold = profile.game ? MATCH_THRESHOLD : STRONG_THRESHOLD;
    if (best.total < threshold) {
        return { status: 'below_threshold', best, candidates: scored, appliedRules, candidateCount };
    }
    return { status: 'ok', best, candidates: scored, appliedRules, candidateCount };
}

/** 统计某频道被反馈为 Bad Case 的次数（用于 AI 发现频道降权） */
function countChannelBadCases(channelName) {
    const data = loadFeedback();
    return (data.bad || []).filter(b => (b.wrongChannel || b.recommended) === channelName).length;
}

/** 匹配等级：80~100 强匹配 / 60~79 一般匹配 */
function matchLevel(score) {
    return score >= STRONG_THRESHOLD ? '强匹配' : '一般匹配';
}

/** 从 localStorage 的 Bad Case 记录中生成排除规则（反馈 → 策略更新闭环） */
function getBadcaseRules() {
    const data = loadFeedback();
    const exclude = {};
    (data.bad || []).forEach(b => {
        if (!b.game || !b.recommended) return;
        if (b.reason === '游戏识别错误' || b.reason === '推荐频道不匹配') {
            (exclude[b.game] = exclude[b.game] || []).push(b.recommended);
        }
    });
    return { exclude };
}

/** 生成推荐理由：必须基于真实命中维度，禁止编造（如“你是新手所以推荐新手组队”） */
function buildReason(profile, channel, scored) {
    const d = scored.detail;
    const meaningful = d.overlap.filter(t => t !== profile.game && t !== profile.intent);
    const dims = [];
    if (d.intentHit && profile.intent) dims.push(`意图「${profile.intent}」`);
    if (d.stageHit && profile.stage) dims.push(`阶段「${profile.stage}」`);
    if (d.topicHit) dims.push('频道主题');

    if (meaningful.length > 0) {
        const quoted = meaningful.map(t => `「${t}」`).join('、');
        return `识别到你正在讨论${profile.game ? `「${profile.game}」的` : ''} ${quoted} 相关内容，` +
            `${dims.length ? dims.join('、') + '均命中，' : ''}与该频道的讨论方向高度匹配。`;
    }
    if (profile.game) {
        return `识别到你在咨询「${profile.game}」相关内容，` +
            `${dims.length ? dims.join('、') + '命中，' : ''}该频道是同游戏频道中与你需求最接近的。`;
    }
    return '未识别到具体游戏，根据描述关键词与频道标签的相关性推荐；建议补充游戏名称以获得更准确的结果。';
}

/* ==================== 页面交互 ==================== */

const $ = id => document.getElementById(id);
const sleep = ms => new Promise(r => setTimeout(r, ms));

let isAnalyzing = false;
let lastResult = null; // 最近一次匹配结果（用于 Bad Case 记录）

/* 渲染热门场景快捷 chips（点击自动填充输入框） */
function renderExamples() {
    $('examplesList').innerHTML = EXAMPLES.map((ex, i) =>
        `<span class="example-chip" data-index="${i}" title="${ex.text}">${ex.label}</span>`
    ).join('');
    document.querySelectorAll('.example-chip').forEach(chip => {
        chip.addEventListener('click', () => {
            $('userInput').value = EXAMPLES[Number(chip.dataset.index)].text;
            $('userInput').classList.remove('input-error');
            $('charCount').textContent = $('userInput').value.length;
            $('userInput').focus();
        });
    });
}

/* 分析步骤动画：识别游戏 → 理解意图 → 提取标签 → 匹配频道（纵向步骤，含每步结果） */
async function runStepsAnimation(profile, matchResult) {
    const best = matchResult.status === 'ok' ? matchResult.best : null;
    const steps = document.querySelectorAll('.step');
    const status = $('stepStatus');

    // 重置步骤状态（恢复数字圆点、清空结果值）
    steps.forEach((s, i) => {
        s.classList.remove('active', 'done');
        s.querySelector('.step-dot').textContent = String(i + 1);
        s.querySelector('.step-value').textContent = '';
    });

    const stepMessages = [
        `正在识别游戏…… 识别结果：<span class="tag">${profile.game || '未识别到具体游戏'}</span>`,
        `正在理解意图…… 意图：<span class="tag">${profile.intent || '未明确'}</span>`,
        `正在提取标签…… ${profile.tags.map(t => `<span class="tag">${t}</span>`).join('') || '无有效标签'}`,
        best
            ? `正在匹配频道…… 游戏过滤后候选 ${candidateCountText()}，Top 1：<span class="tag">${best.channel.name}</span>`
            : matchResult.status === 'no_channel'
                ? `正在匹配频道…… 频道库中暂无「${profile.game}」相关频道，不进行跨游戏推荐`
                : `正在匹配频道…… 最高匹配度低于 ${MATCH_THRESHOLD} 分阈值，不进行推荐`
    ];
    // 每步完成后展示在步骤行尾的结果值
    const stepValues = [
        profile.game || '未识别',
        profile.intent || '未明确',
        profile.tags.filter(t => t !== profile.game && t !== profile.intent).slice(0, 4).join(' / ') || '—',
        best ? `找到 ${matchResult.candidateCount} 个相关频道` : '未找到合适频道'
    ];

    function candidateCountText() {
        return profile.game
            ? `${getAllChannels().filter(c => c.game === profile.game).length} 个（已过滤其他游戏）`
            : `${getAllChannels().length} 个（未识别游戏，全库参与）`;
    }

    for (let i = 0; i < steps.length; i++) {
        steps[i].classList.add('active');
        status.innerHTML = stepMessages[i];
        await sleep(320); // 单步耗时，全程约 1.3s
        steps[i].classList.remove('active');
        steps[i].classList.add('done');
        steps[i].querySelector('.step-dot').textContent = '✓';
        steps[i].querySelector('.step-value').textContent = stepValues[i];
    }
    status.innerHTML = '分析完成，已生成 AI 用户画像与推荐结果。';
}

/* ==================== AI Discovery 过程展示 ==================== */

/** 向 Discovery 日志区追加一行轻量状态 */
function logDiscovery(text) {
    const log = $('discoveryLog');
    log.classList.remove('hidden');
    const line = document.createElement('div');
    line.className = 'discovery-log-line fade-in';
    line.innerHTML = text;
    log.appendChild(line);
}

/** 带过程展示的 Discovery：覆盖检测 → Web 搜索 → 过滤 → 生成画像 */
async function runDiscoveryWithLog(profile, coverage) {
    sessionStats.generated += 0; // 占位，确认生成后再计数
    logDiscovery(coverage.coverage === 'not_found'
        ? `查询频道库 ✓ 频道库中没有「${escapeHTML(profile.game)}」相关频道，允许触发频道扩充。`
        : `查询频道库 ✓ 本地最高匹配低于 ${MATCH_THRESHOLD} 分，「${escapeHTML(profile.game)}」频道主题覆盖不足。`);
    await sleep(350);
    logDiscovery('发现频道覆盖不足，正在寻找相关游戏社区……');
    await sleep(400);

    const discovery = await runDiscovery(profile);
    if (!discovery.ok) {
        logDiscovery('Web Discovery ✓ 暂未找到足够可靠的公开信息，无法生成频道画像。');
        return discovery;
    }
    logDiscovery(`Web Discovery ✓ 检索「${escapeHTML(discovery.queries[0])}」等 ${discovery.queries.length} 组关键词，` +
        `过滤后保留 ${discovery.sources.length} 条目标游戏相关信息（已淘汰其他游戏 / 广告 / 重复）。`);
    await sleep(400);
    sessionStats.generated += 1;
    sessionStats.games.add(profile.game);
    logDiscovery(`生成频道画像 ✓ <b>${escapeHTML(discovery.channel.name)}</b>（置信度 ${Math.round(discovery.channel.confidence * 100)}%）· AI 自动发现频道画像，待人工审核`);
    await sleep(350);
    return discovery;
}

/* Demo 运行期统计（发现游戏 / 生成画像数） */
const sessionStats = { generated: 0, games: new Set() };

/* 渲染 AI 用户画像 */
function renderProfile(profile) {
    $('profileGame').textContent = profile.game || '未识别';
    $('profileIntent').textContent = profile.intent || '未明确';
    $('profileStage').textContent = profile.stage || '未明确';
    $('profileSocial').textContent = profile.social || '未明确';
    $('profileTags').innerHTML = profile.tags.map(t => `<span class="profile-tag">${escapeHTML(t)}</span>`).join('');
    $('profileBox').classList.remove('hidden');
    $('profileBox').classList.add('fade-in');
}

/* 渲染推荐结果（有匹配频道） */
function renderResult(best, matchResult) {
    const profile = lastResult.profile;
    $('resultContent').classList.remove('hidden');
    $('noResult').classList.add('hidden');

    const isDiscovery = best.channel.source === 'AI Discovery';
    $('topChannelName').textContent = best.channel.name;
    $('topChannelTags').innerHTML = best.channel.tags.map(t => `<span class="top-channel-tag">${escapeHTML(t)}</span>`).join('')
        + (best.lowConfidence ? '<span class="top-channel-tag tag-low">低置信频道</span>' : '');
    $('topChannelReason').textContent = buildReason(profile, best.channel, best);

    // 频道来源标识：避免用户误以为是官方 QQ 频道数据
    const sourceEl = $('channelSource');
    if (isDiscovery) {
        const isPending = lastResult.pendingChannel && lastResult.pendingChannel.name === best.channel.name;
        sourceEl.innerHTML = `<span class="badge-ai">AI Discovery</span>` +
            (isPending ? '<span class="badge-pending">待人工审核</span>' : '');
    } else {
        sourceEl.innerHTML = '<span style="font-size:11px;color:#9A9EA6;">本地频道库</span>';
    }

    // 匹配度数字 + 进度条 + 匹配等级（80+ 强匹配 / 60~79 一般匹配）
    const level = matchLevel(best.total);
    $('topScore').textContent = best.total + '%';
    const levelEl = $('scoreLevel');
    levelEl.textContent = level;
    levelEl.className = 'score-level ' + (level === '强匹配' ? 'strong' : 'normal');
    requestAnimationFrame(() => { $('topScoreBar').style.width = best.total + '%'; });

    // 次推荐频道 + 更多备选（候选列表第 2 名起）
    renderRunnerUps(matchResult);

    // 匹配依据（轻量调试信息：让产品经理知道“为什么推荐它”）
    const d = best.detail;
    const lines = [
        profile.game
            ? `游戏过滤：✓ ${profile.game}（候选 ${matchResult.candidateCount} 个，已排除其他游戏）`
            : `游戏过滤：未识别到游戏，全库 ${getAllChannels().length} 个频道参与排序`,
        `意图匹配（权重 ${WEIGHTS.intent}%）：+${d.intentScore}${d.intentHit && profile.intent ? `（${profile.intent}）` : ''}`,
        `标签重合（权重 ${WEIGHTS.tag}%）：+${d.tagScore}（命中 ${d.overlap.length} 个标签）`,
        `阶段匹配（权重 ${WEIGHTS.stage}%）：+${d.stageScore}${d.stageHit && profile.stage ? `（${profile.stage}）` : ''}`,
        `主题匹配（权重 ${WEIGHTS.topic}%）：+${d.topicScore}`,
        matchResult.appliedRules > 0 ? `Bad Case 规则：已排除 ${matchResult.appliedRules} 个历史误判频道` : null,
        best.lowConfidence ? 'Bad Case 降权：该频道被多次反馈错误，已标记为低置信频道' : null,
        `最终得分：${best.total}（${level}）`
    ].filter(Boolean);
    $('scoreDetail').innerHTML = lines.map(l => `<div class="score-detail-item">${escapeHTML(l)}</div>`).join('');

    // AI Discovery 扩充依据 + 人工审核（仅 AI 发现频道展示）
    renderDiscoveryEvidence(best.channel);
    renderReviewBar(best.channel);

    $('resultCard').classList.remove('hidden');
    $('resultCard').classList.add('fade-in');
}

/** 次推荐频道（第 2 名卡片）+ 更多备选（第 3 名起折叠） */
function renderRunnerUps(matchResult) {
    const rest = (matchResult.candidates || []).slice(1);
    const runner = $('runnerUp');
    const more = $('moreChannels');
    if (rest.length === 0) {
        runner.classList.add('hidden');
        more.classList.add('hidden');
        return;
    }
    const second = rest[0];
    runner.innerHTML = `
        <div>
            <div class="runner-up-name">${escapeHTML(second.channel.name)}</div>
            <div class="runner-up-tags">${escapeHTML(second.channel.tags.slice(0, 3).join('　'))}</div>
        </div>
        <div class="runner-up-right">
            <div class="runner-up-score">${second.total}%</div>
            <div class="runner-up-source">${second.channel.source === 'AI Discovery' ? 'AI Discovery' : '本地频道库'}</div>
        </div>`;
    runner.classList.remove('hidden');

    const others = rest.slice(1);
    if (others.length === 0) {
        more.classList.add('hidden');
        return;
    }
    more.innerHTML = `<details>
        <summary>更多备选频道（${others.length}）</summary>
        ${others.map(c => `<div class="more-item"><span>${escapeHTML(c.channel.name)}</span><b>${c.total}%</b></div>`).join('')}
    </details>`;
    more.classList.remove('hidden');
}

/** 扩充依据（右侧边栏「频道扩充记录」）：画像来源 / 主题 / 来源数量 / 置信度 */
function renderDiscoveryEvidence(channel) {
    const box = $('discoveryEvidence');
    const empty = $('expansionBody');
    if (channel.source !== 'AI Discovery') {
        // 本地频道推荐：边栏恢复空状态
        box.classList.add('hidden');
        empty.classList.remove('hidden');
        return;
    }
    empty.classList.add('hidden');
    const isPending = lastResult.pendingChannel && lastResult.pendingChannel.name === channel.name;
    $('expansionNotice').textContent = isPending
        ? '✓ 已通过 AI Discovery 扩充 1 个频道画像'
        : '✓ 该频道画像来自 AI Discovery 扩展频道库';
    $('evidenceName').textContent = channel.name;
    $('evidenceTopics').textContent = (channel.topics || channel.tags.slice(0, 4)).join(' / ');
    $('evidenceSourceCount').textContent = (channel.sources || []).length || 0;
    $('evidenceConfidence').textContent = Math.round((channel.confidence || 0) * 100) + '%';
    $('evidenceSources').innerHTML = (channel.sources || [])
        .map(s => `<div class="evidence-source-item">${escapeHTML(s)}</div>`).join('');
    $('evidenceSources').classList.add('hidden');
    $('toggleEvidence').textContent = '查看发现依据 ›';
    box.classList.remove('hidden');
}

/** Bad Case 边栏面板：展示本机反馈记录（默认最多 2 条，可查看全部） */
let showAllBadcases = false;
function renderBadcasePanel() {
    const panel = $('badcasePanel');
    const data = loadFeedback();
    const list = (data.bad || []).slice().reverse();
    if (list.length === 0) {
        panel.innerHTML = '<div class="side-empty">📄<br>暂无 Bad Case 记录<br><small>反馈的问题将帮助 AI 持续优化推荐效果</small></div>';
        $('toggleBadcaseList').style.display = 'none';
        return;
    }
    const shown = showAllBadcases ? list : list.slice(0, 2);
    panel.innerHTML = shown.map(b => `
        <div class="badcase-item">
            <b>${escapeHTML(b.reason)}</b>｜游戏：${escapeHTML(b.game || '未识别')}<br>
            频道：${escapeHTML(b.wrongChannel || b.recommended || '—')}<br>
            <small>${escapeHTML((b.input || '').slice(0, 30))}${(b.input || '').length > 30 ? '…' : ''}</small>
        </div>`).join('');
    const link = $('toggleBadcaseList');
    link.style.display = list.length > 2 ? '' : 'none';
    link.textContent = showAllBadcases ? '收起 ›' : '查看全部 ›';
}

/** 人工审核栏：AI 建议 → 人工确认（仅待审核频道展示） */
function renderReviewBar(channel) {
    const bar = $('reviewBar');
    const isPending = channel.source === 'AI Discovery' &&
        lastResult.pendingChannel && lastResult.pendingChannel.name === channel.name;
    if (!isPending) {
        bar.classList.add('hidden');
        return;
    }
    $('reviewStatus').textContent = 'AI 自动发现 · 待人工审核';
    bar.classList.remove('hidden');
    bar.classList.add('fade-in');
}

/* 渲染无结果状态（无该游戏频道 / 低于最低阈值） */
function renderNoResult(matchResult) {
    const profile = lastResult.profile;
    $('resultContent').classList.add('hidden');
    const box = $('noResult');
    box.classList.remove('hidden');

    if (matchResult.status === 'no_channel') {
        $('noResultTitle').textContent = `当前暂无合适的「${profile.game}」频道`;
        $('noResultDesc').textContent = '当前频道库中暂无该游戏相关频道，不会为你推荐其他游戏的频道。';
        $('noResultMeta').innerHTML =
            `<span>识别游戏：<b>${escapeHTML(profile.game)}</b></span>` +
            `<span>候选频道：<b>0</b></span>` +
            `<span>建议：扩充该游戏频道</span>`;
    } else if (matchResult.status === 'discovery_failed') {
        // Web 信息不足：拒绝生成频道画像（避免 AI 幻觉）
        $('noResultTitle').textContent = '暂未找到足够可靠的公开信息，无法生成频道画像';
        $('noResultDesc').textContent =
            `AI Discovery 未检索到与「${profile.game}」相关的可靠公开社区信息。为避免生成不可靠内容，本次不创建频道画像，也不会推荐其他游戏的频道。`;
        $('noResultMeta').innerHTML =
            `<span>识别游戏：<b>${escapeHTML(profile.game)}</b></span>` +
            `<span>候选频道：<b>0</b></span>` +
            `<span>建议：人工补充该游戏的频道画像</span>`;
    } else {
        // below_threshold：有候选但最高分未达阈值，不强行推荐
        const threshold = profile.game ? MATCH_THRESHOLD : STRONG_THRESHOLD;
        $('noResultTitle').textContent = '当前暂无高度匹配频道';
        $('noResultDesc').textContent =
            `候选频道中最高匹配度为 ${matchResult.best.total}%，低于最低推荐阈值 ${threshold}%` +
            `${profile.game ? '' : '（未识别到具体游戏，置信度要求更高）'}，为避免打扰不进行推荐。`;
        $('noResultMeta').innerHTML =
            `<span>识别游戏：<b>${escapeHTML(profile.game || '未识别')}</b></span>` +
            `<span>最高候选：<b>${escapeHTML(matchResult.best.channel.name)}（${matchResult.best.total}%）</b></span>` +
            `<span>建议：补充游戏名称或具体需求后重新匹配</span>`;
    }

    $('resultCard').classList.remove('hidden');
    $('resultCard').classList.add('fade-in');
}

/* 主流程：开始智能匹配 */
async function startMatch() {
    if (isAnalyzing) return;

    const text = $('userInput').value.trim();
    if (!text) {
        // 空输入提示
        $('userInput').classList.add('input-error');
        $('userInput').placeholder = '请先输入内容，例如：有没有人一起玩逆水寒手游？';
        $('userInput').focus();
        setTimeout(() => $('userInput').classList.remove('input-error'), 1600);
        return;
    }

    isAnalyzing = true;
    const btn = $('matchBtn');
    btn.disabled = true;
    btn.textContent = 'AI 分析中…';

    // 重置结果区
    ['resultCard', 'feedbackCard', 'profileBox', 'engineNotice', 'discoveryLog', 'discoveryEvidence',
        'reviewBar', 'runnerUp', 'moreChannels']
        .forEach(id => $(id).classList.add('hidden'));
    $('discoveryLog').innerHTML = '';
    $('expansionBody').classList.remove('hidden'); // 边栏扩充记录恢复空状态（由渲染逻辑决定）
    resetFeedback();
    $('analysisCard').classList.remove('hidden');
    $('analysisCard').classList.add('fade-in');

    // 1) 意图理解：真实 AI 模式优先调用 LLM；任何失败自动回退本地规则，
    //    保证无 API Key / 网络异常时 Demo 始终可以完整演示。
    let profile;
    let engineName = '本地规则';
    const llmReady = aiConfig.mode === 'llm' && aiConfig.apiKey && aiConfig.baseUrl && aiConfig.model;
    if (aiConfig.mode === 'llm' && !llmReady) {
        const notice = $('engineNotice');
        notice.textContent = '真实 AI 模式配置不完整（缺少 API Key / Base URL / 模型名称），已使用本地 Demo 模式。';
        notice.classList.remove('hidden');
    }
    if (llmReady) {
        $('stepStatus').textContent = '正在调用 AI 模型理解需求……';
        try {
            const raw = await callLLM(aiConfig, buildAnalysisPrompt(text));
            profile = normalizeLLMProfile(raw);
            engineName = PROVIDERS[aiConfig.provider].name;
        } catch (err) {
            profile = analyzeInput(text);
            const notice = $('engineNotice');
            notice.textContent = '模型调用失败，已自动切换至本地 Demo 模式。';
            notice.classList.remove('hidden');
            notice.classList.add('fade-in');
        }
    } else {
        profile = analyzeInput(text);
    }
    $('profileEngine').textContent = `（理解层：${engineName}）`;

    // 2) 两阶段匹配：游戏硬过滤 → 候选排序 → 阈值判定
    const matchResult = matchChannels(profile);
    const coverage = checkChannelCoverage(profile);
    lastResult = { input: text, profile, matchResult, coverage, pendingChannel: null, discoveryResult: null };

    // 3) 分析动画（0.8~1.5s）
    await runStepsAnimation(profile, matchResult);

    // 4) 频道空缺检测：覆盖不足 / 游戏不存在 → 触发 AI Discovery（有合适结果则不触发）
    renderProfile(profile);
    if (matchResult.status === 'ok') {
        logDiscovery('查询频道库 ✓ 频道库已有高匹配结果，无需扩充。');
        renderResult(matchResult.best, matchResult);
    } else if (coverage.shouldExpand) {
        const discovery = await runDiscoveryWithLog(profile, coverage);
        lastResult.discoveryResult = discovery;
        if (discovery.ok) {
            // 生成频道画像（待人工审核），临时参与重新匹配
            lastResult.pendingChannel = discovery.channel;
            const reMatch = matchChannels(profile, discovery.channel);
            lastResult.matchResult = reMatch;
            if (reMatch.status === 'ok') {
                logDiscovery(`重新匹配 ✓ 已通过 AI Discovery 扩充 1 个频道画像（Demo 画像，非真实 QQ 频道）。`);
                renderResult(reMatch.best, reMatch);
            } else {
                renderNoResult(reMatch);
            }
        } else {
            // Web 信息不足：拒绝生成频道画像，避免 AI 幻觉
            renderNoResult({ status: 'discovery_failed', best: matchResult.best, candidateCount: matchResult.candidateCount });
        }
    } else {
        renderNoResult(matchResult);
    }
    $('feedbackCard').classList.remove('hidden');
    $('feedbackCard').classList.add('fade-in');

    btn.disabled = false;
    btn.textContent = '重新匹配';
    isAnalyzing = false;
}

/* ==================== 用户反馈 & Bad Case ==================== */

const FEEDBACK_KEY = 'qqgame_demo_feedback'; // localStorage 键名

function loadFeedback() {
    try { return JSON.parse(localStorage.getItem(FEEDBACK_KEY)) || { good: 0, bad: [] }; }
    catch { return { good: 0, bad: [] }; }
}
function saveFeedback(data) {
    localStorage.setItem(FEEDBACK_KEY, JSON.stringify(data));
}

function resetFeedback() {
    $('feedbackAsk').classList.remove('hidden');
    $('badcaseForm').classList.add('hidden');
    $('feedbackDone').classList.add('hidden');
    document.querySelectorAll('input[name="badcase"]').forEach(r => { r.checked = false; });
}

function showFeedbackDone(message) {
    $('feedbackAsk').classList.add('hidden');
    $('badcaseForm').classList.add('hidden');
    const done = $('feedbackDone');
    done.textContent = message;
    done.classList.remove('hidden');
    done.classList.add('fade-in');
    renderLocalFeedback();
    renderBadcasePanel();
    renderDiscoveryStats();
}

/** 本机累计反馈展示（写入评估区，体现数据回流） */
function renderLocalFeedback() {
    const data = loadFeedback();
    if (data.good === 0 && data.bad.length === 0) {
        $('evalLocal').textContent = '';
        return;
    }
    const badCount = {};
    data.bad.forEach(b => { badCount[b.reason] = (badCount[b.reason] || 0) + 1; });
    const badText = Object.entries(badCount).map(([k, v]) => `${k} ${v} 次`).join('，');
    $('evalLocal').textContent =
        `本机累计反馈：匹配准确 ${data.good} 次，Bad Case ${data.bad.length} 条${badText ? '（' + badText + '）' : ''}。`;
}

/* ==================== AI Discovery：审核 / 统计 / 清空 ==================== */

/** AI Discovery 统计（仅 Demo 当前运行数据 + 本机持久化数据） */
function renderDiscoveryStats() {
    const data = loadFeedback();
    const games = new Set([...discoveredChannels.map(c => c.game), ...sessionStats.games]);
    $('discoveryStats').innerHTML =
        `<div class="ds-row"><span>发现游戏</span><b>${games.size}</b></div>` +
        `<div class="ds-row"><span>扩充频道画像</span><b>${initialDiscoveredCount + sessionStats.generated}</b></div>` +
        `<div class="ds-row"><span>用户确认加入</span><b>${discoveredChannels.length}</b></div>` +
        `<div class="ds-row"><span>Bad Case</span><b>${data.bad.length}</b></div>`;
}

/** 人工审核：加入频道库（持久化到 localStorage） */
function approvePendingChannel() {
    const ch = lastResult && lastResult.pendingChannel;
    if (!ch) return;
    discoveredChannels.push(ch);
    saveDiscoveredChannels();
    lastResult.pendingChannel = null;
    // 更新审核栏与来源标识
    const status = $('reviewStatus');
    status.textContent = '已加入 AI 扩充频道库（仅保存在本机浏览器）';
    status.className = 'badge-approved';
    $('btnApproveChannel').classList.add('hidden');
    $('btnIgnoreChannel').classList.add('hidden');
    $('channelSource').innerHTML = '<span class="badge-ai">AI Discovery</span>';
    logDiscovery(`人工审核 ✓ 「${escapeHTML(ch.name)}」已加入扩展频道库，后续推荐将直接使用。`);
    renderDiscoveryStats();
}

/** 人工审核：忽略（不入库，重新匹配） */
function ignorePendingChannel() {
    if (!lastResult || !lastResult.pendingChannel) return;
    const ignored = lastResult.pendingChannel;
    lastResult.pendingChannel = null;
    $('reviewBar').classList.add('hidden');
    $('discoveryEvidence').classList.add('hidden');
    logDiscovery(`人工审核 ✓ 已忽略「${escapeHTML(ignored.name)}」，不加入频道库。`);
    // 不含被忽略候选，重新匹配
    const reMatch = matchChannels(lastResult.profile);
    lastResult.matchResult = reMatch;
    if (reMatch.status === 'ok') {
        renderResult(reMatch.best, reMatch);
    } else {
        renderNoResult(reMatch);
    }
    renderDiscoveryStats();
}

/* ==================== 事件绑定 ==================== */

document.addEventListener('DOMContentLoaded', () => {
    renderExamples();
    renderLocalFeedback();

    $('matchBtn').addEventListener('click', startMatch);

    // 匹配准确
    $('btnGood').addEventListener('click', () => {
        const data = loadFeedback();
        data.good += 1;
        saveFeedback(data);
        showFeedbackDone('感谢反馈！正向样本已记录，将用于验证匹配策略的有效性。');
    });

    // 匹配不准确 -> 展开 Bad Case 表单
    $('btnBad').addEventListener('click', () => {
        $('badcaseForm').classList.remove('hidden');
        $('badcaseForm').classList.add('fade-in');
    });

    // 提交 Bad Case
    $('badcaseSubmit').addEventListener('click', () => {
        const selected = document.querySelector('input[name="badcase"]:checked');
        if (!selected) {
            $('badcaseSubmit').textContent = '请先选择错误类型';
            setTimeout(() => { $('badcaseSubmit').textContent = '提交反馈'; }, 1500);
            return;
        }
        const data = loadFeedback();
        const best = lastResult && lastResult.matchResult ? lastResult.matchResult.best : null;
        data.bad.push({
            reason: selected.value,
            input: lastResult ? lastResult.input : '',
            game: lastResult && lastResult.profile ? lastResult.profile.game : null,
            wrongChannel: best ? best.channel.name : null,
            recommended: best ? best.channel.name : '',
            time: new Date().toISOString()
        });
        saveFeedback(data);
        // 「游戏识别错误 / 推荐频道不匹配」会更新匹配策略，直接影响下一次推荐：
        // 本地频道 → 排除；AI 发现频道 → 降权（多次反馈标记为低置信频道）
        const willUpdateStrategy = selected.value === '游戏识别错误' || selected.value === '推荐频道不匹配';
        const isDiscoveryChannel = best && best.channel.source === 'AI Discovery';
        showFeedbackDone(
            willUpdateStrategy && isDiscoveryChannel
                ? '已记录 Bad Case，并降低该 AI 频道画像的推荐权重；多次反馈将标记为低置信频道。'
                : willUpdateStrategy
                    ? '已记录 Bad Case，并更新本地匹配策略：该频道将在同类需求的后续推荐中被排除。'
                    : '已记录 Bad Case，可用于后续模型与匹配策略优化。');
    });

    // 评估区：查看 Bad Case 明细（模拟数据）
    $('toggleBadcase').addEventListener('click', () => {
        const panel = $('evalBadcase');
        const isHidden = panel.classList.toggle('hidden');
        $('toggleBadcase').textContent = isHidden ? '查看 Bad Case ›' : '收起 Bad Case ›';
    });

    initSettings();

    // ---------- 输入区：字数统计 + 清空 ----------
    $('userInput').addEventListener('input', () => {
        $('charCount').textContent = $('userInput').value.length;
    });
    $('clearInput').addEventListener('click', () => {
        $('userInput').value = '';
        $('charCount').textContent = '0';
        $('userInput').focus();
    });

    // ---------- AI Discovery 相关绑定 ----------
    renderDiscoveryStats();
    renderBadcasePanel();
    $('toggleBadcaseList').addEventListener('click', () => {
        showAllBadcases = !showAllBadcases;
        renderBadcasePanel();
    });

    // 展开 / 收起「发现依据」
    $('toggleEvidence').addEventListener('click', () => {
        const s = $('evidenceSources');
        const hidden = s.classList.toggle('hidden');
        $('toggleEvidence').textContent = hidden ? '查看发现依据 ›' : '收起发现依据 ›';
    });

    // 人工审核：加入频道库 / 忽略
    $('btnApproveChannel').addEventListener('click', approvePendingChannel);
    $('btnIgnoreChannel').addEventListener('click', ignorePendingChannel);

    // 清空 AI 扩充频道（二次确认，不影响原始频道库）
    let clearArmed = false;
    $('clearDiscovered').addEventListener('click', () => {
        if (!clearArmed) {
            clearArmed = true;
            $('clearDiscovered').textContent = '确认清空？（不影响本地频道库）';
            setTimeout(() => {
                clearArmed = false;
                $('clearDiscovered').textContent = '清空 AI 扩充频道';
            }, 2500);
            return;
        }
        discoveredChannels = [];
        localStorage.removeItem(DISCOVERED_KEY);
        clearArmed = false;
        $('clearDiscovered').textContent = '已清空';
        setTimeout(() => { $('clearDiscovered').textContent = '清空 AI 扩充频道'; }, 1500);
        renderDiscoveryStats();
    });
});

/* ==================== AI 模型配置（设置抽屉） ==================== */

/** 更新顶栏 AI 状态标识 */
function updateAiStatus() {
    const badge = $('aiStatus');
    if (aiConfig.mode === 'llm') {
        badge.textContent = '当前模型：' + PROVIDERS[aiConfig.provider].name + ' ▾';
        badge.classList.add('llm');
    } else {
        badge.textContent = '当前模型：本地 Demo ▾';
        badge.classList.remove('llm');
    }
}

/** 根据所选提供商回填 Base URL 预设与模型名候选（均可继续手动修改） */
function applyProviderPreset() {
    const p = PROVIDERS[$('cfgProvider').value];
    $('cfgBaseUrl').value = p.baseUrl;
    $('cfgModel').value = p.models[0] || '';
    $('modelPresets').innerHTML = p.models.map(m => `<option value="${m}">`).join('');
}

function openDrawer() {
    // 将当前运行配置回填到表单
    document.querySelector(`input[name="aiMode"][value="${aiConfig.mode}"]`).checked = true;
    $('cfgProvider').value = aiConfig.provider;
    $('cfgApiKey').value = aiConfig.apiKey;
    $('cfgBaseUrl').value = aiConfig.baseUrl || PROVIDERS[aiConfig.provider].baseUrl;
    $('cfgModel').value = aiConfig.model;
    $('modelPresets').innerHTML = PROVIDERS[$('cfgProvider').value].models.map(m => `<option value="${m}">`).join('');
    $('testResult').classList.add('hidden');
    $('drawerMask').classList.remove('hidden');
    $('settingsDrawer').classList.add('open');
}

function closeDrawer() {
    $('drawerMask').classList.add('hidden');
    $('settingsDrawer').classList.remove('open');
}

/** 测试连接：用真实分析 Prompt 请求一次，并校验返回 JSON 结构 */
async function testConnection() {
    const result = $('testResult');
    const cfg = readSettingsForm();
    if (!cfg.apiKey || !cfg.baseUrl || !cfg.model) {
        result.className = 'test-result fail';
        result.innerHTML = '✕ 请先完整填写 API Key、Base URL 与模型名称';
        result.classList.remove('hidden');
        return;
    }

    const btn = $('testConnBtn');
    btn.disabled = true;
    result.className = 'test-result testing';
    result.textContent = '正在连接模型……';
    result.classList.remove('hidden');

    try {
        const raw = await callLLM(cfg, buildAnalysisPrompt('测试：有没有人一起玩游戏？'));
        // JSON 结构校验：必须包含四个核心字段
        const ok = raw && typeof raw === 'object' &&
            'game' in raw && 'intent' in raw && 'stage' in raw && 'tags' in raw;
        if (!ok) throw new Error('JSON 结构不完整');
        result.className = 'test-result ok';
        result.innerHTML = '✓ API 连接成功<br>✓ 模型响应正常<br>✓ JSON 格式校验通过';
    } catch (err) {
        // 根据错误类型给出可定位的失败原因
        let reason = '网络或跨域限制（部分模型服务不允许浏览器直接调用，需代理中转）';
        if (err.status === 401 || err.status === 403) reason = 'API Key 无效';
        else if (err.status === 404) reason = 'Base URL 错误';
        else if (err.status === 400 || err.status === 422) reason = '模型名称错误或请求参数不兼容';
        else if (err.message && err.message.includes('JSON')) reason = '模型未按约定输出 JSON，可更换模型或调整 Prompt';
        result.className = 'test-result fail';
        result.innerHTML = '✕ 连接失败<br>可能原因：' + reason;
    } finally {
        btn.disabled = false;
    }
}

function readSettingsForm() {
    return {
        mode: document.querySelector('input[name="aiMode"]:checked').value,
        provider: $('cfgProvider').value,
        apiKey: $('cfgApiKey').value.trim(),
        baseUrl: $('cfgBaseUrl').value.trim(),
        model: $('cfgModel').value.trim()
    };
}

function initSettings() {
    // 恢复「记住配置」保存的内容（仅当用户曾主动勾选）
    try {
        const saved = JSON.parse(localStorage.getItem(AI_CONFIG_KEY));
        if (saved && saved.mode) {
            aiConfig = saved;
            $('cfgRemember').checked = true;
        }
    } catch { /* 忽略损坏数据 */ }
    updateAiStatus();
    applyProviderPreset();

    $('openSettings').addEventListener('click', openDrawer);
    $('closeSettings').addEventListener('click', closeDrawer);
    $('drawerMask').addEventListener('click', closeDrawer);
    $('cfgProvider').addEventListener('change', applyProviderPreset);
    $('testConnBtn').addEventListener('click', testConnection);

    $('saveSettings').addEventListener('click', () => {
        aiConfig = readSettingsForm();
        if ($('cfgRemember').checked) {
            // 用户主动选择本地保存（含 API Key），页面已有明确提示
            localStorage.setItem(AI_CONFIG_KEY, JSON.stringify(aiConfig));
        } else {
            // 默认不落盘，配置仅保留在当前页面内存
            localStorage.removeItem(AI_CONFIG_KEY);
        }
        updateAiStatus();
        closeDrawer();
    });
}
