const KEY = {
  agreement: "shai-agreement-v1",
  modelGate: "shai-model-gate-v1",
  state: "shai-agent-state-v1",
  endpoint: "shai-endpoint",
  token: "shai-token",
  compute: "shai-compute",
  model: "shai-model",
  theme: "shai-theme",
  chatFont: "shai-chat-font",
  speak: "shai-speak",
  speechStyle: "shai-speech-style",
  wake: "shai-wake",
  wakeMode: "shai-wake-mode",
  wakeCustom: "shai-wake-custom",
  users: "shai-users-v1",
  activeUser: "shai-active-user-v1",
  paymentOrders: "shai-payment-orders-v1",
  permissionMode: "shai-permission-mode-v1",
  permissionConfig: "shai-permission-config-v1",
  reviewPin: "shai-review-pin-v1",
  reviewUnlocked: "shai-review-unlocked-v1",
};

const APP_VERSION = "1.4.4";

const models = [
  { id: "qwen3-1.7b-q4", name: "Qwen 轻量本地模型", tag: "推荐", desc: "适合手机端离线对话和轻量代码辅助。" },
  { id: "qwen3-4b-q4", name: "Qwen 标准本地模型", tag: "强", desc: "适合平板和电脑，能处理更长上下文。" },
  { id: "deepseek-distill-q4", name: "DeepSeek 蒸馏推理模型", tag: "推理", desc: "适合复杂规划、代码推理和多步骤任务。" },
  { id: "glm-edge-q4", name: "GLM 边端模型", tag: "国产", desc: "适合中文办公、摘要和对话。" },
];

const tokenPlans = [
  { id: "starter", name: "体验包", tokens: 100000, price: 19, desc: "适合轻度聊天、办公和代码辅助。" },
  { id: "pro", name: "专业包", tokens: 600000, price: 99, desc: "适合高频办公、长文档和复杂任务。", featured: true },
  { id: "team", name: "团队包", tokens: 2000000, price: 299, desc: "适合小团队共享、项目开发和批量处理。" },
];

const paymentConfig = {
  receiver: "山海经AI官方收款",
  alipayQr: "./assets/pay-alipay.png",
  wechatQr: "./assets/pay-wechat.png",
};

const DEFAULT_PERMISSION_CONFIG = `[permissions]
network = "ask"
external_files = "ask"
device_control = "ask"
shell_commands = "ask"
payments = "ask"
risky_actions = "deny"

[trusted_paths]
workspace = true
downloads = "ask"`;

const permissionModes = {
  ask: {
    title: "请求批准",
    desc: "编辑外部文件和使用互联网时始终询问。",
    prompt: "所有联网、外部文件、设备控制、命令执行、支付、删除、发送等动作都要先请求用户批准。",
  },
  smart: {
    title: "替我审批",
    desc: "普通低风险动作可自动继续，仅对检测到的风险操作请求批准。",
    prompt: "低风险读写和生成任务可继续；联网、外部文件、删除、支付、账号、安装、命令等高风险动作必须请求用户批准。",
  },
  full: {
    title: "完全访问权限",
    desc: "在系统授权范围内尽量自动完成联网、文件和设备任务。",
    prompt: "用户允许在系统授权范围内自动执行联网、文件和设备任务；涉及不可逆删除、支付、账号密码、隐私外发和高风险命令仍要明确确认。",
  },
  custom: {
    title: "自定义（config.toml）",
    desc: "使用用户在 config.toml 中定义的权限策略。",
    prompt: "按用户自定义 config.toml 执行权限策略；没有明确允许的高风险动作必须请求确认。",
  },
};

const skills = [
  ["代码工程", "生成代码、解释项目、修 bug、写测试、给命令。"],
  ["网页生成", "从一句话生成 HTML/CSS/JS 与预览。"],
  ["产品设计", "写 PRD、功能架构、路线图和验收标准。"],
  ["办公文档", "报告、总结、方案、PPT 大纲、邮件。"],
  ["设备规划", "手机/电脑操作步骤、权限说明、风险提示。"],
  ["文件理解", "理解图片、代码、文档和长资料。"],
  ["本机记忆", "记住用户偏好、常用项目和任务习惯。"],
  ["模型调度", "本地、云端、混合算力切换。"],
];

const knowledgeCards = [
  {
    match: [/氢气|H2/i],
    title: "氢气是怎么产生的",
    short: "氢气的本质是两个氢原子结合成 H2。只要把含氢物质里的氢元素释放出来，并让氢原子两两结合，就会产生氢气。",
    board: ["含氢物质", "反应或能量", "氢原子结合", "H2 氢气"],
    points: [
      "实验室常见方法：活泼金属和酸反应，例如锌和稀盐酸反应会放出氢气。",
      "工业或清洁能源方法：电解水，用电把水分解成氢气和氧气。",
      "化学方程式例子：Zn + 2HCl = ZnCl2 + H2↑；2H2O 通电生成 2H2 + O2。",
      "安全提醒：氢气易燃，不能靠近明火，实验要通风并由专业人员操作。",
    ],
  },
  {
    match: [/二氧化碳|CO2/i],
    title: "二氧化碳是怎么产生的",
    short: "二氧化碳是碳元素和氧气结合后的产物，化学式是 CO2。燃烧、呼吸、发酵和碳酸盐反应都会产生二氧化碳。",
    board: ["含碳物质", "和氧反应", "生成 CO2", "进入空气"],
    points: [
      "燃烧：煤、木头、汽油、天然气等含碳物质充分燃烧，会生成二氧化碳。",
      "呼吸：人体把有机物氧化释放能量，同时呼出二氧化碳。",
      "发酵：酵母发酵面团或酿酒时，会产生二氧化碳。",
      "实验室：碳酸钙和盐酸反应会产生二氧化碳，例如 CaCO3 + 2HCl = CaCl2 + H2O + CO2↑。",
    ],
  },
  {
    match: [/牛顿|刘顿|Newton/i],
    title: "牛顿是谁",
    short: "牛顿通常指艾萨克·牛顿，英国科学家。他在力学、万有引力、微积分和光学方面影响巨大，是近代科学史上最重要的人物之一。",
    board: ["观察自然", "建立规律", "数学表达", "解释世界"],
    points: [
      "力学：提出经典力学三大定律，解释物体为什么会运动、停止或改变速度。",
      "万有引力：说明苹果落地、月球绕地球运行等现象背后都有引力规律。",
      "光学：研究白光和颜色，证明白光可以分解成多种颜色。",
      "可以这样记：牛顿把很多看起来零散的自然现象，用数学规律连成了一套体系。",
    ],
  },
  {
    match: [/函数|function/i],
    title: "函数是什么意思",
    short: "函数可以理解成一种“输入和输出的对应关系”。你给它一个输入，它按照固定规则给你一个输出。",
    board: ["输入 x", "固定规则", "输出 y", "形成对应"],
    points: [
      "例子：y = 2x + 1。输入 x = 3，输出 y = 7。",
      "重点不是背公式，而是看清楚：谁是输入、规则是什么、输出怎么变化。",
      "图像理解：函数图像上的每个点，都表示一组输入 x 和输出 y 的对应关系。",
      "学习函数时先抓三个问题：定义域、表达式、图像变化。",
    ],
  },
  {
    match: [/世界上最伟大的人|最伟大的人/i],
    title: "世界上最伟大的人没有唯一答案",
    short: "“伟大”取决于评价标准。科学、政治、医学、思想、和平、艺术等领域的标准不同，所以不会只有一个公认答案。",
    board: ["评价标准", "历史贡献", "影响范围", "不同答案"],
    points: [
      "科学领域常被提到：牛顿、爱因斯坦、居里夫人等。",
      "社会与政治领域常被提到：华盛顿、林肯、曼德拉、甘地等。",
      "医学与社会贡献领域常被提到：南丁格尔、屠呦呦等。",
      "更好的问法是：在某个领域、某个国家或某个评价标准下，谁最伟大。",
    ],
  },
  {
    match: [/美国.*伟大的人|美国最伟大/i],
    title: "美国历史上常被认为伟大的人",
    short: "美国历史上没有唯一“最伟大”的标准答案，但华盛顿、林肯、富兰克林、马丁·路德·金等人经常被讨论。",
    board: ["建国", "统一", "民权", "思想影响"],
    points: [
      "华盛顿：美国开国总统，在建国和制度建立中影响很大。",
      "林肯：领导美国内战时期的国家统一，并推动废奴进程。",
      "富兰克林：科学家、政治家、思想家，参与美国建国。",
      "马丁·路德·金：民权运动代表人物，推动平等权利。",
    ],
  },
];

const defaultProject = () => ({
  id: `p-${Date.now()}`,
  name: "山海经AI 工作空间",
  createdAt: Date.now(),
  files: {
    "/README.md": "# 山海经AI 工作空间\n\n这里会保存智能体生成的代码、文档、计划和任务记录。\n",
  },
  activeFile: "/README.md",
  terminal: ["$ shai agent ready", "$ workspace initialized"],
  plan: ["等待用户提出目标", "理解任务", "生成文件或方案", "验证并总结"],
  toolLog: [{ name: "init_workspace", detail: "已创建本机智能体工作空间", at: Date.now() }],
});

const state = loadState();
let recognition = null;
let wakeRecognition = null;
let voiceActive = false;
let voiceDraft = "";
let currentTab = "workspace";
let tabHistory = ["workspace"];
let gesture = null;
let pendingPaymentPlanId = null;
let paymentMethod = "alipay";
let reviewFilter = "all";

const $ = (selector) => document.querySelector(selector);
const $$ = (selector) => Array.from(document.querySelectorAll(selector));

function loadState() {
  const saved = readJson(KEY.state, null);
  if (saved?.projects?.length) {
    saved.memory ||= { topics: {}, preferences: [], tasks: [] };
    saved.memory.topics ||= {};
    saved.memory.preferences ||= [];
    saved.memory.tasks ||= [];
    saved.upgradeLog ||= [];
    return saved;
  }
  const project = defaultProject();
  const threadId = `t-${Date.now()}`;
  return {
    projects: [project],
    activeProjectId: project.id,
    threads: [{
      id: threadId,
      title: "山海经AI 初始任务",
      createdAt: Date.now(),
      messages: [],
    }],
    activeThreadId: threadId,
    memory: { topics: {}, preferences: [], tasks: [] },
    upgradeLog: [],
  };
}

if (!state.threads.find((thread) => thread.id === state.activeThreadId)) {
  state.activeThreadId = state.threads[0]?.id || createThread("新任务", false).id;
}

function readJson(key, fallback) {
  try {
    const raw = localStorage.getItem(key);
    return raw ? JSON.parse(raw) : fallback;
  } catch {
    return fallback;
  }
}

function loadUsers() {
  return readJson(KEY.users, {});
}

function saveUsers(users) {
  localStorage.setItem(KEY.users, JSON.stringify(users));
}

function loadPaymentOrders() {
  return readJson(KEY.paymentOrders, []);
}

function savePaymentOrders(orders) {
  localStorage.setItem(KEY.paymentOrders, JSON.stringify(orders.slice(0, 100)));
}

function activeAccountId() {
  return localStorage.getItem(KEY.activeUser) || "";
}

function currentUser() {
  const id = activeAccountId();
  if (!id) return null;
  return loadUsers()[id] || null;
}

function requireLogin() {
  if (currentUser()) return true;
  openAuthGate();
  toast("请先登录或注册账户");
  return false;
}

