const express = require('express');
const fs = require('fs');
const path = require('path');

const app = express();
const PORT = process.env.PORT || 8099;
const API_BASE = process.env.API_BASE || 'http://168.107.1.220:83/v1';
const API_KEY = process.env.API_KEY || 'sk-CvfodVnfQtcE5bshSmYEN5EqRriZCBTB';
const MODEL = process.env.MODEL || 'gpt-5-5';
const DATA_DIR = path.join(__dirname, 'data');
const KB_FILE = path.join(DATA_DIR, 'knowledge.json');
const QUESTION_BANK_FILE = path.join(DATA_DIR, 'question_bank.json');

const QUESTION_TYPES = [
  '组织活动类','观点认知类','现象认知类','人际关系类','应急应变类','职业认知类','岗位匹配类','漫画寓言类','材料分析类','综合分析类','其他类'
];
const SUBTYPE_EXAMPLES = {
  '组织活动类': ['组织会议','组织群众活动','组织宣传活动','组织调研','组织培训','组织比赛','接待安排'],
  '观点认知类': ['对一句话的理解','对价值观的理解','对干部品质的理解','对工作态度的理解'],
  '现象认知类': ['政治现象认知','经济现象认知','社会民生认知','基层治理认知','科技创新认知','生态文明认知','公共服务认知'],
  '人际关系类': ['和同事的关系','和领导的关系','和群众的关系','和下属的关系','多方矛盾协调'],
  '应急应变类': ['现场突发','舆情投诉','群众冲突','工作失误补救','资源不足'],
  '职业认知类': ['岗位理解','基层苦累','服务意识','纪律规矩','成长规划']
};
const MATERIAL_GROUPS = ['答题方法','名言金句','好做法','名人事迹','政策表达','结构模板','典型案例','岗位落实','易错提醒'];
const LEGACY_MATERIAL_CATEGORIES = new Set(['名言金句','好做法','答题规律','结构模板','政策表达','名人事迹','典型案例','岗位落实','题型打法','常见问题']);

const SYSTEM_PROMPT = `你是“事业编面试逐字稿”专项教练。你的任务是根据用户输入的题目、素材或已有草稿，输出适合事业单位/事业编面试的高质量逐字稿。

核心风格：
1. 反模板：避免“首先其次最后”“我认为”“各位考官”等机械套话，语言要像真实考场表达，结构清晰但不僵硬。
2. 有高度：能上升到国家、政府、基层治理、公共服务、人民立场、中国式现代化、治理效能等层面，但不要空喊口号。
3. 有落点：必须说到未来自己怎么做，尤其是如果成为事业单位工作人员，将如何在岗位中落实。
4. 有细节：结合题干场景，给出具体做法、沟通方法、执行步骤、风险意识、复盘改进。
5. 可学习：回答后附“规律提炼”，帮助用户总结这类题怎么想、怎么答、怎么迁移。

输出要求：
- 先给“逐字稿”，口语化、可直接背诵，字数根据题目复杂度控制在约700-1200字。
- 再给“本题答题规律”。
- 再给“可沉淀素材”，提炼名言金句、好做法、名人事迹、典型案例、政策表达、题型打法、可复用结构。
- 如果知识库中有相关内容，要自然融入，但不要硬塞。
- 禁止编造具体政策名称、人物事迹细节；不确定就用稳妥概括表达。`;