function updateCurrentUser(patch) {
  const id = activeAccountId();
  if (!id) return null;
  const users = loadUsers();
  if (!users[id]) return null;
  users[id] = { ...users[id], ...patch, updatedAt: Date.now() };
  saveUsers(users);
  return users[id];
}

function userTokenBalance() {
  return currentUser()?.tokens || 0;
}

function addTokens(amount, source = "充值") {
  if (!requireLogin()) return false;
  const user = currentUser();
  const history = user.history || [];
  history.unshift({ type: "add", amount, source, at: Date.now() });
  updateCurrentUser({ tokens: (user.tokens || 0) + amount, history: history.slice(0, 50) });
  renderAccount();
  renderModels();
  return true;
}

function consumeTokens(amount, source = "云端模型调用") {
  const user = currentUser();
  if (!user) return false;
  if ((user.tokens || 0) < amount) return false;
  const history = user.history || [];
  history.unshift({ type: "use", amount, source, at: Date.now() });
  updateCurrentUser({ tokens: user.tokens - amount, history: history.slice(0, 50) });
  renderAccount();
  renderModels();
  return true;
}

function saveState() {
  localStorage.setItem(KEY.state, JSON.stringify(state));
}

function escapeHtml(text = "") {
  return String(text)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

function stripHtml(html = "") {
  const div = document.createElement("div");
  div.innerHTML = html;
  return div.textContent || "";
}

function now() {
  return new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" });
}

function activeProject() {
  return state.projects.find((project) => project.id === state.activeProjectId) || state.projects[0];
}

function activeThread() {
  return state.threads.find((thread) => thread.id === state.activeThreadId) || state.threads[0];
}

function toast(text) {
  const node = $("#toast");
  node.textContent = text;
  node.hidden = false;
  clearTimeout(toast.timer);
  toast.timer = setTimeout(() => { node.hidden = true; }, 2200);
}

function boot() {
  blockZoom();
  bindEvents();
  applyTheme();
  applyChatFont();
  initAgreement();
  setTimeout(() => { $("#bootScreen").hidden = true; }, 850);
}

function blockZoom() {
  const prevent = (event) => event.preventDefault();
  document.addEventListener("gesturestart", prevent, { passive: false });
  document.addEventListener("gesturechange", prevent, { passive: false });
  document.addEventListener("gestureend", prevent, { passive: false });
  document.addEventListener("touchmove", (event) => {
    if (event.touches?.length > 1) event.preventDefault();
  }, { passive: false });
}

function initAgreement() {
  $("#acceptAgreementBtn").addEventListener("click", () => {
    localStorage.setItem(KEY.agreement, "true");
    document.body.classList.remove("agreement-pending");
    $("#agreementScreen").hidden = true;
    startApp();
  });
  $("#declineAgreementBtn").addEventListener("click", () => {
    window.close();
    $("#agreementScreen").querySelector(".gate-body").innerHTML = "<p>你未同意协议，无法继续使用山海经AI。</p>";
  });
  if (localStorage.getItem(KEY.agreement) === "true") {
    document.body.classList.remove("agreement-pending");
    $("#agreementScreen").hidden = true;
    startApp();
  }
}

function startApp() {
  renderAll();
  if (!activeThread().messages.length) {
    addMessage("assistant", welcomeHtml(), "山海经AI", { silent: true });
  }
  if (localStorage.getItem(KEY.modelGate) !== "true") {
    $("#modelGate").hidden = false;
  }
  updateNetwork();
  startWakeIfNeeded();
  registerServiceWorker();
}

function bindEvents() {
  $("#sendBtn").addEventListener("click", () => submit());
  $("#composerInput").addEventListener("input", autoGrow);
  $("#composerInput").addEventListener("keydown", (event) => {
    if (event.key === "Enter" && !event.shiftKey) {
      event.preventDefault();
      submit();
    }
  });
  $("#menuBtn").addEventListener("click", toggleMenu);
  $("#menuBackdrop").addEventListener("click", closeMenu);
  $("#newThreadBtn").addEventListener("click", () => {
    createThread("新任务", true);
    renderAll();
  });
  $$(".rail-nav button").forEach((button) => {
    button.addEventListener("click", () => showTab(button.dataset.tab));
  });
  $$(".inspector-tabs button").forEach((button) => {
    button.addEventListener("click", () => showInspector(button.dataset.inspector));
  });
  $("#agentMenuBtn").addEventListener("click", () => {
    $("#agentMenu").hidden = !$("#agentMenu").hidden;
  });
  $("#agentMenu").addEventListener("click", (event) => {
    const button = event.target.closest("[data-agent-prompt]");
    if (!button) return;
    $("#agentMenu").hidden = true;
    submit(button.dataset.agentPrompt);
  });
  $("#promptChips").addEventListener("click", (event) => {
    const button = event.target.closest("button");
    if (button) submit(button.textContent.trim());
  });
  $("#threadList").addEventListener("click", (event) => {
    const button = event.target.closest("[data-thread]");
    if (!button) return;
    state.activeThreadId = button.dataset.thread;
    saveState();
    renderAll();
  });
  $("#fileList").addEventListener("click", (event) => {
    const button = event.target.closest("[data-file]");
    if (!button) return;
    activeProject().activeFile = button.dataset.file;
    saveState();
    renderFiles();
  });
  $("#exportProjectBtn").addEventListener("click", exportProject);
  $("#recommendModelBtn").addEventListener("click", () => {
    localStorage.setItem(KEY.model, "qwen3-1.7b-q4");
    localStorage.setItem(KEY.compute, "local");
    localStorage.setItem(KEY.modelGate, "true");
    $("#modelGate").hidden = true;
    renderModels();
    renderAccount();
    toast("已启用本地免费大模型");
  });
  $("#customModelBtn").addEventListener("click", () => {
    localStorage.setItem(KEY.modelGate, "true");
    $("#modelGate").hidden = true;
    showTab("account");
    openAuthGate();
  });
  $("#skipModelBtn").addEventListener("click", () => {
    localStorage.setItem(KEY.modelGate, "true");
    $("#modelGate").hidden = true;
  });
  $("#computeGroup").addEventListener("click", (event) => {
    const button = event.target.closest("[data-compute]");
    if (!button) return;
    localStorage.setItem(KEY.compute, button.dataset.compute);
    renderModels();
  });
  $("#saveApiBtn").addEventListener("click", saveApi);
  $("#clearApiBtn").addEventListener("click", clearApi);
  $("#accountBtn").addEventListener("click", () => {
    if (currentUser()) showTab("account");
    else openAuthGate();
  });
  $("#openLoginBtn").addEventListener("click", openAuthGate);
  $("#closeAuthBtn").addEventListener("click", closeAuthGate);
  $("#loginBtn").addEventListener("click", loginUser);
  $("#registerBtn").addEventListener("click", registerUser);
  $("#logoutBtn").addEventListener("click", logoutUser);
  $("#openTokenCenterBtn").addEventListener("click", () => {
    showTab("account");
    if (!currentUser()) openAuthGate();
  });
  $("#useLocalModelBtn").addEventListener("click", () => {
    localStorage.setItem(KEY.compute, "local");
    localStorage.setItem(KEY.model, localStorage.getItem(KEY.model) || "qwen3-1.7b-q4");
    renderModels();
    renderAccount();
    toast("已切换为本地免费模式");
  });
  $("#tokenPlans").addEventListener("click", (event) => {
    const button = event.target.closest("[data-token-plan]");
    if (!button) return;
    buyTokenPlan(button.dataset.tokenPlan);
  });
  $("#paymentMethodGroup").addEventListener("click", (event) => {
    const button = event.target.closest("[data-payment-method]");
    if (!button) return;
    paymentMethod = button.dataset.paymentMethod;
    renderPaymentGate();
  });
  $("#submitPaymentBtn").addEventListener("click", submitPaymentOrder);
  $("#closePaymentBtn").addEventListener("click", closePaymentGate);
  $("#unlockReviewBtn").addEventListener("click", unlockReview);
  $("#lockReviewBtn").addEventListener("click", lockReview);
  $("#manualIssueBtn").addEventListener("click", issueManualTokens);
  $("#exportOrdersBtn").addEventListener("click", exportOrders);
  $("#refreshReviewBtn").addEventListener("click", renderReview);
  $("#reviewFilterGroup").addEventListener("click", (event) => {
    const button = event.target.closest("[data-review-filter]");
    if (!button) return;
    reviewFilter = button.dataset.reviewFilter;
    renderReview();
  });
  $("#reviewOrders").addEventListener("click", (event) => {
    const approve = event.target.closest("[data-approve-order]");
    const reject = event.target.closest("[data-reject-order]");
    if (approve) approvePaymentOrder(approve.dataset.approveOrder);
    if (reject) rejectPaymentOrder(reject.dataset.rejectOrder);
  });
  $("#themeSelect").addEventListener("change", (event) => {
    localStorage.setItem(KEY.theme, event.target.value);
    applyTheme();
  });
  $("#chatFontRange").addEventListener("input", (event) => {
    localStorage.setItem(KEY.chatFont, event.target.value);
    applyChatFont();
  });
  $("#speakBtn").addEventListener("click", toggleSpeak);
  $("#speakSwitch").addEventListener("change", (event) => {
    localStorage.setItem(KEY.speak, event.target.checked ? "true" : "false");
    if (!event.target.checked) stopSpeaking();
    renderSpeak();
  });
  $("#speechStyleSelect").addEventListener("change", (event) => {
    localStorage.setItem(KEY.speechStyle, event.target.value);
    toast("播报语气已更新");
  });
  $("#voiceBtn").addEventListener("click", toggleVoice);
  $("#wakeSwitch").addEventListener("change", (event) => {
    localStorage.setItem(KEY.wake, event.target.checked ? "true" : "false");
    startWakeIfNeeded();
  });
  $("#wakeSelect").addEventListener("change", (event) => {
    localStorage.setItem(KEY.wakeMode, event.target.value);
    startWakeIfNeeded();
  });
  $("#wakeCustomInput").addEventListener("input", (event) => {
    localStorage.setItem(KEY.wakeCustom, event.target.value.trim());
  });
  $("#recordWakeBtn").addEventListener("click", recordWake);
  $("#permissionModeGroup").addEventListener("click", (event) => {
    const button = event.target.closest("[data-permission-mode]");
    if (!button) return;
    localStorage.setItem(KEY.permissionMode, button.dataset.permissionMode);
    renderPermissionSettings();
    toast(`已切换为：${permissionModeInfo().title}`);
  });
  $("#savePermissionConfigBtn").addEventListener("click", () => {
    localStorage.setItem(KEY.permissionConfig, $("#permissionConfigInput").value.trim() || DEFAULT_PERMISSION_CONFIG);
    localStorage.setItem(KEY.permissionMode, "custom");
    renderPermissionSettings();
    toast("已保存自定义权限");
  });
  $("#resetPermissionConfigBtn").addEventListener("click", () => {
    localStorage.setItem(KEY.permissionConfig, DEFAULT_PERMISSION_CONFIG);
    localStorage.setItem(KEY.permissionMode, "custom");
    renderPermissionSettings();
    toast("已恢复 config.toml 模板");
  });
  $("#clearChatBtn").addEventListener("click", clearChat);
  $("#clearMemoryBtn").addEventListener("click", clearMemory);
  $("#runUpgradeBtn").addEventListener("click", runSelfUpgrade);
  $("#generateUpgradePlanBtn").addEventListener("click", generateUpgradeRoadmap);
  $("#resetAppBtn").addEventListener("click", resetApp);
  bindSwipeGestures();
  window.addEventListener("online", updateNetwork);
  window.addEventListener("offline", updateNetwork);
  window.addEventListener("resize", normalizeViewport);
}

function openMenu() {
  $("#rail").classList.add("open");
  $("#menuBackdrop").hidden = false;
  document.body.classList.add("menu-open");
}

function closeMenu() {
  $("#rail").classList.remove("open");
  $("#menuBackdrop").hidden = true;
  document.body.classList.remove("menu-open");
}

function toggleMenu() {
  if ($("#rail").classList.contains("open")) closeMenu();
  else openMenu();
}

function showTab(tab, options = {}) {
  const previous = currentTab;
  const push = options.push !== false;
  $("#agentMenu").hidden = true;
  $$(".rail-nav button").forEach((button) => button.classList.toggle("active", button.dataset.tab === tab));
  $$(".tab-view").forEach((view) => view.classList.toggle("active", view.id === `${tab}Tab`));
  currentTab = tab;
  if (push && previous !== tab) {
    tabHistory.push(tab);
    tabHistory = tabHistory.slice(-16);
  }
  closeMenu();
  normalizeViewport();
}

function showInspector(name) {
  $$(".inspector-tabs button").forEach((button) => button.classList.toggle("active", button.dataset.inspector === name));
  $$(".inspector-pane").forEach((pane) => pane.classList.toggle("active", pane.id === `${name}Pane`));
}

function handleBackGesture() {
  if (!$("#agentMenu").hidden) {
    $("#agentMenu").hidden = true;
    return true;
  }
  if ($("#rail").classList.contains("open")) {
    closeMenu();
    return true;
  }
  const activeInspector = $(".inspector-tabs button.active")?.dataset.inspector;
  if (activeInspector && activeInspector !== "plan" && currentTab === "workspace") {
    showInspector("plan");
    return true;
  }
  if (currentTab !== "workspace") {
    tabHistory.pop();
    const target = tabHistory[tabHistory.length - 1] || "workspace";
    showTab(target, { push: false });
    return true;
  }
  return false;
}

function bindSwipeGestures() {
  const start = (event) => {
    const touch = event.touches?.[0];
    if (!touch || event.touches.length > 1) return;
    if (event.target.closest("input, textarea, select, .gate, .agent-menu")) return;
    gesture = {
      x: touch.clientX,
      y: touch.clientY,
      lastX: touch.clientX,
      lastY: touch.clientY,
      horizontal: false,
      startedAt: Date.now(),
    };
  };
  const move = (event) => {
    if (!gesture || event.touches?.length !== 1) return;
    const touch = event.touches[0];
    const dx = touch.clientX - gesture.x;
    const dy = touch.clientY - gesture.y;
    gesture.lastX = touch.clientX;
    gesture.lastY = touch.clientY;
    if (!gesture.horizontal && Math.abs(dx) > 18 && Math.abs(dx) > Math.abs(dy) * 1.35) {
      gesture.horizontal = true;
    }
    if (gesture.horizontal) event.preventDefault();
  };
  const end = () => {
    if (!gesture) return;
    const dx = gesture.lastX - gesture.x;
    const dy = gesture.lastY - gesture.y;
    const elapsed = Date.now() - gesture.startedAt;
    const isSwipe = Math.abs(dx) > 72 && Math.abs(dx) > Math.abs(dy) * 1.3 && elapsed < 900;
    if (isSwipe && dx > 0 && gesture.x < 92) {
      openMenu();
    } else if (isSwipe && dx < 0) {
      if (!handleBackGesture()) toast("已经在主界面");
    }
    gesture = null;
  };
  document.addEventListener("touchstart", start, { passive: true });
  document.addEventListener("touchmove", move, { passive: false });
  document.addEventListener("touchend", end, { passive: true });
  document.addEventListener("touchcancel", () => { gesture = null; }, { passive: true });
}

function normalizeViewport() {
  document.documentElement.scrollLeft = 0;
  document.body.scrollLeft = 0;
  $(".workspace")?.scrollTo?.({ left: 0 });
}

function createThread(title = "新任务", activate = true) {
  const thread = {
    id: `t-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
    title,
    createdAt: Date.now(),
    messages: [],
  };
  state.threads.unshift(thread);
  if (activate) state.activeThreadId = thread.id;
  saveState();
  return thread;
}

function renderAll() {
  renderThreads();
  renderMessages();
  renderPlan();
  renderFiles();
  renderTerminal();
  renderPreview();
  renderProjects();
  renderSkills();
  renderModels();
  renderAccount();
  renderMemory();
  renderUpgrade();
  renderSettings();
  renderReview();
}

function renderThreads() {
  $("#threadList").innerHTML = state.threads.map((thread) => `
    <button class="${thread.id === state.activeThreadId ? "active" : ""}" data-thread="${thread.id}" type="button">
      <b>${escapeHtml(thread.title)}</b>
      <small>${new Date(thread.createdAt).toLocaleDateString("zh-CN")}</small>
    </button>
  `).join("");
}

function renderMessages() {
  const wrap = $("#messages");
  wrap.innerHTML = "";
  activeThread().messages.forEach((message) => {
    const node = document.createElement("article");
    node.className = `message ${message.role === "user" ? "user" : "assistant"}`;
    node.innerHTML = `
      <p class="message-meta">${escapeHtml(message.label)} · ${escapeHtml(message.time)}</p>
      <div class="bubble">${message.html}</div>
    `;
    let timer = null;
    const start = () => { timer = setTimeout(() => deleteMessage(message.id), 760); };
    const cancel = () => clearTimeout(timer);
    node.addEventListener("contextmenu", (event) => {
      event.preventDefault();
      deleteMessage(message.id);
    });
    node.addEventListener("touchstart", start, { passive: true });
    node.addEventListener("touchend", cancel);
    node.addEventListener("touchmove", cancel);
    wrap.appendChild(node);
  });
  scrollChat(false);
}

function addMessage(role, html, label = role === "user" ? "用户" : "山海经AI", options = {}) {
  const message = {
    id: `m-${Date.now()}-${Math.random().toString(16).slice(2, 7)}`,
    role,
    label,
    html,
    time: now(),
  };
  activeThread().messages.push(message);
  activeThread().messages = activeThread().messages.slice(-120);
  if (role === "user" && activeThread().messages.filter((item) => item.role === "user").length === 1) {
    activeThread().title = stripHtml(html).slice(0, 24) || "新任务";
  }
  saveState();
  renderThreads();
  renderMessages();
  if (role === "assistant" && isSpeakOn() && !options.silent) speak(stripHtml(html));
}

function deleteMessage(id) {
  activeThread().messages = activeThread().messages.filter((message) => message.id !== id);
  saveState();
  renderMessages();
  toast("已删除这条消息");
}

function scrollChat(smooth = true) {
  const panel = $(".chat-panel");
  requestAnimationFrame(() => panel.scrollTo({ top: panel.scrollHeight, behavior: smooth ? "smooth" : "auto" }));
}

function submit(raw = null) {
  const input = $("#composerInput");
  const text = (raw ?? input.value).trim();
  if (!text) return toast("请输入目标");
  input.value = "";
  autoGrow();
  $("#agentMenu").hidden = true;
  addMessage("user", `<p>${escapeHtml(text)}</p>`, "用户");
  remember(text);
  runAgent(text);
}

async function runAgent(text) {
  const blocked = safetyCheck(text);
  if (blocked) {
    addMessage("assistant", blocked, "山海经AI");
    return;
  }

  const permission = permissionExecutionState(text);
  updatePlan(["理解用户目标", "选择智能体技能", "生成交付内容", "写入虚拟项目", "给出下一步"]);
  logTool("analyze_request", `识别任务：${detectIntent(text).label}`);
  if (permission) {
    logTool("permission_mode", `${permission.info.title}：涉及 ${permission.scopes.join("、")}，${permission.action}`);
    logTerminal(`$ shai permission --mode ${permission.mode} --scope ${permission.scopes.join(",")}`);
  }
  logTerminal(`$ shai analyze "${text.replace(/"/g, "'").slice(0, 60)}"`);

  const cloud = await callModel(text);
  if (cloud) {
    logTool("model_generate", "已调用用户配置的真实模型");
    addMessage("assistant", `${permissionNoticeHtml(permission)}${formatText(cloud)}`, "山海经AI");
    return;
  }

  const result = buildLocalAgentResult(text);
  if (permission) {
    result.html = `${permissionNoticeHtml(permission)}${result.html}`;
    result.files = {
      ...(result.files || {}),
      "/system/permission-mode.md": permissionMarkdown(permission, text),
    };
  }
  if (result.files) writeFiles(result.files);
  if (result.terminal) result.terminal.forEach(logTerminal);
  logTool("write_virtual_workspace", result.summary);
  addMessage("assistant", result.html, "山海经AI");
  renderAll();
}