function ensureData() {
  fs.mkdirSync(DATA_DIR, { recursive: true });
  if (!fs.existsSync(KB_FILE)) fs.writeFileSync(KB_FILE, JSON.stringify({ categories: [] }, null, 2));
  if (!fs.existsSync(QUESTION_BANK_FILE)) fs.writeFileSync(QUESTION_BANK_FILE, JSON.stringify({ types: [] }, null, 2));
}
function readJson(file, fallback) {
  ensureData();
  try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return fallback; }
}
function writeJson(file, data) {
  ensureData();
  fs.writeFileSync(file, JSON.stringify(data, null, 2));
}
function getOrCreate(arr, name, extra = {}) {
  let found = arr.find(x => x.name === name);
  if (!found) arr.push(found = { name, ...extra });
  return found;
}
function normalizeQuestionType(name) {
  if (!name) return '其他类';
  const n = String(name).trim();
  if (QUESTION_TYPES.includes(n)) return n;
  if (['政治理论类','经济发展类','基层治理类','公共服务类','社会民生类','科技创新类','生态文明类'].includes(n)) return '现象认知类';
  if (['计划组织类'].includes(n)) return '组织活动类';
  return n.endsWith('类') ? n : '其他类';
}
function inferLegacySubType(catName, sectionName) {
  const text = `${catName || ''}${sectionName || ''}`;
  if (/同事/.test(text)) return '和同事的关系';
  if (/群众/.test(text)) return /活动|宣传|会议|调研|培训/.test(text) ? '组织群众活动' : '和群众的关系';
  if (/领导/.test(text)) return '和领导的关系';
  if (/会议|开会/.test(text)) return '组织会议';
  if (/宣传/.test(text)) return '组织宣传活动';
  if (/调研/.test(text)) return '组织调研';
  if (/经济|就业|产业|消费/.test(text)) return '经济现象认知';
  if (/基层|治理/.test(text)) return '基层治理认知';
  if (/科技|人工智能|技术/.test(text)) return '科技创新认知';
  if (/生态|环保/.test(text)) return '生态文明认知';
  if (/民生|教育|医疗|养老/.test(text)) return '社会民生认知';
  return '通用场景';
}
function inferLegacyType(catName, sectionName) {
  const text = `${catName || ''}${sectionName || ''}`;
  if (/组织|活动|会议|宣传|调研|培训/.test(text)) return '组织活动类';
  if (/同事|领导|群众|关系|矛盾|沟通/.test(text)) return '人际关系类';
  if (/观点|理解|启示|名言|品质|价值观/.test(text)) return '观点认知类';
  if (/应急|突发|投诉|舆情|冲突/.test(text)) return '应急应变类';
  if (/岗位|职业|基层苦累|服务意识/.test(text)) return '职业认知类';
  return '现象认知类';
}
function migrateKb(kb) {
  if (!kb || !Array.isArray(kb.categories)) return { categories: [] };
  const migrated = { categories: [] };
  for (const cat of kb.categories) {
    // 目标结构：一级=题型大类；二级=细分场景；三级=素材类型/答题方法。
    if (QUESTION_TYPES.includes(cat.name) || !LEGACY_MATERIAL_CATEGORIES.has(cat.name)) {
      const typeName = normalizeQuestionType(cat.name);
      const targetCat = getOrCreate(migrated.categories, typeName, { description: cat.description || '', sections: [] });
      for (const sec of cat.sections || []) {
        const subName = sec.name && !LEGACY_MATERIAL_CATEGORIES.has(sec.name) ? sec.name : inferLegacySubType(cat.name, sec.name);
        const targetSec = getOrCreate(targetCat.sections, subName, { description: sec.description || '', groups: [] });
        for (const g of sec.groups || []) {
          const groupName = MATERIAL_GROUPS.includes(g.name) ? g.name : (['答题规律','题型打法'].includes(g.name) ? '答题方法' : (LEGACY_MATERIAL_CATEGORIES.has(g.name) ? g.name : '答题方法'));
          const targetGroup = getOrCreate(targetSec.groups, groupName, { description: g.description || '', items: [] });
          targetGroup.items.push(...(g.items || []));
        }
      }
      if (Array.isArray(cat.items) && cat.items.length) {
        const targetSec = getOrCreate(targetCat.sections, '通用场景', { description: '', groups: [] });
        const targetGroup = getOrCreate(targetSec.groups, '答题方法', { description: '', items: [] });
        targetGroup.items.push(...cat.items);
      }
    } else {
      const legacyGroup = cat.name === '答题规律' ? '答题方法' : cat.name;
      for (const sec of cat.sections || [{ name: '通用场景', groups: [{ name: legacyGroup, items: cat.items || [] }] }]) {
        const typeName = inferLegacyType(cat.name, sec.name);
        const subName = inferLegacySubType(cat.name, sec.name);
        const targetCat = getOrCreate(migrated.categories, typeName, { description: `${typeName}相关积累`, sections: [] });
        const targetSec = getOrCreate(targetCat.sections, subName, { description: '', groups: [] });
        for (const g of sec.groups || []) {
          const groupName = legacyGroup;
          const targetGroup = getOrCreate(targetSec.groups, groupName, { description: g.description || '', items: [] });
          targetGroup.items.push(...(g.items || []));
        }
      }
    }
  }
  // 去重
  for (const c of migrated.categories) for (const s of c.sections || []) for (const g of s.groups || []) {
    const seen = new Set();
    g.items = (g.items || []).filter(i => {
      const key = `${i.title || ''}|${i.content || ''}`;
      if (seen.has(key)) return false;
      seen.add(key); return true;
    });
  }
  return migrated;
}
function readKb() { const kb = migrateKb(readJson(KB_FILE, { categories: [] })); writeJson(KB_FILE, kb); return kb; }
function writeKb(kb) { writeJson(KB_FILE, migrateKb(kb)); }
function readQuestionBank() { return readJson(QUESTION_BANK_FILE, { types: [] }); }
function writeQuestionBank(qb) { writeJson(QUESTION_BANK_FILE, qb); }
function itemCountInCat(c) { return (c.sections||[]).reduce((a,s)=>a+(s.groups||[]).reduce((b,g)=>b+(g.items||[]).length,0),0); }
function flattenKbItems(kb, limit = 80) {
  const all = [];
  for (const c of kb.categories || []) for (const s of c.sections || []) for (const g of s.groups || []) for (const i of g.items || []) all.push({ c:c.name, s:s.name, g:g.name, ...i });
  return all.slice(-limit);
}
function kbText(kb) {
  const lines = [];
  for (const c of kb.categories || []) {
    lines.push(`【${c.name}】${c.description ? c.description : ''}`);
    for (const s of (c.sections || []).slice(-8)) {
      lines.push(`  <${s.name}>${s.description ? s.description : ''}`);
      for (const g of (s.groups || []).slice(-8)) {
        const items = (g.items || []).slice(-8).map(i => `    - [${g.name}/${i.type || '素材'}] ${i.title || ''}：${i.content || ''}`).join('\n');
        if (items) lines.push(items);
      }
    }
  }
  return lines.join('\n').slice(-14000);
}
function safeJsonSlice(text) {
  const m = text.match(/```json\s*([\s\S]*?)```/i) || text.match(/\{[\s\S]*\}/);
  if (!m) return null;
  const raw = m[1] || m[0];
  try { return JSON.parse(raw); } catch { return null; }
}
function normalizeTextForSimilarity(s) {
  return String(s || '').toLowerCase().replace(/[\s\p{P}\p{S}]/gu, '').replace(/[，。！？；：“”‘’、（）【】《》]/g, '');
}
function bigrams(s) {
  const t = normalizeTextForSimilarity(s);
  const set = new Set();
  if (t.length <= 1) { if (t) set.add(t); return set; }
  for (let i = 0; i < t.length - 1; i++) set.add(t.slice(i, i + 2));
  return set;
}
function similarity(a, b) {
  const A = bigrams(a), B = bigrams(b);
  if (!A.size || !B.size) return 0;
  let inter = 0;
  for (const x of A) if (B.has(x)) inter++;
  return inter / (A.size + B.size - inter);
}
function allQuestions(qb) {
  const out = [];
  for (const t of qb.types || []) for (const q of t.questions || []) out.push({ ...q, questionType: t.name });
  return out;
}
function questionPreview(q) {
  return {
    id: q.id,
    questionType: q.questionType,
    subType: q.subType || '',
    title: q.title || String(q.question || '').slice(0, 40),
    question: q.question || '',
    answer: q.answer || '',
    answers: q.answers || null,
    tags: q.tags || [],
    difficulty: q.difficulty || '',
    createdAt: q.createdAt,
    updatedAt: q.updatedAt
  };
}
function findSimilarQuestions(question, limit = 5) {
  const qb = readQuestionBank();
  return allQuestions(qb)
    .map(q => ({ ...q, similarity: similarity(question, q.question) }))
    .filter(q => q.similarity >= 0.45)
    .sort((a,b) => b.similarity - a.similarity)
    .slice(0, limit)
    .map(q => ({ ...questionPreview(q), similarity: Math.round(q.similarity * 100) }));
}
function searchQuestions(keyword = '', tags = '', limit = 80) {
  const words = String(keyword || '').trim().toLowerCase().split(/\s+/).filter(Boolean);
  const tagWords = String(tags || '').split(/[，,\s#]+/).map(x => x.trim().toLowerCase()).filter(Boolean);
  const scored = [];
  for (const q of allQuestions(readQuestionBank())) {
    const hay = [q.title, q.question, q.answer, q.subType, q.questionType, ...(q.tags || [])].join('\n').toLowerCase();
    const qtags = (q.tags || []).map(x => String(x).toLowerCase());
    const wordOk = !words.length || words.every(w => hay.includes(w));
    const tagOk = !tagWords.length || tagWords.every(t => qtags.some(x => x.includes(t)) || hay.includes(t));
    if (!wordOk || !tagOk) continue;
    let score = 0;
    for (const w of words) score += (String(q.title || '').toLowerCase().includes(w) ? 5 : 0) + (String(q.question || '').toLowerCase().includes(w) ? 3 : 0) + (hay.includes(w) ? 1 : 0);
    for (const t of tagWords) score += qtags.some(x => x.includes(t)) ? 6 : 1;
    scored.push({ ...questionPreview(q), score });
  }
  return scored.sort((a,b) => (b.score || 0) - (a.score || 0) || String(b.createdAt || '').localeCompare(String(a.createdAt || ''))).slice(0, limit);
}
async function callChat(messages, stream = false, temperature = 0.7) {
  const r = await fetch(`${API_BASE}/chat/completions`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}` },
    body: JSON.stringify({ model: MODEL, messages, stream, temperature })
  });
  if (!r.ok) throw new Error(`webchat2api ${r.status}: ${await r.text()}`);
  return r;
}
function findQuestionById(qb, id) {
  for (const t of qb.types || []) {
    const q = (t.questions || []).find(x => x.id === id);
    if (q) return { type: t, question: q };
  }
  return null;
}
function normalizeQuestionRecord(q) {
  if (!Array.isArray(q.answers)) {
    q.answers = [];
    if (q.answer) {
      q.answers.push({ id: q.id + '_a1', content: q.answer, source: '生成答案', createdAt: q.createdAt || new Date().toISOString() });
    }
  }
  if (!q.answer && q.answers.length) q.answer = q.answers[0].content;
  return q;
}
function saveQuestion(question, answer, meta, existingQuestionId = '') {
  const qb = readQuestionBank();
  if (existingQuestionId) {
    const found = findQuestionById(qb, existingQuestionId);
    if (found) {
      const q = normalizeQuestionRecord(found.question);
      const ans = { id: Date.now().toString(36) + Math.random().toString(36).slice(2,6), content: answer, source: '重新生成', createdAt: new Date().toISOString() };
      q.answers.unshift(ans);
      q.answer = ans.content;
      q.updatedAt = ans.createdAt;
      if (Array.isArray(meta?.tags)) q.tags = Array.from(new Set([...(q.tags || []), ...meta.tags]));
      writeQuestionBank(qb);
      return qb;
    }
  }
  const typeName = normalizeQuestionType(meta?.questionType || '其他类');
  const type = getOrCreate(qb.types, typeName, { description: meta?.typeReason || '', questions: [] });
  if (!type.description && meta?.typeReason) type.description = meta.typeReason;
  const id = Date.now().toString(36) + Math.random().toString(36).slice(2,6);
  type.questions.unshift({
    id,
    question,
    answer,
    answers: [{ id: id + '_a1', content: answer, source: '生成答案', createdAt: new Date().toISOString() }],
    reviews: [],
    title: meta?.title || question.slice(0, 40),
    tags: Array.isArray(meta?.tags) ? meta.tags : [],
    subType: meta?.subType || '',
    difficulty: meta?.difficulty || '',
    createdAt: new Date().toISOString()
  });
  writeQuestionBank(qb);
  return qb;
}
function mergeKb(additions) {
  if (!additions || !Array.isArray(additions.categories)) return false;
  const fresh = readKb();
  for (const nc of additions.categories) {
    if (!nc || !nc.name) continue;
    const cName = normalizeQuestionType(nc.name);
    const c = getOrCreate(fresh.categories, cName, { description: nc.description || '', sections: [] });
    if (!c.description && nc.description) c.description = nc.description;
    const sections = Array.isArray(nc.sections) ? nc.sections : [{ name: nc.section || '通用场景', description: '', groups: [{ name: nc.group || '答题方法', items: nc.items || [] }] }];
    for (const ns of sections) {
      if (!ns || !ns.name) continue;
      const s = getOrCreate(c.sections, ns.name, { description: ns.description || '', groups: [] });
      if (!s.description && ns.description) s.description = ns.description;
      const groups = Array.isArray(ns.groups) ? ns.groups : [{ name: ns.group || '答题方法', description: '', items: ns.items || [] }];
      for (const ng of groups) {
        if (!ng || !ng.name) continue;
        const groupName = MATERIAL_GROUPS.includes(ng.name) ? ng.name : (['答题规律','题型打法'].includes(ng.name) ? '答题方法' : (LEGACY_MATERIAL_CATEGORIES.has(ng.name) ? ng.name : '答题方法'));
        const g = getOrCreate(s.groups, groupName, { description: ng.description || '', items: [] });
        if (!g.description && ng.description) g.description = ng.description;
        for (const item of (ng.items || [])) {
          if (!item || !item.content) continue;
          const dup = g.items.some(x => x.content === item.content || (x.title === item.title && String(x.content).slice(0,30) === String(item.content).slice(0,30)));
          if (!dup) g.items.push({
            id: Date.now().toString(36) + Math.random().toString(36).slice(2,6),
            title: item.title || '素材',
            type: item.type || '素材',
            content: item.content,
            source: item.source || 'AI提取',
            createdAt: new Date().toISOString()
          });
        }
      }
    }
  }
  writeKb(fresh);
  return true;
}

app.use(express.json({ limit: '6mb' }));
app.use(express.static(path.join(__dirname, 'public')));

app.get('/api/kb', (req, res) => res.json(readKb()));
app.get('/api/questions', (req, res) => {
  const qb = readQuestionBank();
  let changed = false;
  for (const q of allQuestions(qb)) {
    const found = findQuestionById(qb, q.id);
    if (found) { normalizeQuestionRecord(found.question); changed = true; }
  }
  if (changed) writeQuestionBank(qb);
  res.json(qb);
});
app.post('/api/similar', (req, res) => {
  const question = String(req.body?.question || '').trim();
  if (!question) return res.json({ matches: [] });
  res.json({ matches: findSimilarQuestions(question) });
});
app.get('/api/questions/search', (req, res) => {
  const keyword = String(req.query?.keyword || '');
  const tags = String(req.query?.tags || '');
  res.json({ results: searchQuestions(keyword, tags), keyword, tags });
});

app.post('/api/kb/add', (req, res) => {
  const { category = '其他类', section = '通用场景', group = '答题方法', title = '新素材', content = '', type = '手动素材', source = '手动添加' } = req.body || {};
  const kb = readKb();
  const c = getOrCreate(kb.categories, normalizeQuestionType(category), { description: '', sections: [] });
  const s = getOrCreate(c.sections, section || '通用场景', { description: '', groups: [] });
  const groupName = MATERIAL_GROUPS.includes(group) ? group : '答题方法';
  const g = getOrCreate(s.groups, groupName, { description: '', items: [] });
  g.items.push({ id: Date.now().toString(36), title, content, type, source, createdAt: new Date().toISOString() });
  writeKb(kb);
  res.json(kb);
});

async function extractReviewKnowledge(question, review, userAnswer) {
  const messages = [
    { role: 'system', content: `你是事业编面试知识库整理员。请从AI点评中提炼考生可复用的好表达、改进方法、易错提醒。只输出JSON。知识库结构必须是：一级=题型大类，二级=细分场景，三级=答题方法/名言金句/好做法/岗位落实/易错提醒。格式：{"categories":[{"name":"题型大类","sections":[{"name":"细分场景","groups":[{"name":"三级内容类型","items":[{"title":"标题","type":"好做法/答题方法/易错提醒/名言金句/岗位落实","content":"精简内容","source":"来自AI点评"}]}]}]}]}` },
    { role: 'user', content: `题目：${question}\n\n考生作答：${userAnswer}\n\nAI点评：${review}\n\n题型大类可选：${QUESTION_TYPES.join('、')}。二级示例：${JSON.stringify(SUBTYPE_EXAMPLES)}。` }
  ];
  const r = await callChat(messages, false, 0.2);
  const jobj = await r.json();
  return safeJsonSlice(jobj.choices?.[0]?.message?.content || '');
}

app.post('/api/answer/improve', async (req, res) => {
  const questionId = String(req.body?.questionId || '').trim();
  const answerId = String(req.body?.answerId || '').trim();
  const instruction = String(req.body?.instruction || '').trim();
  if (!questionId || !instruction) return res.status(400).json({ error: '请提供题目ID和修改要求' });
  try {
    const qb = readQuestionBank();
    const found = findQuestionById(qb, questionId);
    if (!found) return res.status(404).json({ error: '未找到这道题' });
    const q = normalizeQuestionRecord(found.question);
    const baseAnswer = (answerId ? (q.answers || []).find(a => a.id === answerId) : null) || (q.answers || [])[0] || { content: q.answer || '' };
    const messages = [
      { role: 'system', content: `你是事业编面试逐字稿精修老师。请严格按照学生提出的修改要求，基于“原题”和“已有答案”改写出一版更好的完整逐字稿。保留原答案中可用的结构和亮点，但要真正落实学生要求。输出结构：先给“修改后逐字稿”，再给“本次修改说明”，最后给“可继续优化方向”。不要只点评，必须直接给改好的完整内容。` },
      { role: 'user', content: `原题：\n${q.question}\n\n题型：${found.type.name || ''} ${q.subType || ''}\n标签：${(q.tags || []).join('、')}\n\n已有答案：\n${baseAnswer.content || q.answer || ''}\n\n学生希望这样改：\n${instruction}\n\n请按学生要求改写已有答案。` }
    ];
    const r = await callChat(messages, false, 0.55);
    const jobj = await r.json();
    const improved = jobj.choices?.[0]?.message?.content || '';
    const ans = { id: Date.now().toString(36) + Math.random().toString(36).slice(2,6), content: improved, source: '按要求改进', instruction, baseAnswerId: baseAnswer.id || '', createdAt: new Date().toISOString() };
    q.answers.unshift(ans);
    q.answer = ans.content;
    q.updatedAt = ans.createdAt;
    writeQuestionBank(qb);
    res.json({ answer: improved, questionId, answerId: ans.id });
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

app.post('/api/review', async (req, res) => {
  const question = String(req.body?.question || '').trim();
  const userAnswer = String(req.body?.userAnswer || '').trim();
  const questionId = String(req.body?.questionId || '').trim();
  if (!question || !userAnswer) return res.status(400).json({ error: '请提供题目和考生作答文字' });
  try {
    const qb = readQuestionBank();
    let existing = questionId ? findQuestionById(qb, questionId) : null;
    const referenceAnswer = existing?.question?.answer || '';
    const messages = [
      { role: 'system', content: `你是事业编面试陪练点评老师。不要打分。请针对考生作答进行具体点评：肯定好的点，指出问题，给出怎么改、怎么润色、如何补充高度、如何落到岗位。语气鼓励、具体、可执行。输出结构：一、整体评价；二、好的地方；三、主要问题；四、润色改进建议；五、可替换表达。` },
      { role: 'user', content: `题目：\n${question}\n\n考生作答文字：\n${userAnswer}\n\n${referenceAnswer ? `参考逐字稿：\n${referenceAnswer.slice(0, 5000)}` : ''}` }
    ];
    const r = await callChat(messages, false, 0.45);
    const jobj = await r.json();
    const review = jobj.choices?.[0]?.message?.content || '';
    if (existing) {
      const q = normalizeQuestionRecord(existing.question);
      q.reviews = Array.isArray(q.reviews) ? q.reviews : [];
      q.reviews.unshift({ id: Date.now().toString(36), userAnswer, review, createdAt: new Date().toISOString() });
      writeQuestionBank(qb);
    }
    try { mergeKb(await extractReviewKnowledge(question, review, userAnswer)); } catch {}
    res.json({ review, kbUpdated: true });
  } catch (e) {
    res.status(500).json({ error: e.message });
  }
});

app.get('/api/default-prompt', (req, res) => res.json({ prompt: SYSTEM_PROMPT }));

app.post('/api/chat', async (req, res) => {
  const question = String((req.body && req.body.question) || '').trim();
  const existingQuestionId = String(req.body?.existingQuestionId || '').trim();
  const customPrompt = String(req.body?.customPrompt || '').trim();
  const promptForThisRequest = customPrompt || SYSTEM_PROMPT;
  if (!question) return res.status(400).json({ error: '请输入题目或材料' });
  const kb = readKb();
  const qb = readQuestionBank();
  const messages = [
    { role: 'system', content: promptForThisRequest },
    { role: 'system', content: `当前知识库可用于迁移参考：\n${kbText(kb) || '暂无知识库内容。'}\n\n已有题型：${(qb.types||[]).map(t=>`${t.name}(${(t.questions||[]).length})`).join('、') || '暂无'}。生成答案时，尽量总结题型规律，方便后期喂大量题目后归纳学习。${customPrompt ? '\n\n注意：本次使用了用户临时修改的预设提示词；该提示词只对本次生成生效，不保存、不影响下次或其他用户。' : ''}` },
    { role: 'user', content: question }
  ];

  res.writeHead(200, {
    'Content-Type': 'text/event-stream; charset=utf-8',
    'Cache-Control': 'no-cache, no-transform',
    'Connection': 'keep-alive',
    'X-Accel-Buffering': 'no'
  });

  let full = '';
  try {
    const upstream = await callChat(messages, true, 0.75);
    const decoder = new TextDecoder();
    let buf = '';
    for await (const chunk of upstream.body) {
      buf += decoder.decode(chunk, { stream: true });
      const lines = buf.split('\n');
      buf = lines.pop() || '';
      for (const line of lines) {
        const s = line.trim();
        if (!s.startsWith('data:')) continue;
        const data = s.slice(5).trim();
        if (data === '[DONE]') continue;
        try {
          const obj = JSON.parse(data);
          const delta = obj.choices?.[0]?.delta?.content || obj.choices?.[0]?.message?.content || '';
          if (delta) { full += delta; res.write(`data: ${JSON.stringify({ delta })}\n\n`); }
        } catch {}
      }
    }

    const extractMessages = [
      { role: 'system', content: `你是事业编面试题库与知识库整理员。请完成两件事：1）判断题型并保存题库元信息；2）从答案中提炼精简、可复用的知识库内容。只输出JSON，不要解释。
题型可选但不限于：${QUESTION_TYPES.join('、')}。如果用户分类不全面，你可以新增更准确题型。
知识库必须按用户指定的三级结构：一级category=题型大类，只能/优先使用这些大类：${QUESTION_TYPES.join('、')}；二级section=该大类下的细分场景，例如组织活动类下面是组织会议、组织群众活动、组织宣传活动、组织调研，观点认知类下面是对一句话的理解/干部品质理解，人际关系类下面是和同事的关系/和群众的关系/和领导的关系，现象认知类下面是经济现象认知/基层治理认知/社会民生认知/科技创新认知；三级group=该细分场景下的内容类型，例如答题方法、名言金句、好做法、名人事迹、政策表达、结构模板、典型案例、岗位落实、易错提醒。不要把“名言金句/好做法/政策表达”等作为一级分类，也不要把“组织活动类/观点认知类”等放到二级。
输出格式：{"questionMeta":{"questionType":"一级题型大类","subType":"二级细分场景","title":"题目短标题","typeReason":"分类理由","difficulty":"简单/中等/较难","tags":["标签"]},"categories":[{"name":"一级题型大类","description":"说明","sections":[{"name":"二级细分场景","description":"说明","groups":[{"name":"三级内容类型","description":"说明","items":[{"title":"标题","type":"名言金句/好做法/名人事迹/政策表达/答题规律/结构模板/题型打法/岗位落实","content":"可复用内容，尽量短而精","source":"来自本题"}]}]}]}]}` },
      { role: 'user', content: `题目：\n${question}\n\n生成答案：\n${full}\n\n已有题型大类：${(qb.types||[]).map(t=>t.name).join('、') || '暂无'}。\n已有知识库一级题型大类：${(kb.categories||[]).map(c=>c.name).join('、') || '暂无'}。二级分类示例：${JSON.stringify(SUBTYPE_EXAMPLES)}。请严格按 一级=题型大类、二级=细分场景、三级=素材/方法类型 输出。` }
    ];
    let meta = null;
    try {
      const kr = await callChat(extractMessages, false, 0.25);
      const jobj = await kr.json();
      const text = jobj.choices?.[0]?.message?.content || '';
      const extracted = safeJsonSlice(text);
      meta = extracted?.questionMeta || null;
      if (mergeKb(extracted)) res.write(`data: ${JSON.stringify({ kbUpdated: true })}\n\n`);
    } catch (e) {
      res.write(`data: ${JSON.stringify({ note: '逐字稿已生成，知识库提取失败：' + e.message })}\n\n`);
    }
    saveQuestion(question, full, meta || { questionType: '其他类', title: question.slice(0,40), tags: [] }, existingQuestionId);
    res.write(`data: ${JSON.stringify({ questionSaved: true, questionType: normalizeQuestionType(meta?.questionType || '其他类'), existingQuestionId: existingQuestionId || null })}\n\n`);
    res.write('data: [DONE]\n\n');
    res.end();
  } catch (e) {
    res.write(`data: ${JSON.stringify({ error: e.message })}\n\n`);
    res.end();
  }
});

app.listen(PORT, '0.0.0.0', () => console.log(`kaogongbot listening on http://0.0.0.0:${PORT}`));