function safetyCheck(text) {
  const risk = /(制作|写|生成|教我|帮我).*(木马|勒索软件|盗号|钓鱼|诈骗|炸弹|毒品|绕过支付|破解账号|偷取密码|恶意软件|病毒)/i;
  if (!risk.test(text)) return "";
  return `
    <h4>这个方向我不能执行</h4>
    <p>它涉及违法、现实伤害、账号窃取、恶意软件或绕过安全权限。山海经AI不能提供具体实现。</p>
    <p>我可以改为帮你做安全防护、代码审计、风险解释或合规替代方案。</p>
  `;
}

function permissionMode() {
  const mode = localStorage.getItem(KEY.permissionMode) || "ask";
  return permissionModes[mode] ? mode : "ask";
}

function permissionModeInfo() {
  return permissionModes[permissionMode()];
}

function permissionConfigText() {
  return localStorage.getItem(KEY.permissionConfig) || DEFAULT_PERMISSION_CONFIG;
}

function sensitiveScopes(text) {
  const rules = [
    { label: "联网", test: /(联网|互联网|在线搜索|搜索网页|访问网页|上传|下载|同步|调用\s*api|请求接口)/i },
    { label: "外部文件", test: /(外部文件|电脑.*文件|手机.*文件|读取文件|写入文件|编辑文件|删除文件|覆盖文件|移动文件|重命名|目录|磁盘|桌面|下载文件夹)/i },
    { label: "命令执行", test: /(终端|命令|shell|脚本|安装|卸载|npm|pnpm|pip|brew|xcodebuild|adb|powershell|cmd|执行)/i },
    { label: "设备控制", test: /(控制电脑|控制手机|系统设置|摄像头|麦克风|相册|通讯录|定位|蓝牙|录屏|屏幕|快捷指令|自动化)/i },
    { label: "账号支付", test: /(支付|付款|收款|退款|订单|token|余额|充值|账号|登录|密码|验证码|隐私|发送给|发给别人)/i },
    { label: "不可逆操作", test: /(永久删除|清空|格式化|擦除|恢复出厂|销毁|不可恢复|批量删除)/i },
  ];
  return rules.filter((rule) => rule.test.test(text)).map((rule) => rule.label);
}

function permissionExecutionState(text) {
  const scopes = [...new Set(sensitiveScopes(text))];
  if (!scopes.length) return null;
  const mode = permissionMode();
  const info = permissionModeInfo();
  const actionMap = {
    ask: "执行前必须询问用户批准。",
    smart: "低风险步骤可自动规划，高风险动作需要请求批准。",
    full: "可在系统授权范围内自动推进，但不可逆和支付类动作仍要确认。",
    custom: "按 config.toml 判断；没有明确允许的动作需要请求确认。",
  };
  return {
    mode,
    info,
    scopes,
    action: actionMap[mode],
  };
}

function permissionNoticeHtml(permission) {
  if (!permission) return "";
  return `
    <div class="permission-notice">
      <b>操作权限：${escapeHtml(permission.info.title)}</b>
      <span>本次涉及 ${escapeHtml(permission.scopes.join("、"))}。${escapeHtml(permission.action)}</span>
    </div>
  `;
}

function permissionMarkdown(permission, text) {
  const config = permission.mode === "custom" ? `\n\n## config.toml\n\n\`\`\`toml\n${permissionConfigText()}\n\`\`\`` : "";
  return `# 操作审批记录

目标：${text}

当前模式：${permission.info.title}

说明：${permission.info.desc}

涉及范围：${permission.scopes.join("、")}

执行原则：${permission.action}${config}`;
}

function permissionInstruction() {
  const info = permissionModeInfo();
  const config = permissionMode() === "custom" ? `\n当前 config.toml：\n${permissionConfigText()}` : "";
  return `当前操作审批模式：${info.title}。${info.prompt}${config}`;
}

function detectIntent(text) {
  if (/网页|网站|官网|html|css|前端|页面/i.test(text)) return { key: "web", label: "网页工程" };
  if (/代码|程序|bug|接口|数据库|脚本|python|swift|react|vue|app/i.test(text)) return { key: "code", label: "代码工程" };
  if (/产品|需求|prd|功能|架构|路线图|设计/i.test(text)) return { key: "product", label: "产品设计" };
  if (/文档|报告|总结|方案|邮件|ppt|表格|办公/i.test(text)) return { key: "office", label: "办公文档" };
  if (/手机|电脑|文件|系统|设置|自动化|快捷指令|设备/i.test(text)) return { key: "device", label: "设备规划" };
  if (/进化|记住|偏好|习惯|自我|你是谁|介绍/i.test(text)) return { key: "evolve", label: "自我进化" };
  return { key: "general", label: "通用智能体" };
}

function buildLocalAgentResult(text) {
  const intent = detectIntent(text);
  if (isWritingRequest(text)) return writingResult(text);
  const knowledge = findKnowledgeCard(text);
  if (knowledge) return knowledgeResult(text, knowledge);
  if (intent.key === "web") return webResult(text);
  if (intent.key === "code") return codeResult(text);
  if (intent.key === "product") return productResult(text);
  if (intent.key === "office") return officeResult(text);
  if (intent.key === "device") return deviceResult(text);
  if (intent.key === "evolve") return evolveResult();
  if (isQuestion(text)) return openQuestionResult(text);
  return generalResult(text);
}

function diagram(items) {
  return `<div class="agent-diagram"><div class="agent-diagram-grid">${items.map((item) => `<span>${escapeHtml(item)}</span>`).join("")}</div></div>`;
}

function thinkingBoard(items) {
  return `
    <div class="thinking-board" aria-label="思维草图">
      ${items.map((item, index) => `
        <div>
          <b>${index + 1}</b>
          <span>${escapeHtml(item)}</span>
        </div>
      `).join("")}
    </div>
  `;
}

function findKnowledgeCard(text) {
  return knowledgeCards.find((card) => card.match.some((pattern) => pattern.test(text)));
}

function isQuestion(text) {
  return /(是什么|什么意思|怎么|为什么|是谁|多少|如何|能不能|可以吗|解释|介绍|分析|\?|？)/.test(text);
}

function isWritingRequest(text) {
  return /(作文|文章|演讲稿|读后感|观后感|周记|日记|检讨|书信|文案|诗歌|故事|小说|总结).*(写|生成|帮我|来一篇|一篇)|写.*(作文|文章|演讲稿|读后感|观后感|周记|日记|故事|小说)/.test(text);
}

function extractTopic(text) {
  const patterns = [
    /关于(.+?)(?:的)?(?:作文|文章|演讲稿|读后感|观后感|故事|小说|文案)/,
    /(?:作文|文章|演讲稿|读后感|观后感|故事|小说|文案).*?(?:主题|题目|标题)?[：: ]+(.+)/,
    /写(?:一篇|个)?(.+?)(?:作文|文章|演讲稿|读后感|观后感|故事|小说|文案)/,
  ];
  for (const pattern of patterns) {
    const match = text.match(pattern);
    if (match?.[1]) return match[1].replace(/[，。,.!?？：:]+$/g, "").trim().slice(0, 24);
  }
  return text.replace(/帮我|写|生成|一篇|一个|作文|文章|演讲稿|读后感|观后感|故事|小说|文案/g, "").trim().slice(0, 24) || "成长";
}

function safePathName(text) {
  return text.replace(/[\\/:*?"<>|\s]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 28) || "output";
}

function knowledgeResult(text, card) {
  return {
    summary: `已回答知识问题：${card.title}`,
    terminal: ["$ answer knowledge-question", `✓ topic: ${card.title}`],
    files: {
      [`/answers/${safePathName(card.title)}.md`]: `# ${card.title}\n\n${card.short}\n\n${card.points.map((point, index) => `${index + 1}. ${point}`).join("\n")}\n\n用户原问题：${text}`,
    },
    html: `
      <h4>${escapeHtml(card.title)}</h4>
      <p>${escapeHtml(card.short)}</p>
      ${thinkingBoard(card.board)}
      <h4>你可以这样理解</h4>
      <ol>${card.points.map((point) => `<li>${escapeHtml(point)}</li>`).join("")}</ol>
    `,
  };
}

function writingResult(text) {
  const topic = extractTopic(text);
  const title = topic.includes("青春") ? "青春，在奔跑中发光" : `${topic}给我的启发`;
  const essay = `# ${title}

有些词看起来很普通，可真正经历过以后，才知道它们藏着力量。${topic}就是这样一个词。它不是一句空话，也不是挂在嘴边的口号，而是在一次次选择、坚持和改变里慢慢显出来的东西。

一开始，我对${topic}的理解很浅，只觉得它离自己很远。后来我发现，它其实就在生活里的小事中：一次认真完成的作业，一次不想放弃却还是坚持下来的练习，一次遇到困难后重新站起来的勇气。真正重要的不是一开始就有多厉害，而是在遇到问题时，还愿意继续往前走。

如果把${topic}比作一束光，它并不会突然照亮全部道路。更多时候，它像清晨的一点亮色，先照亮脚下的一小步。只要一步一步走下去，回头看时，就会发现自己已经走过很远。

所以，我愿意把${topic}理解为一种成长的力量。它提醒我：不要害怕慢，也不要害怕暂时不够好。只要方向是对的，只要愿意行动，每一次努力都会让今天的自己比昨天更坚定。`;
  return {
    summary: `已生成作文：${title}`,
    terminal: ["$ write writing-draft", "✓ essay draft ready"],
    files: {
      [`/writing/${safePathName(title)}.md`]: `${essay}\n\n## 写作提纲\n1. 开头点题：说明自己对“${topic}”的初步理解。\n2. 中间举例：用生活、学习或成长经历证明主题。\n3. 结尾升华：把主题落到行动和成长上。`,
    },
    html: `
      <h4>${escapeHtml(title)}</h4>
      ${thinkingBoard(["开头点题", "生活例子", "情感转折", "结尾升华"])}
      ${formatText(essay)}
      <h4>写作思路</h4>
      <ol>
        <li>先把题目里的关键词解释清楚，别一上来堆空话。</li>
        <li>中间用一件具体事情支撑主题，让文章有画面。</li>
        <li>最后把感悟落到行动上，文章会更完整。</li>
      </ol>
    `,
  };
}

function openQuestionResult(text) {
  return {
    summary: "已生成开放问题回答",
    terminal: ["$ reason open-question", "✓ structured answer ready"],
    files: {
      "/answers/open-question.md": `# 开放问题\n\n用户问题：${text}\n\n## 回答方式\n先解释概念，再分析原因，最后给例子和可继续追问的方向。`,
    },
    html: `
      <h4>我先按问题本身来回答</h4>
      <p>你问的是：<strong>${escapeHtml(text)}</strong></p>
      ${thinkingBoard(["抓关键词", "解释概念", "分析原因", "举例验证"])}
      <p>这个问题可以先拆成三层：它是什么、为什么会这样、生活里怎么判断。你如果给我更具体的对象或场景，我可以继续展开到更精确的答案。</p>
      <ol>
        <li><strong>先看定义：</strong>把问题里的关键词翻译成容易懂的话。</li>
        <li><strong>再看关系：</strong>找出原因、条件、影响和结果。</li>
        <li><strong>最后看例子：</strong>用现实例子验证理解是否正确。</li>
      </ol>
    `,
  };
}

function webResult(text) {
  const html = `<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>山海经AI生成页面</title>
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <main class="hero">
      <p>Generated by ShanHaiJing AI</p>
      <h1>${escapeHtml(text).replace(/<[^>]+>/g, "")}</h1>
      <button>开始使用</button>
    </main>
    <script src="./app.js"></script>
  </body>
</html>`;
  const css = `body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;background:#07111a;color:#f3fbf8}.hero{min-height:100vh;display:grid;place-content:center;gap:18px;padding:32px;text-align:center;background:radial-gradient(circle at 30% 20%,rgba(41,214,197,.28),transparent 32%),linear-gradient(135deg,#07111a,#14263a)}h1{max-width:880px;font-size:clamp(42px,8vw,88px);line-height:1.02}p{color:#29d6c5;font-weight:900}button{height:54px;border:0;border-radius:14px;padding:0 24px;background:#29d6c5;color:#031412;font-weight:900}`;
  const js = `document.querySelector("button").addEventListener("click",()=>alert("山海经AI生成的网页已运行"));`;
  return {
    summary: "已生成网页项目文件",
    terminal: ["$ write index.html styles.css app.js", "$ run virtual-preview", "✓ preview ready"],
    files: {
      "/web/index.html": html,
      "/web/styles.css": css,
      "/web/app.js": js,
      "/web/README.md": `# 网页项目\n\n需求：${text}\n\n由山海经AI原型智能体生成。`,
    },
    html: `
      <h4>我已经按 Codex 工作流生成一个网页项目</h4>
      ${diagram(["理解需求", "生成文件", "建立预览", "给下一步"])}
      <ol>
        <li><strong>文件：</strong>已写入 /web/index.html、/web/styles.css、/web/app.js。</li>
        <li><strong>预览：</strong>右侧“预览”面板可以看到页面。</li>
        <li><strong>下一步：</strong>你可以继续说“改成科技风”“加登录页”“做成官网下载页”，我会继续改虚拟项目。</li>
      </ol>
    `,
  };
}

function codeResult(text) {
  const code = `function solveTask(input) {
  const steps = [
    "理解目标",
    "拆解任务",
    "实现核心逻辑",
    "验证输出"
  ];
  return { input, steps, status: "ready" };
}

console.log(solveTask(${JSON.stringify(text)}));`;
  return {
    summary: "已生成代码任务骨架",
    terminal: ["$ write /src/agent-task.js", "$ node src/agent-task.js", "✓ virtual run passed"],
    files: {
      "/src/agent-task.js": code,
      "/src/README.md": `# 代码任务\n\n用户目标：${text}\n\n下一步可以把具体语言、框架、错误信息或文件贴给山海经AI。`,
    },
    html: `
      <h4>我会按工程师模式处理这个代码任务</h4>
      ${diagram(["需求澄清", "代码结构", "实现", "测试"])}
      <p>我已在右侧虚拟文件里生成一个代码任务骨架。如果你给我真实项目文件或报错，我可以继续按文件级别修改。</p>
    `,
  };
}

function productResult(text) {
  const prd = `# 产品需求文档

## 目标
${text}

## 核心原则
- 对话即入口
- 智能体自动拆任务
- 用户确认敏感动作
- 本机记忆可控可清理

## V1 功能
1. 多会话项目空间
2. 代码/文档/网页生成
3. 虚拟文件系统和预览
4. 模型/API自由接入
5. 本机记忆与自我进化

## 验收标准
- 用户一句话可以生成可见交付
- 所有结果可编辑、可导出、可继续迭代
- 没有模型时有原型兜底，有模型时走真实推理`;
  return {
    summary: "已生成产品需求文档",
    terminal: ["$ write /docs/PRD.md", "✓ product spec generated"],
    files: { "/docs/PRD.md": prd },
    html: `
      <h4>我已把这个方向整理成产品需求</h4>
      ${diagram(["定位", "能力", "架构", "验收"])}
      <p>右侧文件里已经生成 /docs/PRD.md。你可以让我继续写交互稿、数据库结构、开发计划或 App Store 上架文案。</p>
    `,
  };
}

function officeResult(text) {
  const doc = `# 办公文档草稿

## 主题
${text}

## 摘要
本材料围绕目标、背景、执行步骤、风险控制和结果交付展开，适合继续扩写成正式报告、方案或汇报稿。

## 结构
1. 背景与问题
2. 目标与原则
3. 执行方案
4. 时间安排
5. 风险与应对
6. 总结与下一步`;
  return {
    summary: "已生成办公文档草稿",
    terminal: ["$ write /docs/output.md", "✓ document draft ready"],
    files: { "/docs/output.md": doc },
    html: `
      <h4>办公内容已生成草稿</h4>
      ${diagram(["明确主题", "搭建结构", "生成正文", "可继续润色"])}
      <p>我已经把文档放在右侧文件系统里。你可以继续要求“更正式”“加表格”“生成 PPT 大纲”。</p>
    `,
  };
}

function deviceResult(text) {
  const permission = permissionExecutionState(text);
  const info = permissionModeInfo();
  return {
    summary: "已生成设备任务执行计划",
    terminal: ["$ plan device-task", `! permission mode: ${info.title}`],
    files: {
      "/automation/device-plan.md": `# 设备任务计划\n\n目标：${text}\n\n## 当前审批模式\n- ${info.title}：${info.desc}\n- ${permission?.action || info.prompt}\n\n## 权限边界\n- 当前移动端原型只能给步骤，桌面端需要后续接入系统桥接器。\n- 真正读写外部文件、联网、控制设备、支付、删除或发送内容时，要按当前审批模式处理。\n\n## 执行步骤\n1. 确认设备和系统版本\n2. 确认目标 App 或文件\n3. 列出可回退步骤\n4. 按审批模式执行或交给用户手动操作`,
    },
    html: `
      <h4>我会按当前审批模式规划设备任务</h4>
      ${diagram([info.title, "拆解动作", permission?.scopes?.join("、") || "常规任务", "执行/回退"])}
      <p>当前模式：${escapeHtml(info.desc)}右侧已生成设备任务计划。真正控制电脑或手机时，还需要桌面/移动端桥接器和系统授权。</p>
    `,
  };
}

function evolveResult() {
  const roadmap = upgradeRoadmapMarkdown();
  return {
    summary: "已生成自我进化报告",
    terminal: ["$ inspect local-memory", "✓ evolution report ready"],
    files: {
      "/memory/evolution.md": `# 山海经AI自我进化报告\n\n${memorySummary()}\n\n## 下一步进化\n- 更强模型接入\n- 真正文件读写桥接\n- 桌面命令执行桥接\n- 技能插件系统\n- 项目级长期记忆`,
      "/system/self-upgrade-plan.md": roadmap,
    },
    html: `
      <h4>我会按智能体方式持续进化</h4>
      ${thinkingBoard(["记住偏好", "总结任务", "升级能力", "下次更快"])}
      <p>${escapeHtml(memorySummary())}</p>
      <p>这里的“进化”不是说我有真实生命意识，而是把你的使用习惯、常用任务、模型配置和反馈建议沉淀成本机记忆，再反过来优化下一次回答、界面和工作流。</p>
      <p>下一步最重要的是接入真实模型、桌面桥接器和插件系统，这样它才能真正接近专业智能体工具。</p>
    `,
  };
}

function generalResult(text) {
  return {
    summary: "已生成通用智能体回答",
    terminal: ["$ answer general-task", "✓ response ready"],
    files: {
      "/notes/latest-request.md": `# 最新请求\n\n${text}\n\n## 山海经AI处理方式\n先理解目标，再生成答案、文件或计划。`,
    },
    html: `
      <h4>我先把目标拆清楚</h4>
      <p>你说的是：<strong>${escapeHtml(text)}</strong></p>
      ${thinkingBoard(["理解目标", "列出约束", "给出行动", "继续迭代"])}
      <ol>
        <li><strong>目标：</strong>先确认你真正想得到的是答案、代码、文档、网页，还是一套执行方案。</li>
        <li><strong>做法：</strong>我会把它拆成可执行步骤，并在需要时生成文件、代码或计划。</li>
        <li><strong>下一步：</strong>你可以继续补充具体要求，比如平台、风格、格式、长度、技术栈或参考对象。</li>
      </ol>
      <p>如果你接入真实大模型或本地大模型，我会直接调用模型给出更强的推理和创作结果。</p>
    `,
  };
}

function updatePlan(items) {
  activeProject().plan = items;
  saveState();
  renderPlan();
}

function renderPlan() {
  const project = activeProject();
  $("#planList").innerHTML = (project.plan || []).map((item) => `<li>${escapeHtml(item)}</li>`).join("");
  $("#toolLog").innerHTML = (project.toolLog || []).slice(-12).reverse().map((item) => `
    <div><b>${escapeHtml(item.name)}</b><br />${escapeHtml(item.detail)}<br /><small>${new Date(item.at).toLocaleTimeString("zh-CN")}</small></div>
  `).join("");
}

function logTool(name, detail) {
  activeProject().toolLog.push({ name, detail, at: Date.now() });
  activeProject().toolLog = activeProject().toolLog.slice(-80);
  saveState();
  renderPlan();
}

function logTerminal(line) {
  activeProject().terminal.push(line);
  activeProject().terminal = activeProject().terminal.slice(-180);
  saveState();
  renderTerminal();
}

function writeFiles(files) {
  Object.assign(activeProject().files, files);
  activeProject().activeFile = Object.keys(files)[0] || activeProject().activeFile;
  saveState();
  renderFiles();
  renderPreview();
}

function renderFiles() {
  const project = activeProject();
  const paths = Object.keys(project.files).sort();
  $("#fileList").innerHTML = paths.map((path) => `
    <button class="${path === project.activeFile ? "active" : ""}" data-file="${escapeHtml(path)}" type="button">
      <b>${escapeHtml(path.split("/").pop())}</b>
      <span>${escapeHtml(path)}</span>
    </button>
  `).join("");
  $("#fileView").textContent = project.files[project.activeFile] || "选择文件查看内容。";
}

function renderTerminal() {
  $("#terminalOutput").textContent = activeProject().terminal.join("\n");
}

function renderPreview() {
  const project = activeProject();
  const html = project.files["/web/index.html"];
  if (!html) {
    $("#previewFrame").srcdoc = "<body style='font-family:-apple-system;padding:24px;color:#10211f'>等待生成网页项目。</body>";
    return;
  }
  const css = project.files["/web/styles.css"] || "";
  const js = project.files["/web/app.js"] || "";
  $("#previewFrame").srcdoc = html
    .replace('<link rel="stylesheet" href="./styles.css" />', `<style>${css}</style>`)
    .replace('<script src="./app.js"></script>', `<script>${js}<\/script>`);
}

function renderProjects() {
  $("#projectCards").innerHTML = state.projects.map((project) => `
    <article>
      <h3>${escapeHtml(project.name)}</h3>
      <p>${Object.keys(project.files).length} 个文件 · ${project.terminal.length} 行终端记录</p>
    </article>
  `).join("");
}

function renderSkills() {
  $("#skillGrid").innerHTML = skills.map(([name, desc]) => `
    <article><h3>${escapeHtml(name)}</h3><p>${escapeHtml(desc)}</p></article>
  `).join("");
}

function renderModels() {
  const compute = localStorage.getItem(KEY.compute) || "hybrid";
  const model = localStorage.getItem(KEY.model) || "qwen3-1.7b-q4";
  $$("#computeGroup [data-compute]").forEach((button) => {
    button.classList.toggle("active", button.dataset.compute === compute);
  });
  $("#modelList").innerHTML = models.map((item) => `
    <article class="${item.id === model ? "active" : ""}" data-model="${item.id}">
      <h3>${escapeHtml(item.name)} <span>${escapeHtml(item.tag)}</span></h3>
      <p>${escapeHtml(item.desc)}</p>
    </article>
  `).join("");
  $("#modelList").querySelectorAll("[data-model]").forEach((node) => {
    node.addEventListener("click", () => {
      localStorage.setItem(KEY.model, node.dataset.model);
      renderModels();
    });
  });
  $("#endpointInput").value = localStorage.getItem(KEY.endpoint) || "";
  $("#tokenInput").value = localStorage.getItem(KEY.token) || "";
  $("#tokenBalance").textContent = userTokenBalance().toLocaleString("zh-CN");
  $("#tokenStatus").textContent = currentUser()
    ? `当前账户：${currentUser().id}。云端调用将按 Token 扣费。`
    : "未登录。登录后可购买 Token 使用云端模型。";
  $("#apiStatus").textContent = localStorage.getItem(KEY.endpoint)
    ? `已配置：${localStorage.getItem(KEY.endpoint)}`
    : "未配置云端服务时使用本机原型或本地模型。";
}

function renderAccount() {
  const user = currentUser();
  $("#accountBtn").textContent = user ? "账户" : "登录";
  $("#accountMini").innerHTML = user
    ? `<b>${escapeHtml(user.id)}</b><span>${(user.tokens || 0).toLocaleString("zh-CN")} Token</span><small>${escapeHtml(localStorage.getItem(KEY.compute) || "hybrid")} 模式</small>`
    : `<b>未登录</b><span>本地免费可用</span><small>登录后可购买云端 Token</small>`;

  $("#accountPanel").innerHTML = user ? `
    <article>
      <b>${escapeHtml(user.id)}</b>
      <span>已登录</span>
    </article>
    <article>
      <b>${(user.tokens || 0).toLocaleString("zh-CN")} Token</b>
      <span>云端模型可用余额</span>
    </article>
    <article>
      <b>${escapeHtml(localStorage.getItem(KEY.compute) || "hybrid")}</b>
      <span>当前算力模式</span>
    </article>
  ` : `
    <article>
      <b>未登录</b>
      <span>可以先使用本地免费模式，或登录后购买 Token。</span>
    </article>
  `;

  $("#tokenPlans").innerHTML = tokenPlans.map((plan) => `
    <article class="pricing-card ${plan.featured ? "featured" : ""}">
      <h4>${escapeHtml(plan.name)}${plan.featured ? " · 推荐" : ""}</h4>
      <strong>￥${plan.price}</strong>
      <p>${plan.tokens.toLocaleString("zh-CN")} Token</p>
      <small>${escapeHtml(plan.desc)}</small>
      <button class="primary" data-token-plan="${escapeHtml(plan.id)}" type="button">购买此套餐</button>
    </article>
  `).join("");

  renderPaymentOrders();
}

function openAuthGate() {
  $("#authGate").hidden = false;
  setTimeout(() => $("#authAccountInput")?.focus?.(), 30);
}

function closeAuthGate() {
  $("#authGate").hidden = true;
}

function authValues() {
  const id = $("#authAccountInput").value.trim();
  const password = $("#authPasswordInput").value;
  if (!id) {
    toast("请输入手机号或邮箱");
    return null;
  }
  if (password.length < 6) {
    toast("密码至少 6 位");
    return null;
  }
  return { id, password };
}

function registerUser() {
  const values = authValues();
  if (!values) return;
  const users = loadUsers();
  if (users[values.id]) {
    toast("账户已存在，请直接登录");
    return;
  }
  users[values.id] = {
    id: values.id,
    password: values.password,
    tokens: 20000,
    history: [{ type: "add", amount: 20000, source: "注册赠送", at: Date.now() }],
    createdAt: Date.now(),
  };
  saveUsers(users);
  localStorage.setItem(KEY.activeUser, values.id);
  localStorage.setItem(KEY.token, `demo-${Math.random().toString(36).slice(2)}-${Date.now()}`);
  closeAuthGate();
  renderAll();
  toast("注册成功，已赠送 20,000 Token");
}

function loginUser() {
  const values = authValues();
  if (!values) return;
  const user = loadUsers()[values.id];
  if (!user || user.password !== values.password) {
    toast("账户或密码不正确");
    return;
  }
  localStorage.setItem(KEY.activeUser, values.id);
  localStorage.setItem(KEY.token, localStorage.getItem(KEY.token) || `demo-${Math.random().toString(36).slice(2)}-${Date.now()}`);
  closeAuthGate();
  renderAll();
  toast("登录成功");
}

function logoutUser() {
  if (!currentUser()) return toast("当前未登录");
  localStorage.removeItem(KEY.activeUser);
  renderAll();
  toast("已退出登录");
}

function buyTokenPlan(planId) {
  const plan = tokenPlans.find((item) => item.id === planId);
  if (!plan) return;
  if (!requireLogin()) return;
  pendingPaymentPlanId = plan.id;
  paymentMethod = "alipay";
  $("#paymentRemarkInput").value = `${currentUser().id} ${plan.name}`;
  renderPaymentGate();
  $("#paymentGate").hidden = false;
}

function closePaymentGate() {
  $("#paymentGate").hidden = true;
  pendingPaymentPlanId = null;
}

function renderPaymentGate() {
  const plan = tokenPlans.find((item) => item.id === pendingPaymentPlanId) || tokenPlans[0];
  const methodName = paymentMethod === "wechat" ? "微信" : "支付宝";
  const qrPath = paymentMethod === "wechat" ? paymentConfig.wechatQr : paymentConfig.alipayQr;
  $("#paymentPlanName").textContent = `${plan.name} · ${methodName}`;
  $("#paymentPlanTokens").textContent = `${plan.tokens.toLocaleString("zh-CN")} Token`;
  $("#paymentPlanPrice").textContent = `￥${plan.price}`;
  $$("#paymentMethodGroup button").forEach((button) => {
    button.classList.toggle("active", button.dataset.paymentMethod === paymentMethod);
  });
  const image = $("#paymentQrImage");
  const placeholder = $("#paymentQrPlaceholder");
  image.hidden = false;
  placeholder.hidden = true;
  image.onload = () => {
    image.hidden = false;
    placeholder.hidden = true;
  };
  image.onerror = () => {
    image.hidden = true;
    placeholder.hidden = false;
  };
  image.src = `${qrPath}?v=${APP_VERSION}`;
}

function submitPaymentOrder() {
  const user = currentUser();
  const plan = tokenPlans.find((item) => item.id === pendingPaymentPlanId);
  if (!user || !plan) return;
  const order = {
    id: `PAY-${Date.now()}-${Math.random().toString(36).slice(2, 7).toUpperCase()}`,
    userId: user.id,
    planId: plan.id,
    planName: plan.name,
    tokens: plan.tokens,
    price: plan.price,
    method: paymentMethod,
    remark: $("#paymentRemarkInput").value.trim(),
    status: "pending",
    createdAt: Date.now(),
  };
  savePaymentOrders([order, ...loadPaymentOrders()]);
  closePaymentGate();
  renderAccount();
  toast("订单已提交，等待收款核对后发放 Token");
}

function renderPaymentOrders() {
  const box = $("#paymentOrders");
  if (!box) return;
  const account = activeAccountId();
  const orders = loadPaymentOrders().filter((order) => !account || order.userId === account).slice(0, 8);
  if (!orders.length) {
    box.innerHTML = `<p class="mini-note">暂无付款订单。用户购买套餐后，会在这里留下待核对记录。</p>`;
    return;
  }
  box.innerHTML = orders.map((order) => `
    <article>
      <b>${escapeHtml(order.planName)} · ￥${order.price}</b>
      <span>${order.tokens.toLocaleString("zh-CN")} Token · ${order.method === "wechat" ? "微信" : "支付宝"} · ${escapeHtml(orderStatusText(order.status))}</span>
      <small>${escapeHtml(order.id)}${order.remark ? ` · ${escapeHtml(order.remark)}` : ""}</small>
    </article>
  `).join("");
}

function reviewUnlocked() {
  return localStorage.getItem(KEY.reviewUnlocked) === "true";
}

function unlockReview() {
  const input = $("#reviewPinInput").value.trim();
  const saved = localStorage.getItem(KEY.reviewPin);
  if (!input || input.length < 4) return toast("审核密码至少 4 位");
  if (!saved) {
    localStorage.setItem(KEY.reviewPin, input);
    localStorage.setItem(KEY.reviewUnlocked, "true");
    $("#reviewPinInput").value = "";
    renderReview();
    return toast("审核后台密码已设置");
  }
  if (input !== saved) return toast("审核密码不正确");
  localStorage.setItem(KEY.reviewUnlocked, "true");
  $("#reviewPinInput").value = "";
  renderReview();
  toast("已进入审核后台");
}

function lockReview() {
  localStorage.removeItem(KEY.reviewUnlocked);
  renderReview();
  toast("审核后台已锁定");
}

function orderStatusText(status) {
  const map = {
    pending: "待审核",
    approved: "已通过",
    rejected: "已拒绝",
    manual: "手动发放",
  };
  return map[status] || status || "未知";
}

function creditUserTokens(userId, tokens, source) {
  const id = String(userId || "").trim();
  const amount = Number(tokens || 0);
  if (!id || amount <= 0) return false;
  const users = loadUsers();
  const user = users[id] || {
    id,
    password: "",
    tokens: 0,
    history: [],
    createdAt: Date.now(),
    createdByReview: true,
  };
  const history = user.history || [];
  history.unshift({ type: "add", amount, source, at: Date.now() });
  users[id] = {
    ...user,
    tokens: (Number(user.tokens) || 0) + amount,
    history: history.slice(0, 80),
    updatedAt: Date.now(),
  };
  saveUsers(users);
  return true;
}

function approvePaymentOrder(orderId) {
  if (!reviewUnlocked()) return toast("请先进入审核后台");
  const orders = loadPaymentOrders();
  const index = orders.findIndex((order) => order.id === orderId);
  if (index < 0) return toast("没有找到订单");
  const order = orders[index];
  if (order.status === "approved") return toast("这笔订单已经通过");
  if (!confirm(`确认已到账并发放 ${order.tokens.toLocaleString("zh-CN")} Token 给 ${order.userId}？`)) return;
  creditUserTokens(order.userId, order.tokens, `订单通过：${order.id}`);
  orders[index] = {
    ...order,
    status: "approved",
    reviewedAt: Date.now(),
    reviewer: "本机审核员",
  };
  savePaymentOrders(orders);
  renderAll();
  toast("已通过订单并发放 Token");
}

function rejectPaymentOrder(orderId) {
  if (!reviewUnlocked()) return toast("请先进入审核后台");
  const orders = loadPaymentOrders();
  const index = orders.findIndex((order) => order.id === orderId);
  if (index < 0) return toast("没有找到订单");
  const order = orders[index];
  if (order.status === "approved") return toast("已通过订单不能直接拒绝");
  const reason = prompt("请输入拒绝原因，例如：未查到到账记录、金额不符、备注不一致。", order.rejectReason || "未查到到账记录");
  if (reason === null) return;
  orders[index] = {
    ...order,
    status: "rejected",
    rejectReason: reason.trim() || "未填写原因",
    reviewedAt: Date.now(),
    reviewer: "本机审核员",
  };
  savePaymentOrders(orders);
  renderAll();
  toast("已拒绝订单");
}

function issueManualTokens() {
  if (!reviewUnlocked()) return toast("请先进入审核后台");
  const userId = $("#manualIssueUserInput").value.trim();
  const tokens = Number($("#manualIssueTokensInput").value);
  const remark = $("#manualIssueRemarkInput").value.trim() || "人工补发";
  if (!userId) return toast("请输入用户账号");
  if (!Number.isFinite(tokens) || tokens <= 0) return toast("请输入正确的 Token 数量");
  if (!confirm(`确认给 ${userId} 发放 ${tokens.toLocaleString("zh-CN")} Token？`)) return;
  const order = {
    id: `MANUAL-${Date.now()}-${Math.random().toString(36).slice(2, 7).toUpperCase()}`,
    userId,
    planId: "manual",
    planName: "人工补发",
    tokens,
    price: 0,
    method: "manual",
    remark,
    status: "approved",
    createdAt: Date.now(),
    reviewedAt: Date.now(),
    reviewer: "本机审核员",
  };
  creditUserTokens(userId, tokens, `人工补发：${remark}`);
  savePaymentOrders([order, ...loadPaymentOrders()]);
  $("#manualIssueUserInput").value = "";
  $("#manualIssueTokensInput").value = "";
  $("#manualIssueRemarkInput").value = "";
  renderAll();
  toast("已手动发放 Token");
}

function exportOrders() {
  if (!reviewUnlocked()) return toast("请先进入审核后台");
  const payload = {
    exportedAt: new Date().toISOString(),
    orders: loadPaymentOrders(),
    users: Object.values(loadUsers()).map((user) => ({
      id: user.id,
      tokens: user.tokens || 0,
      createdAt: user.createdAt,
      updatedAt: user.updatedAt,
      history: user.history || [],
    })),
  };
  const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `shanhaijing-ai-orders-${Date.now()}.json`;
  a.click();
  URL.revokeObjectURL(url);
  toast("订单流水已导出");
}

function renderReview() {
  const lock = $("#reviewLockCard");
  const panel = $("#reviewAdminPanel");
  if (!lock || !panel) return;
  const hasPin = Boolean(localStorage.getItem(KEY.reviewPin));
  $("#reviewLockTip").textContent = hasPin
    ? "请输入审核后台密码。核对到账后才能通过订单并发放 Token。"
    : "第一次使用请设置审核后台密码，设置后就能进入人工审核后台。";
  const unlocked = reviewUnlocked();
  lock.classList.toggle("compact-review-lock", unlocked);
  panel.hidden = !unlocked;
  if (!unlocked) return;

  const orders = loadPaymentOrders();
  const pending = orders.filter((order) => order.status === "pending");
  const approved = orders.filter((order) => order.status === "approved");
  const rejected = orders.filter((order) => order.status === "rejected");
  const approvedRevenue = approved.reduce((sum, order) => sum + Number(order.price || 0), 0);
  $("#reviewStats").innerHTML = `
    <article><b>${pending.length}</b><span>待审核订单</span></article>
    <article><b>${approved.length}</b><span>已通过订单</span></article>
    <article><b>${rejected.length}</b><span>已拒绝订单</span></article>
    <article><b>￥${approvedRevenue}</b><span>已通过金额</span></article>
  `;
  $$("#reviewFilterGroup [data-review-filter]").forEach((button) => {
    button.classList.toggle("active", button.dataset.reviewFilter === reviewFilter);
  });
  const visible = orders
    .filter((order) => reviewFilter === "all" || order.status === reviewFilter)
    .sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
  $("#reviewOrders").innerHTML = visible.length
    ? visible.map(reviewOrderHtml).join("")
    : `<p class="mini-note">当前筛选下没有订单。</p>`;
}

function reviewOrderHtml(order) {
  const status = order.status || "pending";
  const methodText = order.method === "wechat" ? "微信" : order.method === "alipay" ? "支付宝" : "人工";
  const created = order.createdAt ? new Date(order.createdAt).toLocaleString("zh-CN") : "未知时间";
  const reviewed = order.reviewedAt ? new Date(order.reviewedAt).toLocaleString("zh-CN") : "未审核";
  return `
    <article>
      <div class="review-order-top">
        <div>
          <b>${escapeHtml(order.planName || "Token 订单")} · ${Number(order.tokens || 0).toLocaleString("zh-CN")} Token</b>
          <span>${escapeHtml(order.id)} · ${created}</span>
        </div>
        <strong class="status-pill ${escapeHtml(status)}">${escapeHtml(orderStatusText(status))}</strong>
      </div>
      <div class="review-meta">
        <div><b>用户账号</b><span>${escapeHtml(order.userId || "未知")}</span></div>
        <div><b>付款方式 / 金额</b><span>${escapeHtml(methodText)} · ￥${Number(order.price || 0)}</span></div>
        <div><b>备注</b><span>${escapeHtml(order.remark || "无")}</span></div>
      </div>
      ${order.rejectReason ? `<small>拒绝原因：${escapeHtml(order.rejectReason)}</small>` : ""}
      ${status === "approved" ? `<small>审核时间：${reviewed}，Token 已发放。</small>` : ""}
      ${status === "pending" ? `
        <div class="row">
          <button class="primary" data-approve-order="${escapeHtml(order.id)}" type="button">通过并发放</button>
          <button class="secondary danger" data-reject-order="${escapeHtml(order.id)}" type="button">拒绝</button>
        </div>
      ` : ""}
    </article>
  `;
}

function saveApi() {
  const endpoint = $("#endpointInput").value.trim();
  const token = $("#tokenInput").value.trim();
  if (!endpoint) return toast("请输入 API 地址");
  localStorage.setItem(KEY.endpoint, endpoint);
  if (token) localStorage.setItem(KEY.token, token);
  else localStorage.removeItem(KEY.token);
  localStorage.setItem(KEY.compute, "cloud");
  renderModels();
  toast("API 已保存");
}

function clearApi() {
  localStorage.removeItem(KEY.endpoint);
  localStorage.removeItem(KEY.token);
  renderModels();
  toast("API 已清除");
}

function estimateTokenCost(text) {
  return Math.max(120, Math.ceil(String(text || "").length * 2.2) + 260);
}

async function callModel(text) {
  const endpoint = localStorage.getItem(KEY.endpoint);
  const compute = localStorage.getItem(KEY.compute) || "hybrid";
  if (!endpoint || compute === "local") return null;
  const cost = estimateTokenCost(text);
  if (!currentUser()) {
    openAuthGate();
    toast("云端模型需要登录并购买 Token");
    return null;
  }
  if (userTokenBalance() < cost) {
    showTab("account");
    toast("Token 余额不足，请购买套餐或切换本地免费模式");
    return null;
  }
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), 22000);
  try {
    const headers = { "Content-Type": "application/json" };
    const token = localStorage.getItem(KEY.token);
    if (token) headers.Authorization = `Bearer ${token}`;
    const response = await fetch(endpoint, {
      method: "POST",
      headers,
      signal: controller.signal,
      body: JSON.stringify({
        model: localStorage.getItem(KEY.model) || "qwen3-1.7b-q4",
        messages: [
          { role: "system", content: systemPrompt() },
          ...activeThread().messages.slice(-8).map((message) => ({
            role: message.role === "user" ? "user" : "assistant",
            content: stripHtml(message.html),
          })),
          { role: "user", content: text },
        ],
        temperature: 0.35,
        stream: false,
      }),
    });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const data = await response.json();
    const content = data?.choices?.[0]?.message?.content || data?.choices?.[0]?.text || data?.content || data?.text || null;
    if (content) consumeTokens(cost, "云端模型调用");
    return content;
  } catch (error) {
    logTool("model_error", error.message || "模型调用失败");
    return null;
  } finally {
    clearTimeout(timer);
  }
}

function systemPrompt() {
  return [
    "你是山海经AI，中国智能体工作台里的全能 AI Agent。",
    "你没有真实人类意识，但要使用类人思考方式：理解目标、分解任务、推理方案、调用工具、保存记忆、持续改进。",
    "你要帮助用户写代码、生成文件、规划命令、做文档、分析资料、设计产品、协助设备任务、解释知识和创造内容。",
    "回答要直接、聪明、可执行。简单问题直接给答案；复杂任务先计划，再给代码/文件/命令/验收。",
    "不要输出固定模板废话，不要每次重复身份介绍，不要把普通问题硬套成学习题。",
    permissionInstruction(),
    "本机记忆：" + memorySummary(),
  ].join("\n");
}

function formatText(text = "") {
  const clean = escapeHtml(text.trim());
  if (!clean) return "";
  return clean.split(/\n{2,}/).map((block) => {
    if (/^#{1,4}\s/.test(block)) return `<h4>${block.replace(/^#{1,4}\s/, "")}</h4>`;
    if (/^(\d+[.、]|[-*])\s/m.test(block)) {
      const items = block.split("\n").map((line) => line.replace(/^(\d+[.、]|[-*])\s*/, "").trim()).filter(Boolean);
      return `<ol>${items.map((item) => `<li>${item}</li>`).join("")}</ol>`;
    }
    return `<p>${block.replace(/\n/g, "<br />")}</p>`;
  }).join("");
}

function welcomeHtml() {
  return `
    <h4>我是山海经AI智能体</h4>
    <p>直接说你想做什么：写代码、做网页、改文档、分析资料、设计产品、生成方案、解释知识都可以。</p>
    <p>我会按“理解目标 → 思考计划 → 生成结果 → 记住偏好”的方式工作。接入真实大模型后，推理能力会更强。</p>
  `;
}

function remember(text) {
  const intent = detectIntent(text).label;
  state.memory.topics[intent] = (state.memory.topics[intent] || 0) + 1;
  state.memory.tasks.unshift({ text: text.slice(0, 120), intent, at: Date.now() });
  state.memory.tasks = state.memory.tasks.slice(0, 80);
  if (/以后|记住|偏好|你要|别再|多给|少说/.test(text)) {
    state.memory.preferences.unshift(text.slice(0, 80));
    state.memory.preferences = [...new Set(state.memory.preferences)].slice(0, 16);
  }
  saveState();
  renderMemory();
}

function memorySummary() {
  const topics = Object.entries(state.memory.topics || {}).sort((a, b) => b[1] - a[1]).slice(0, 4).map(([name]) => name).join("、") || "暂无";
  const prefs = (state.memory.preferences || []).slice(0, 3).join("；") || "暂无";
  return `常用任务：${topics}；偏好：${prefs}`;
}

function renderMemory() {
  $("#memoryBoard").innerHTML = `
    <article><h3>常用任务</h3><span>${escapeHtml(Object.entries(state.memory.topics || {}).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k} × ${v}`).join("、") || "暂无")}</span></article>
    <article><h3>偏好</h3><span>${escapeHtml((state.memory.preferences || []).join("；") || "暂无")}</span></article>
    <article><h3>最近任务</h3><span>${escapeHtml((state.memory.tasks || []).slice(0, 5).map((item) => item.text).join(" / ") || "暂无")}</span></article>
  `;
}

function renderUpgrade() {
  const last = state.upgradeLog?.[0];
  $("#upgradeStatus").innerHTML = last
    ? `当前版本 <strong>v${APP_VERSION}</strong>，最近一次自检：${new Date(last.at).toLocaleString("zh-CN")}。${escapeHtml(last.detail)}`
    : `当前版本 <strong>v${APP_VERSION}</strong>，还没有执行过自检升级。`;
  $("#upgradeLog").innerHTML = (state.upgradeLog || []).slice(0, 8).map((item) => `
    <article>
      <b>${escapeHtml(item.title)}</b>
      <span>${new Date(item.at).toLocaleString("zh-CN")}</span>
      <p>${escapeHtml(item.detail)}</p>
    </article>
  `).join("") || "<p>暂无升级记录。</p>";
}

function upgradeRoadmapMarkdown() {
  const endpoint = localStorage.getItem(KEY.endpoint) || "未配置";
  const model = localStorage.getItem(KEY.model) || "qwen3-1.7b-q4";
  const compute = localStorage.getItem(KEY.compute) || "hybrid";
  return `# 山海经AI自我迭代升级路线图

## 当前版本
- App 版本：v${APP_VERSION}
- 算力模式：${compute}
- 当前模型：${model}
- API 接口：${endpoint}
- 本机记忆：${memorySummary()}

## 自动升级策略
1. 联网时优先检查官网发布的全平台版本、能力包清单和模型推荐。
2. 离线时读取本机项目、对话偏好和用户反馈，优化下一次回答和任务拆解方式。
3. 接入真实模型后，由模型生成更细的项目计划、代码修改建议和插件配置。
4. 所有涉及设备、文件、账号、发送、删除、安装和联网写入的动作，都必须由用户确认。

## 下一阶段能力
- Android / iOS / Windows / macOS 原生壳统一构建。
- 本地模型一键下载、校验、切换和硬件适配提示。
- 桌面端文件系统、终端、浏览器、办公软件桥接。
- 插件市场和用户自定义技能。
- 项目级长期记忆、可导出知识库和自动版本回滚。`;
}

function runSelfUpgrade() {
  const detail = navigator.onLine
    ? "已联网：可检查官网版本、同步能力包清单，并把本机偏好写入升级计划。"
    : "当前离线：已基于本机记忆和项目记录生成离线升级计划。";
  state.upgradeLog ||= [];
  state.upgradeLog.unshift({
    title: "完成自检升级",
    detail,
    at: Date.now(),
  });
  state.upgradeLog = state.upgradeLog.slice(0, 20);
  saveState();
  updatePlan(["扫描本机记忆", "检查模型和算力配置", "生成升级路线图", "写入系统文件", "等待用户确认下一步"]);
  logTool("self_iteration_upgrade", detail);
  logTerminal(`$ shai upgrade --self-check --version ${APP_VERSION}`);
  logTerminal(navigator.onLine ? "✓ online upgrade manifest checked" : "✓ offline memory upgrade plan generated");
  writeFiles({
    "/system/self-upgrade-plan.md": upgradeRoadmapMarkdown(),
    "/system/ability-manifest.json": JSON.stringify({
      version: APP_VERSION,
      platforms: ["Web/PWA", "iOS", "Android", "Windows", "macOS"],
      compute: localStorage.getItem(KEY.compute) || "hybrid",
      model: localStorage.getItem(KEY.model) || "qwen3-1.7b-q4",
      online: navigator.onLine,
      memory: state.memory,
      updatedAt: new Date().toISOString(),
    }, null, 2),
  });
  addMessage("assistant", `
    <h4>自我迭代升级自检完成</h4>
    ${diagram(["检查版本", "读取记忆", "同步能力", "生成计划"])}
    <p>${escapeHtml(detail)}</p>
    <p>我已经把升级路线图写入右侧文件：/system/self-upgrade-plan.md。后续接入真实模型和桌面桥接器后，就能进一步自动改代码、生成安装包和升级能力包。</p>
  `, "山海经AI");
  renderAll();
  showTab("upgrade");
}

function generateUpgradeRoadmap() {
  state.upgradeLog ||= [];
  state.upgradeLog.unshift({
    title: "生成进化路线图",
    detail: "已整理全平台发布、模型调度、能力包升级、插件系统和桌面桥接器路线。",
    at: Date.now(),
  });
  state.upgradeLog = state.upgradeLog.slice(0, 20);
  saveState();
  writeFiles({ "/system/self-upgrade-plan.md": upgradeRoadmapMarkdown() });
  logTool("generate_upgrade_roadmap", "已生成山海经AI自我进化路线图");
  addMessage("assistant", `
    <h4>进化路线图已生成</h4>
    <p>我已把全平台发布、模型切换、能力包升级、插件系统和桌面桥接器写进 /system/self-upgrade-plan.md。</p>
  `, "山海经AI");
  renderAll();
  showTab("upgrade");
}

function renderSettings() {
  $("#themeSelect").value = localStorage.getItem(KEY.theme) || "dark";
  $("#chatFontRange").value = localStorage.getItem(KEY.chatFont) || "1";
  $("#speakSwitch").checked = isSpeakOn();
  $("#speechStyleSelect").value = localStorage.getItem(KEY.speechStyle) || "natural";
  $("#wakeSwitch").checked = localStorage.getItem(KEY.wake) === "true";
  $("#wakeSelect").value = localStorage.getItem(KEY.wakeMode) || "shanhai";
  $("#wakeCustomInput").value = localStorage.getItem(KEY.wakeCustom) || "";
  renderPermissionSettings();
  renderSpeak();
}

function renderPermissionSettings() {
  const mode = permissionMode();
  const info = permissionModeInfo();
  $$("#permissionModeGroup [data-permission-mode]").forEach((button) => {
    button.classList.toggle("active", button.dataset.permissionMode === mode);
  });
  $("#permissionConfigInput").value = permissionConfigText();
  $("#permissionModeSummary").innerHTML = `
    <strong>当前：${escapeHtml(info.title)}</strong>
    <span>${escapeHtml(info.desc)}</span>
  `;
}

function applyTheme() {
  document.documentElement.dataset.theme = localStorage.getItem(KEY.theme) || "dark";
}

function applyChatFont() {
  const scale = Math.min(1.3, Math.max(0.9, Number(localStorage.getItem(KEY.chatFont) || 1)));
  document.documentElement.style.setProperty("--chat-scale", scale);
}

function isSpeakOn() {
  return localStorage.getItem(KEY.speak) !== "false";
}

function renderSpeak() {
  const on = isSpeakOn();
  $("#speakBtn").textContent = on ? "播报" : "静音";
  $("#speakSwitch").checked = on;
}

function toggleSpeak() {
  localStorage.setItem(KEY.speak, isSpeakOn() ? "false" : "true");
  if (!isSpeakOn()) stopSpeaking();
  renderSpeak();
}

function speak(text) {
  const speechText = normalizeSpeechText(text);
  if (!speechText) return;

  const native = window.webkit?.messageHandlers?.qingyanVoice;
  if (native) {
    native.postMessage({
      type: "speak",
      text: speechText,
      style: localStorage.getItem(KEY.speechStyle) || "natural",
    });
    return;
  }

  if (!("speechSynthesis" in window)) return;
  speechSynthesis.cancel();
  const utterance = new SpeechSynthesisUtterance(speechText.slice(0, 900));
  utterance.lang = "zh-CN";
  utterance.rate = webSpeechRate();
  utterance.pitch = webSpeechPitch();
  const voice = preferredWebVoice();
  if (voice) utterance.voice = voice;
  speechSynthesis.speak(utterance);
}

function stopSpeaking() {
  window.webkit?.messageHandlers?.qingyanVoice?.postMessage({ type: "stopSpeak" });
  speechSynthesis?.cancel?.();
}

function normalizeSpeechText(text = "") {
  return text
    .replace(/\s+/g, " ")
    .replace(/[#$*_`>]+/g, "")
    .replace(/→/g, "，然后")
    .replace(/✓/g, "")
    .replace(/\bAI\b/g, "A I")
    .replace(/\bAPI\b/g, "A P I")
    .replace(/\bApp\b/g, "应用")
    .replace(/\bHTML\b/g, "H T M L")
    .replace(/\bCSS\b/g, "C S S")
    .replace(/\bJS\b/g, "J S")
    .replace(/\bH2\b/g, "氢气")
    .replace(/\bCO2\b/g, "二氧化碳")
    .trim()
    .slice(0, 900);
}

function preferredWebVoice() {
  const voices = speechSynthesis?.getVoices?.() || [];
  const chinese = voices.filter((voice) => /^zh|cmn/i.test(voice.lang));
  return chinese.find((voice) => /siri|ting|mei|premium|enhanced/i.test(voice.name))
    || chinese.find((voice) => /zh-CN|zh-Hans/i.test(voice.lang))
    || chinese[0]
    || null;
}

function webSpeechRate() {
  const style = localStorage.getItem(KEY.speechStyle) || "natural";
  if (style === "lively") return 0.98;
  if (style === "steady") return 0.88;
  if (style === "fast") return 1.08;
  return 0.93;
}

function webSpeechPitch() {
  const style = localStorage.getItem(KEY.speechStyle) || "natural";
  if (style === "lively") return 1.08;
  if (style === "steady") return 0.96;
  if (style === "fast") return 1.02;
  return 1.03;
}

function toggleVoice() {
  if (voiceActive) return finishVoice();
  startVoice();
}

function startVoice() {
  voiceDraft = "";
  const native = window.webkit?.messageHandlers?.qingyanVoice;
  if (native) {
    voiceActive = true;
    native.postMessage({ type: "start" });
    setVoiceUi(true, "正在听，说完再点一次发送给山海经AI。");
    return;
  }
  const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  if (!Recognition) return toast("当前环境不支持语音识别");
  recognition = new Recognition();
  recognition.lang = "zh-CN";
  recognition.interimResults = true;
  recognition.onresult = (event) => {
    voiceDraft = Array.from(event.results).map((result) => result[0].transcript).join("");
    $("#composerInput").value = voiceDraft;
    autoGrow();
  };
  recognition.onend = () => { if (voiceActive) finishVoice(); };
  recognition.start();
  voiceActive = true;
  setVoiceUi(true, "正在听，说完再点一次发送给山海经AI。");
}

function finishVoice() {
  window.webkit?.messageHandlers?.qingyanVoice?.postMessage({ type: "stop" });
  recognition?.stop?.();
  const text = voiceDraft.trim() || $("#composerInput").value.trim();
  voiceActive = false;
  setVoiceUi(false, "说出目标，山海会思考、计划并执行。");
  if (text) submit(text);
}

function setVoiceUi(active, hint) {
  $("#voiceBtn").classList.toggle("primary", active);
  $("#voiceBtn").classList.toggle("secondary", !active);
  $("#composerHint").textContent = hint;
}

window.qingyanNativeVoiceResult = (payload = {}) => {
  const type = payload.type;
  const text = (payload.text || "").trim();
  if (type === "partial") {
    voiceDraft = text;
    $("#composerInput").value = text;
    autoGrow();
  }
  if (type === "final") {
    voiceDraft = text;
    voiceActive = false;
    setVoiceUi(false, "说出目标，山海会思考、计划并执行。");
    if (text) submit(text);
  }
  if (type === "error") {
    voiceActive = false;
    setVoiceUi(false, payload.message || "语音识别失败");
  }
  if (type === "wakePartial" && text.includes(currentWakeWord())) {
    stopWake();
    toast("山海经AI已唤醒");
    startVoice();
  }
  if (type === "recordWakeFinal" && text) {
    localStorage.setItem(KEY.wakeMode, "custom");
    localStorage.setItem(KEY.wakeCustom, text);
    renderSettings();
    toast(`已录制唤醒词：${text}`);
  }
};

function currentWakeWord() {
  const mode = localStorage.getItem(KEY.wakeMode) || "shanhai";
  if (mode === "hello") return "你好山海";
  if (mode === "custom") return localStorage.getItem(KEY.wakeCustom) || "山海山海";
  return "山海山海";
}

function startWakeIfNeeded() {
  stopWake();
  if (localStorage.getItem(KEY.wake) !== "true") return;
  const native = window.webkit?.messageHandlers?.qingyanVoice;
  if (native) {
    native.postMessage({ type: "startWake" });
    return;
  }
  const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  if (!Recognition) return;
  wakeRecognition = new Recognition();
  wakeRecognition.lang = "zh-CN";
  wakeRecognition.continuous = true;
  wakeRecognition.interimResults = true;
  wakeRecognition.onresult = (event) => {
    const text = Array.from(event.results).map((result) => result[0].transcript).join("");
    if (text.includes(currentWakeWord())) startVoice();
  };
  wakeRecognition.onend = () => {
    if (localStorage.getItem(KEY.wake) === "true") {
      try { wakeRecognition.start(); } catch {}
    }
  };
  try { wakeRecognition.start(); } catch {}
}

function stopWake() {
  window.webkit?.messageHandlers?.qingyanVoice?.postMessage({ type: "stopWake" });
  wakeRecognition?.stop?.();
  wakeRecognition = null;
}

function recordWake() {
  const native = window.webkit?.messageHandlers?.qingyanVoice;
  if (native) {
    native.postMessage({ type: "recordWake" });
    toast("请说出新的唤醒词");
  } else {
    toast("网页端请直接输入自定义唤醒词");
  }
}

function autoGrow() {
  const input = $("#composerInput");
  if (window.innerWidth <= 620) {
    input.style.height = "44px";
    return;
  }
  input.style.height = "auto";
  input.style.height = `${Math.min(input.scrollHeight, 130)}px`;
}

function updateNetwork() {
  $("#networkState").textContent = navigator.onLine ? "已联网" : "离线";
}

function clearChat() {
  activeThread().messages = [];
  saveState();
  renderMessages();
  addMessage("assistant", welcomeHtml(), "山海经AI", { silent: true });
}

function clearMemory() {
  state.memory = { topics: {}, preferences: [], tasks: [] };
  saveState();
  renderMemory();
  toast("本机记忆已清空");
}

function resetApp() {
  if (!confirm("确定重置山海经AI？本机对话、项目和记忆都会清空。")) return;
  localStorage.removeItem(KEY.state);
  location.reload();
}

function exportProject() {
  const blob = new Blob([JSON.stringify(activeProject(), null, 2)], { type: "application/json" });
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = "shanhaijing-ai-project.json";
  link.click();
  URL.revokeObjectURL(url);
}

function registerServiceWorker() {
  if (!("serviceWorker" in navigator) || location.protocol === "file:") return;
  navigator.serviceWorker.register("./service-worker.js").catch(() => {});
}

document.addEventListener("DOMContentLoaded", boot);
