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",
  localModels: "shai-local-models-v1",
  modelCatalog: "shai-model-catalog-v2",
  reasoning: "shai-reasoning",
  theme: "shai-theme",
  chatFont: "shai-chat-font",
  speak: "shai-speak",
  speechStyle: "shai-speech-style",
  workMode: "shai-work-mode",
  defaultTarget: "shai-default-target",
  appLanguage: "shai-app-language",
  sidebarDisplay: "shai-sidebar-display",
  autoReview: "shai-auto-review",
  hooks: "shai-hooks",
  connectors: "shai-connectors",
  browserBridge: "shai-browser-bridge",
  computerBridge: "shai-computer-bridge",
  gitMode: "shai-git-mode",
  environmentProfile: "shai-environment-profile",
  archivePolicy: "shai-archive-policy",
  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",
  supportTickets: "shai-support-tickets-v1",
  updateDismissedVersion: "shai-update-dismissed-version-v1",
  lastUpdateCheck: "shai-last-update-check-v1",
};

const APP_VERSION = "1.6.6";
const OFFICIAL_APP_URL = "https://shanhaijingai.pages.dev/webapp/";
const VERSION_MANIFEST_URL = "https://shanhaijingai.pages.dev/webapp/version.json";
const UPDATE_CHECK_INTERVAL = 4 * 60 * 60 * 1000;

const BUILT_IN_MODELS = [
  { id: "shai-5.5-agent", name: "山海 5.5 Agent 云端旗舰", short: "5.5", tag: "旗舰", type: "cloud", group: "shai", provider: "山海经AI", region: "平台", access: "platform", desc: "默认 Agent 核心，适合大型项目、代码工程、复杂办公和多步骤推理。" },
  { id: "openai-gpt-series", name: "OpenAI GPT 系列", short: "GPT", tag: "全能", type: "cloud", group: "cloud-global", provider: "OpenAI", region: "国际", access: "api", desc: "适合通用对话、代码、文档、图像理解和复杂工具调用，用户可自行接入 API。" },
  { id: "anthropic-claude-series", name: "Anthropic Claude 系列", short: "Claude", tag: "长文", type: "cloud", group: "cloud-global", provider: "Anthropic", region: "国际", access: "api", desc: "适合长文档、代码审查、写作润色和复杂项目规划，用户可自行接入 API。" },
  { id: "google-gemini-series", name: "Google Gemini 系列", short: "Gemini", tag: "多模态", type: "cloud", group: "cloud-global", provider: "Google", region: "国际", access: "api", desc: "适合多模态理解、资料整理、办公和跨设备场景，用户可自行接入 API。" },
  { id: "deepseek-r1", name: "DeepSeek R1 推理模型", short: "R1", tag: "推理", type: "cloud", group: "reasoning-code", provider: "DeepSeek", region: "中国/国际", access: "api", desc: "适合长链路推理、方案比较、代码审查和复杂决策。" },
  { id: "qwen3-max", name: "Qwen3 Max 云端模型", short: "Qwen", tag: "中文", type: "cloud", group: "cn-office", provider: "Qwen", region: "中国/国际", access: "api", desc: "适合中文办公、创作、资料整理和通用问答。" },
  { id: "qwen3-1.7b-q4", name: "Qwen 轻量本地模型", short: "1.7B", tag: "推荐", type: "local", group: "local-light", provider: "Qwen", region: "中国/国际", sizeGb: 1.4, minMemory: 2, minCores: 2, rank: 10, version: "2026.06", desc: "适合手机端离线对话和轻量代码辅助。" },
  { id: "llama-3.2-3b-q4", name: "Llama 轻量本地模型", short: "Llama", tag: "全球", type: "local", group: "local-light", provider: "Meta", region: "国际", sizeGb: 2.2, minMemory: 4, minCores: 4, rank: 14, version: "2026.06", desc: "适合英文资料、轻量代码、通用问答和离线实验。" },
  { id: "phi-4-mini-q4", name: "Phi Mini 本地模型", short: "Phi", tag: "轻巧", type: "local", group: "local-light", provider: "Microsoft", region: "国际", sizeGb: 2.4, minMemory: 4, minCores: 4, rank: 15, version: "2026.06", desc: "适合低配置设备、快速问答、摘要和轻量办公。" },
  { id: "glm-edge-q4", name: "GLM 边端模型", short: "GLM", tag: "中文", type: "local", group: "cn-office", provider: "GLM", region: "中国/国际", sizeGb: 2.6, minMemory: 4, minCores: 4, rank: 18, version: "2026.06", desc: "适合中文办公、摘要和对话。" },
  { id: "gemma-4b-q4", name: "Gemma 标准本地模型", short: "Gemma", tag: "均衡", type: "local", group: "local-standard", provider: "Google", region: "国际", sizeGb: 3.1, minMemory: 5, minCores: 4, rank: 19, version: "2026.06", desc: "适合通用问答、资料总结、学习研究和轻量代码。" },
  { id: "qwen3-4b-q4", name: "Qwen 标准本地模型", short: "4B", tag: "强", type: "local", group: "local-standard", provider: "Qwen", region: "中国/国际", sizeGb: 3.2, minMemory: 5, minCores: 4, rank: 20, version: "2026.06", desc: "适合平板和电脑，能处理更长上下文。" },
  { id: "mistral-7b-q4", name: "Mistral 本地模型", short: "Mistral", tag: "代码", type: "local", group: "reasoning-code", provider: "Mistral AI", region: "国际", sizeGb: 4.3, minMemory: 6, minCores: 6, rank: 25, version: "2026.06", desc: "适合英文代码、工具调用思路、文档生成和项目草案。" },
  { id: "deepseek-distill-q4", name: "DeepSeek 蒸馏推理模型", short: "DS", tag: "推理", type: "local", group: "reasoning-code", provider: "DeepSeek", region: "中国/国际", sizeGb: 4.1, minMemory: 6, minCores: 6, rank: 28, version: "2026.06", desc: "适合复杂规划、代码推理和多步骤任务。" },
  { id: "llama-3.1-8b-q4", name: "Llama 高性能本地模型", short: "8B", tag: "高性能", type: "local", group: "local-power", provider: "Meta", region: "国际", sizeGb: 5.6, minMemory: 8, minCores: 8, rank: 30, version: "2026.06", desc: "适合桌面端离线 Agent、复杂英文任务、代码和长上下文。" },
];
const MODEL_GROUPS = [
  { id: "shai", title: "山海推荐", desc: "默认入口，适合大多数用户直接开始用。" },
  { id: "cloud-global", title: "全球云端旗舰", desc: "OpenAI、Claude、Gemini 等国际云端模型，用户可自行接入 API。" },
  { id: "reasoning-code", title: "推理与代码", desc: "适合代码工程、长链路推理、复杂规划和 Agent 工作流。" },
  { id: "cn-office", title: "中文办公", desc: "适合中文写作、办公、资料整理、摘要和日常问答。" },
  { id: "local-light", title: "本地轻量", desc: "低配置设备优先选择，下载小、响应快、适合离线基础使用。" },
  { id: "local-standard", title: "本地标准", desc: "平板和电脑常用选择，质量、速度和占用更均衡。" },
  { id: "local-power", title: "本地高性能", desc: "适合电脑端和高性能设备，能承担更复杂的离线任务。" },
  { id: "other", title: "其他模型", desc: "官网目录同步来的其他模型。" },
];
let models = loadStoredModelCatalog();
const modelDownloadTimers = new Map();

const reasoningModes = [
  { id: "fast", label: "快速", short: "快", timeout: 14000, desc: "聊天、轻任务，响应更快。" },
  { id: "standard", label: "标准", short: "标准", timeout: 22000, desc: "日常任务，速度和质量平衡。" },
  { id: "deep", label: "深度", short: "深度", timeout: 36000, desc: "代码、办公、规划等复杂任务。" },
  { id: "ultra", label: "超高", short: "超高", timeout: 52000, 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 = [
  {
    id: "shai-chat",
    name: "自然对话",
    category: "基础",
    desc: "像正常 AI 助手一样聊天、解释、陪用户理清想法，不把普通对话强行变成任务。",
    triggers: [/聊天|你好|在吗|解释|介绍|为什么|怎么|是什么|什么意思/i],
    workflow: ["先回答问题本身", "需要时追问上下文", "给出更好的下一步"],
    outputs: ["直接回答", "补充例子", "继续追问"],
    system: "普通聊天要自然直接；简单问题直接答，复杂问题再拆步骤。",
  },
  {
    id: "shai-project-builder",
    name: "项目生成",
    category: "Agent",
    desc: "从一句目标生成项目计划、文件结构、虚拟文件和可继续迭代的交付物。",
    triggers: [/项目|做一个|做一款|开发|搭建|系统|应用|app|官网|后台|完整/i],
    workflow: ["确认目标与平台", "拆成模块和文件", "生成首版交付", "给出验收与迭代点"],
    outputs: ["项目计划", "文件树", "核心代码/文档", "下一步清单"],
    system: "遇到完整项目先问缺失信息，再输出计划、文件结构、首版实现和验收标准。",
  },
  {
    id: "shai-code-repair",
    name: "代码修复",
    category: "编程",
    desc: "分析报错、定位原因、给最小修改方案，并补充测试和回退步骤。",
    triggers: [/代码|bug|报错|修复|接口|数据库|脚本|python|swift|react|vue|java|node|npm|编译/i],
    workflow: ["复现问题", "定位根因", "给最小改动", "补测试和回退"],
    outputs: ["原因分析", "补丁方案", "测试命令", "风险提示"],
    system: "代码任务按工程师方式处理：先根因，再补丁，再测试，不要只给泛泛建议。",
  },
  {
    id: "shai-web-studio",
    name: "网页交付",
    category: "前端",
    desc: "生成 HTML/CSS/JS 页面、交互原型和预览说明，适合官网、落地页和工具页面。",
    triggers: [/网页|网站|html|css|前端|页面|落地页|交互|预览/i],
    workflow: ["确定页面目标", "设计信息结构", "生成三件套", "验证响应式和交互"],
    outputs: ["HTML", "CSS", "JS", "预览说明"],
    system: "网页任务要直接生成可运行页面，注意移动端、视觉层级和交互完整性。",
  },
  {
    id: "shai-office-writer",
    name: "办公文档",
    category: "办公",
    desc: "生成报告、方案、PPT 大纲、邮件、合同草稿、宣传文案和结构化总结。",
    triggers: [/文档|报告|总结|方案|邮件|ppt|表格|会议|合同|简历|宣传|文案|办公/i],
    workflow: ["确认读者和目的", "搭建结构", "生成正文", "润色成可提交版本"],
    outputs: ["摘要", "正文", "大纲", "待确认事项"],
    system: "办公任务要先明确读者、目的和语气，输出可以直接复制使用的正式内容。",
  },
  {
    id: "shai-research-analyst",
    name: "资料分析",
    category: "分析",
    desc: "把长资料、截图文字、网页内容、论文或业务材料整理成摘要、问题、洞察和行动建议。",
    triggers: [/分析|资料|文件|论文|材料|截图|图片|阅读|整理|摘要|提炼|归纳|洞察/i],
    workflow: ["提取关键信息", "分层摘要", "找问题和机会", "给行动建议"],
    outputs: ["摘要", "关键点", "问题清单", "建议"],
    system: "资料分析要分清事实、判断和建议；信息不足时说明缺口并提出补充材料。",
  },
  {
    id: "shai-automation-architect",
    name: "自动化编排",
    category: "自动化",
    desc: "把重复工作设计成流程、脚本、快捷指令、审批节点和回退方案。",
    triggers: [/自动化|批量|流程|工作流|快捷指令|定时|脚本|爬取|同步|通知|机器人/i],
    workflow: ["定义输入输出", "拆节点", "标注权限风险", "给脚本或操作方案"],
    outputs: ["流程图", "脚本草案", "审批点", "回退方案"],
    system: "自动化任务要先确认输入、输出、平台和权限，涉及高风险动作必须给审批和回退。",
  },
  {
    id: "shai-security-defender",
    name: "设备安全防御",
    category: "安全",
    desc: "检查账号、设备、支付、隐私和网络风险，给防诈骗、防盗号、防泄露的安全建议。",
    triggers: [/安全|防御|病毒|盗号|诈骗|隐私|支付|风险|防护|勒索|木马|账号|漏洞/i],
    workflow: ["识别风险场景", "判断影响范围", "给防护步骤", "提供应急处理"],
    outputs: ["风险等级", "检查清单", "修复步骤", "预防建议"],
    system: "安全任务以防御和合规为主；不提供攻击、盗号、恶意软件或绕过权限的具体做法。",
  },
  {
    id: "shai-model-router",
    name: "模型调度",
    category: "模型",
    desc: "根据设备、任务和预算推荐本地/云端/混合模型，并解释推理速度选择。",
    triggers: [/模型|大模型|api|token|算力|本地|云端|推理速度|下载模型|OpenAI|Claude|Gemini|Qwen|Llama/i],
    workflow: ["识别任务类型", "评估设备和预算", "推荐模型组合", "说明限制和切换方式"],
    outputs: ["模型推荐", "算力模式", "成本说明", "配置步骤"],
    system: "模型选择要讲清本地、云端、混合的取舍，全球模型由用户自行选择和接入。",
  },
];

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";
let latestUpdateManifest = null;
let updateCheckInFlight = false;

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 normalizeModelCatalog(items = BUILT_IN_MODELS) {
  const byId = new Map(BUILT_IN_MODELS.map((item) => [item.id, item]));
  items.forEach((item) => {
    if (!item?.id || !item?.name) return;
    byId.set(item.id, { ...(byId.get(item.id) || {}), ...item });
  });
  return Array.from(byId.values());
}

function loadStoredModelCatalog() {
  try {
    const raw = localStorage.getItem(KEY.modelCatalog);
    const catalog = raw ? JSON.parse(raw) : null;
    if (Array.isArray(catalog?.models) && catalog.models.length) {
      return normalizeModelCatalog(catalog.models);
    }
  } catch {
    // Falls back to built-in catalog.
  }
  return normalizeModelCatalog(BUILT_IN_MODELS);
}

function saveModelCatalog(modelsToSave, meta = {}) {
  models = normalizeModelCatalog(modelsToSave);
  localStorage.setItem(KEY.modelCatalog, JSON.stringify({
    models,
    updatedAt: Date.now(),
    source: meta.source || "local",
    version: meta.version || APP_VERSION,
  }));
}

function loadLocalModelStatus() {
  return readJson(KEY.localModels, {});
}

function saveLocalModelStatus(status) {
  localStorage.setItem(KEY.localModels, JSON.stringify(status));
}

function getLocalModelStatus(modelId) {
  return loadLocalModelStatus()[modelId] || { state: "none", progress: 0 };
}

function setLocalModelStatus(modelId, patch) {
  const all = loadLocalModelStatus();
  all[modelId] = { ...(all[modelId] || { state: "none", progress: 0 }), ...patch, updatedAt: Date.now() };
  saveLocalModelStatus(all);
  return all[modelId];
}

function removeLocalModelStatus(modelId) {
  const all = loadLocalModelStatus();
  delete all[modelId];
  saveLocalModelStatus(all);
}

function deviceProfile() {
  const ua = navigator.userAgent || "";
  const isMobile = /iPhone|iPad|iPod|Android/i.test(ua);
  const isDesktop = /Macintosh|Windows|Linux/i.test(ua) && !isMobile;
  const memory = Number(navigator.deviceMemory) || (isMobile ? 4 : 8);
  const cores = Number(navigator.hardwareConcurrency) || (isMobile ? 4 : 8);
  const tier = memory >= 8 && cores >= 8 ? "高性能" : memory >= 5 && cores >= 4 ? "标准" : "轻量";
  const platform = isMobile ? "手机/平板" : isDesktop ? "电脑" : "当前设备";
  return { memory, cores, tier, platform, label: `${platform} · ${tier}配置 · 约 ${memory}GB 内存 / ${cores} 核` };
}

function modelCompatibility(model, profile = deviceProfile()) {
  if (model.type !== "local") return { ok: true, label: "云端模型", detail: "不占用本机算力" };
  const needsMemory = Number(model.minMemory || 0);
  const needsCores = Number(model.minCores || 0);
  const ok = profile.memory >= needsMemory && profile.cores >= needsCores;
  return {
    ok,
    label: ok ? "适配当前设备" : "配置可能不足",
    detail: ok
      ? `建议至少 ${needsMemory}GB 内存 / ${needsCores} 核，当前可运行。`
      : `建议至少 ${needsMemory}GB 内存 / ${needsCores} 核，当前设备可能卡顿或无法加载。`,
  };
}

function recommendedLocalModel(profile = deviceProfile()) {
  const localModels = models.filter((item) => item.type === "local");
  const compatible = localModels
    .filter((item) => modelCompatibility(item, profile).ok)
    .sort((a, b) => Number(b.rank || 0) - Number(a.rank || 0));
  return compatible[0] || localModels.sort((a, b) => Number(a.minMemory || 0) - Number(b.minMemory || 0))[0] || null;
}

function modelGroupFor(model) {
  const id = model.group || (model.type === "cloud" ? "cloud-global" : "local-standard");
  return MODEL_GROUPS.find((group) => group.id === id) || MODEL_GROUPS.find((group) => group.id === "other");
}

function groupedModelCatalog() {
  const byGroup = new Map(MODEL_GROUPS.map((group) => [group.id, { group, items: [] }]));
  models.forEach((model) => {
    const group = modelGroupFor(model);
    byGroup.get(group.id).items.push(model);
  });
  return Array.from(byGroup.values()).filter((entry) => entry.items.length);
}

function installedLocalModels() {
  const status = loadLocalModelStatus();
  return models.filter((item) => item.type === "local" && status[item.id]?.state === "installed");
}

function configuredCloudModels() {
  if (!localStorage.getItem(KEY.endpoint)) return [];
  return models.filter((item) => item.type === "cloud");
}

function quickSelectableModels() {
  return [...installedLocalModels(), ...configuredCloudModels()];
}

function downloadStage(progress) {
  if (progress >= 100) return "安装完成";
  if (progress >= 82) return "安装中";
  if (progress >= 68) return "校验模型文件";
  if (progress >= 45) return "解压能力包";
  return "下载中";
}

function installedModelNeedsUpdate(model) {
  if (model.type !== "local") return false;
  const record = getLocalModelStatus(model.id);
  return record.state === "installed" && model.version && record.version && model.version !== record.version;
}

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();
  resumeModelDownloads();
  startWakeIfNeeded();
  registerServiceWorker();
  setTimeout(() => checkForAppUpdate({ reason: "startup" }), 1200);
  setTimeout(() => syncModelCatalog({ silent: true }), 1800);
}

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();
    showTab("workspace");
    $("#composerInput").focus();
  });
  $$(".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;
    closeModelQuickMenu();
  });
  $("#modelQuickBtn").addEventListener("click", (event) => {
    event.stopPropagation();
    toggleModelQuickMenu();
  });
  $("#modelQuickMenu").addEventListener("click", (event) => {
    event.stopPropagation();
    const openModelsButton = event.target.closest("[data-open-models]");
    const deleteButton = event.target.closest("[data-quick-delete-model]");
    const modelButton = event.target.closest("[data-quick-model]");
    const reasoningButton = event.target.closest("[data-quick-reasoning]");
    if (openModelsButton) {
      closeModelQuickMenu();
      showTab("models");
      return;
    }
    if (deleteButton) {
      deleteLocalModel(deleteButton.dataset.quickDeleteModel);
      renderModelQuick();
      return;
    }
    if (modelButton) {
      selectModel(modelButton.dataset.quickModel);
      closeModelQuickMenu();
      return;
    }
    if (reasoningButton) {
      localStorage.setItem(KEY.reasoning, reasoningButton.dataset.quickReasoning);
      renderModelQuick();
    }
  });
  document.addEventListener("click", (event) => {
    if (!event.target.closest(".model-picker")) closeModelQuickMenu();
  });
  $("#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.dataset.prompt || button.textContent.trim());
  });
  $(".starter-guide").addEventListener("click", (event) => {
    const button = event.target.closest("button");
    if (button) submit(button.dataset.prompt || 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", () => {
    const recommended = recommendedLocalModel();
    localStorage.setItem(KEY.compute, "local");
    localStorage.setItem(KEY.modelGate, "true");
    if (recommended) startModelDownload(recommended.id, { closeGate: true });
    else {
      $("#modelGate").hidden = true;
      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();
  });
  $("#syncModelCatalogBtn").addEventListener("click", () => syncModelCatalog({ silent: false }));
  $("#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", () => {
    const recommended = recommendedLocalModel();
    localStorage.setItem(KEY.compute, "local");
    if (recommended) startModelDownload(recommended.id);
    else {
      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);
  $("#updateNowBtn").addEventListener("click", applyAvailableUpdate);
  $("#laterUpdateBtn").addEventListener("click", dismissAvailableUpdate);
  $("#unlockReviewBtn").addEventListener("click", unlockReview);
  $("#lockReviewBtn").addEventListener("click", lockReview);
  $("#manualIssueBtn").addEventListener("click", issueManualTokens);
  $("#exportOrdersBtn").addEventListener("click", exportOrders);
  $("#refreshReviewBtn").addEventListener("click", renderReview);
  $("#clearResolvedSupportBtn").addEventListener("click", clearResolvedSupportTickets);
  $("#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);
  $("#settingsSearchInput").addEventListener("input", renderSettingsSearch);
  $("#workModeSelect").addEventListener("change", (event) => {
    localStorage.setItem(KEY.workMode, event.target.value);
    toast("工作模式已更新");
  });
  $("#defaultTargetSelect").addEventListener("change", (event) => {
    localStorage.setItem(KEY.defaultTarget, event.target.value);
    toast("默认打开目标已更新");
  });
  $("#appLanguageSelect").addEventListener("change", (event) => {
    localStorage.setItem(KEY.appLanguage, event.target.value);
    toast("语言设置已保存");
  });
  $("#sidebarDisplaySwitch").addEventListener("change", (event) => {
    localStorage.setItem(KEY.sidebarDisplay, event.target.checked ? "true" : "false");
    toast("菜单栏显示设置已保存");
  });
  $("#settingsComputeSelect").addEventListener("change", (event) => {
    localStorage.setItem(KEY.compute, event.target.value);
    renderModels();
    renderModelQuick();
    toast("默认算力已更新");
  });
  $("#settingsReasoningSelect").addEventListener("change", (event) => {
    localStorage.setItem(KEY.reasoning, event.target.value);
    renderModelQuick();
    toast("推理速度已更新");
  });
  $("#settingsOpenModelsBtn").addEventListener("click", () => showTab("models"));
  $("#settingsSyncModelsBtn").addEventListener("click", () => syncModelCatalog({ silent: false }));
  $("#hooksSwitch").addEventListener("change", (event) => {
    localStorage.setItem(KEY.hooks, event.target.checked ? "true" : "false");
    toast("应用快照设置已保存");
  });
  $("#connectorsSwitch").addEventListener("change", (event) => {
    localStorage.setItem(KEY.connectors, event.target.checked ? "true" : "false");
    toast("MCP 服务器设置已保存");
  });
  $("#browserBridgeSwitch").addEventListener("change", (event) => {
    localStorage.setItem(KEY.browserBridge, event.target.checked ? "true" : "false");
    toast("浏览器桥接设置已保存");
  });
  $("#computerBridgeSwitch").addEventListener("change", (event) => {
    localStorage.setItem(KEY.computerBridge, event.target.checked ? "true" : "false");
    toast("电脑操控桥接设置已保存");
  });
  $("#gitModeSelect").addEventListener("change", (event) => {
    localStorage.setItem(KEY.gitMode, event.target.value);
    toast("Git 模式已更新");
  });
  $("#environmentProfileSelect").addEventListener("change", (event) => {
    localStorage.setItem(KEY.environmentProfile, event.target.value);
    toast("环境配置已更新");
  });
  $("#archivePolicySelect").addEventListener("change", (event) => {
    localStorage.setItem(KEY.archivePolicy, event.target.value);
    toast("归档策略已更新");
  });
  $("#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 模板");
  });
  $("#submitSupportBtn").addEventListener("click", submitSupportTicket);
  $("#openSupportChatBtn").addEventListener("click", openSupportChat);
  $("#exportSupportBtn").addEventListener("click", exportSupportTickets);
  $("#supportAdminList").addEventListener("click", (event) => {
    const resolve = event.target.closest("[data-resolve-support]");
    const reopen = event.target.closest("[data-reopen-support]");
    const remove = event.target.closest("[data-delete-support]");
    if (resolve) resolveSupportTicket(resolve.dataset.resolveSupport);
    if (reopen) reopenSupportTicket(reopen.dataset.reopenSupport);
    if (remove) deleteSupportTicket(remove.dataset.deleteSupport);
  });
  $("#clearChatBtn").addEventListener("click", clearChat);
  $("#clearMemoryBtn").addEventListener("click", clearMemory);
  $("#runUpgradeBtn").addEventListener("click", runSelfUpgrade);
  $("#generateUpgradePlanBtn").addEventListener("click", generateUpgradeRoadmap);
  $("#resetAppBtn").addEventListener("click", resetApp);
  bindSwipeGestures();
  window.addEventListener("online", handleNetworkChange);
  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;
  closeModelQuickMenu();
  $$(".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 (!$("#modelQuickMenu").hidden) {
    closeModelQuickMenu();
    return true;
  }
  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, .model-picker")) 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();
  renderModelQuick();
  renderAccount();
  renderMemory();
  renderUpgrade();
  renderSettings();
  renderSupport();
  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);
  const activeSkill = selectAgentSkill(text);
  updatePlan(["理解用户目标", `启用 Skill：${activeSkill.name}`, ...skillPlan(activeSkill), "给出下一步"]);
  logTool("analyze_request", `识别任务：${detectIntent(text).label}`);
  logTool("skill_select", `${activeSkill.name}：${activeSkill.desc}`);
  if (permission) {
    logTool("permission_mode", `${permission.info.title}：涉及 ${permission.scopes.join("、")}，${permission.action}`);
    logTerminal(`$ shai permission --mode ${permission.mode} --scope ${permission.scopes.join(",")}`);
  }
  const runtime = modelRuntimeState();
  logTerminal(`$ shai analyze "${text.replace(/"/g, "'").slice(0, 60)}"`);
  logTerminal(`$ shai model --use ${runtime.model.id} --compute ${runtime.compute} --reasoning ${runtime.reasoning.id}`);
  if (runtime.cloudConfigured) logTool("runtime", "已配置云端 API，优先调用真实模型");
  else if (runtime.selectedLocalInstalled) logTool("runtime", `已启用本地模型包：${runtime.model.name}`);
  else logTool("runtime", "未接入真实模型，使用内置 Agent 工作流生成可编辑结果");

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

  const result = buildLocalAgentResult(text, activeSkill);
  result.files = {
    ...(result.files || {}),
    [`/skills/${activeSkill.id}.md`]: skillMarkdown(activeSkill),
  };
  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 skillMatches(skill, text, intent = detectIntent(text)) {
  if (!skill) return false;
  if (skill.intent && skill.intent === intent.key) return true;
  return (skill.triggers || []).some((pattern) => pattern.test(text));
}

function selectAgentSkill(text) {
  const intent = detectIntent(text);
  const priority = [
    "shai-security-defender",
    "shai-model-router",
    "shai-project-builder",
    "shai-code-repair",
    "shai-web-studio",
    "shai-office-writer",
    "shai-research-analyst",
    "shai-automation-architect",
    "shai-chat",
  ];
  const matched = skills.filter((skill) => skillMatches(skill, text, intent));
  return matched.sort((a, b) => priority.indexOf(a.id) - priority.indexOf(b.id))[0]
    || skills.find((skill) => skill.id === "shai-chat")
    || skills[0];
}

function skillPlan(skill) {
  return skill?.workflow?.length ? skill.workflow : ["理解目标", "拆解步骤", "生成结果", "等待迭代"];
}

function skillLibraryPrompt() {
  return skills.map((skill) => (
    `- ${skill.name}（${skill.category}）：${skill.system} 工作流：${skill.workflow.join(" -> ")}。输出：${skill.outputs.join("、")}。`
  )).join("\n");
}

function skillMarkdown(skill) {
  return `# ${skill.name}

类别：${skill.category}

说明：${skill.desc}

## 工作流
${skill.workflow.map((item, index) => `${index + 1}. ${item}`).join("\n")}

## 输出
${skill.outputs.map((item) => `- ${item}`).join("\n")}

## 执行原则
${skill.system}
`;
}

function detectIntent(text) {
  if (/安全|防御|病毒|盗号|诈骗|隐私|支付|风险|防护|勒索|木马|账号|漏洞/i.test(text)) return { key: "security", label: "设备安全防御" };
  if (/模型|大模型|api|token|算力|本地|云端|推理速度|下载模型|OpenAI|Claude|Gemini|Qwen|Llama/i.test(text)) return { key: "model", label: "模型调度" };
  if (/自动化|批量|流程|工作流|快捷指令|定时|脚本|爬取|同步|通知|机器人/i.test(text)) return { key: "automation", label: "自动化编排" };
  if (/分析|资料|文件|论文|材料|截图|图片|阅读|整理|摘要|提炼|归纳|洞察/i.test(text)) return { key: "research", label: "资料分析" };
  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, activeSkill = selectAgentSkill(text)) {
  const intent = detectIntent(text);
  if (isNewUserHelpRequest(text)) return newbieResult();
  if (isCasualChat(text)) return casualChatResult(text);
  let result = null;
  if (isWritingRequest(text)) result = writingResult(text);
  const knowledge = findKnowledgeCard(text);
  if (!result && knowledge) result = knowledgeResult(text, knowledge);
  if (!result && activeSkill?.id === "shai-security-defender") result = securityResult(text);
  if (!result && activeSkill?.id === "shai-model-router") result = modelRouterResult(text);
  if (!result && activeSkill?.id === "shai-research-analyst") result = researchResult(text);
  if (!result && activeSkill?.id === "shai-automation-architect") result = automationResult(text);
  if (!result && intent.key === "web") result = webResult(text);
  if (!result && intent.key === "code") result = codeResult(text);
  if (!result && intent.key === "product") result = productResult(text);
  if (!result && intent.key === "office") result = officeResult(text);
  if (!result && intent.key === "device") result = deviceResult(text);
  if (!result && intent.key === "evolve") result = evolveResult();
  if (!result && isQuestion(text)) result = openQuestionResult(text);
  if (!result) result = generalResult(text);
  return withModelRecommendation(result, text, intent);
}

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

function modelRuntimeState() {
  const model = selectedModel();
  const compute = localStorage.getItem(KEY.compute) || "hybrid";
  const reasoning = selectedReasoning();
  const cloudConfigured = Boolean(localStorage.getItem(KEY.endpoint));
  const installedModels = installedLocalModels();
  const selectedLocalInstalled = model.type === "local" && installedModels.some((item) => item.id === model.id);
  const installedLocalModel = selectedLocalInstalled ? model : installedModels[0] || null;
  return {
    model,
    compute,
    reasoning,
    cloudConfigured,
    selectedLocalInstalled,
    installedLocalModel,
    hasAnyLocalInstalled: Boolean(installedLocalModel),
  };
}

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 isCasualChat(text) {
  const clean = text.trim();
  if (!clean) return false;
  return /(你好|您好|哈喽|hello|hi|嗨|在吗|能.*聊天|正常聊天|陪我聊|聊聊天|随便聊|你好吗|谢谢|辛苦了|早上好|中午好|晚上好|晚安|再见)/i.test(clean);
}

function isNewUserHelpRequest(text) {
  return /(新手|第一次|怎么用|如何使用|不会用|上手|使用方法|能做什么|你会什么|怎么开始|入门|指引|教程)/i.test(text);
}

function newbieResult() {
  const guide = `# 新手快速开始

山海经AI的核心用法是一句话说目标。它会把目标拆成计划、文件、命令、预览和下一步。

## 你可以这样问
1. 帮我做一个个人官网，先问我必要信息。
2. 这段代码报错了，帮我定位原因并给修改方案。
3. 帮我写一份产品介绍文案，语气要简洁专业。
4. 帮我把这个复杂目标拆成三天计划。
5. 帮我选择本地大模型还是云端 API。

## 使用建议
- 普通聊天直接问。
- 大型任务先说目标和平台。
- 需要更强推理时，去模型页安装本地模型或接入 API。`;
  return {
    summary: "已生成新手上手指南",
    terminal: ["$ shai guide --beginner", "✓ quick-start guide ready"],
    files: {
      "/guide/quick-start.md": guide,
    },
    html: `
      <h4>可以，先按最简单的方式开始。</h4>
      ${thinkingBoard(["说目标", "补资料", "看计划", "继续修改"])}
      <p>你不用学习复杂按钮。把山海经AI当成一个会做事的助手，直接说“我想完成什么”。如果信息不够，我会先问你必要问题。</p>
      <h4>最常用的 5 种问法</h4>
      <ol>
        <li><strong>写代码：</strong>帮我做一个登录页面，先问我平台和风格。</li>
        <li><strong>改内容：</strong>帮我把这段文字改得更专业，我接下来粘贴原文。</li>
        <li><strong>做计划：</strong>帮我把这个目标拆成今天能执行的步骤。</li>
        <li><strong>分析资料：</strong>我发你一段资料，请总结重点、问题和建议。</li>
        <li><strong>模型选择：</strong>帮我看当前设备适合本地模型还是云端 API。</li>
      </ol>
      <p>大型任务建议打开“模型”页：安装本地大模型可以离线免费用；接入云端 API 可以得到更强推理和更长上下文。</p>
    `,
  };
}

function casualChatResult(text) {
  const clean = text.trim();
  let title = "我在，可以正常聊。";
  let body = "你可以直接说现在想到的事，我会先回应你本身的问题；如果它后来变成任务，我再帮你拆步骤。";
  let next = "想轻松聊就继续说，想做事就直接给我目标。";

  if (/谢谢|辛苦了|感谢/i.test(clean)) {
    title = "不客气。";
    body = "你继续把要处理的内容发来就行，我会按当前上下文接着帮你整理、判断或生成。";
    next = "如果刚才的结果哪里不合适，也可以直接说要怎么改。";
  } else if (/早上好|中午好|晚上好|晚安/i.test(clean)) {
    title = clean.includes("晚安") ? "晚安，先好好休息。" : "你好，我在。";
    body = clean.includes("晚安")
      ? "今天的事情可以先放一放。下次打开时，你可以继续从当前对话接着做。"
      : "你可以随便聊，也可以直接把今天想完成的事情告诉我，我会先帮你理清楚。";
    next = clean.includes("晚安") ? "需要记录明天要做的事，也可以现在发我一句。" : "如果不知道从哪里开始，就说“帮我规划今天”。";
  } else if (/在吗|hello|hi|嗨|你好|您好|哈喽/i.test(clean)) {
    title = "在，我可以帮你。";
    body = "你不用按固定格式提问。可以是一句话、一个截图里的问题、一段文字，或者一个还没想清楚的目标。";
    next = "先发最粗略的想法也可以，我会帮你追问关键细节。";
  } else if (/陪我聊|聊聊天|随便聊|正常聊天|能.*聊天/i.test(clean)) {
    title = "可以，我们就按聊天来。";
    body = "我不会把每句话都硬拆成任务。你说感受、想法、工作、学习或产品点子都可以，我会自然接住。";
    next = "你可以先说最近最占脑子的那件事。";
  }

  return {
    summary: "已进行普通聊天回复",
    terminal: ["$ chat casual", "✓ natural reply ready"],
    files: null,
    html: `
      <h4>${escapeHtml(title)}</h4>
      <p>${escapeHtml(body)}</p>
      <p>${escapeHtml(next)}</p>
    `,
  };
}

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>
      <p>这个问题我可以继续往下聊。你可以让我用更简单的话解释，也可以让我从原理、例子、步骤、优缺点或实际操作角度展开。</p>
      <p>如果你想要更准确的答案，补一句具体场景就行，比如“我是想用于写代码、办公、学习，还是做产品方案”。</p>
    `,
  };
}

function researchResult(text) {
  const doc = `# 资料分析报告

## 分析目标
${text}

## 推荐处理方式
1. 先提取事实：人物、时间、地点、数字、结论和来源。
2. 再归纳结构：背景、问题、原因、影响、机会。
3. 最后输出行动：下一步要确认什么、该做什么、有什么风险。

## 等待补充
请继续粘贴资料正文、截图文字、网页内容或文件摘要，我会按“摘要、关键点、问题、建议”输出。`;
  return {
    summary: "已启用资料分析 Skill",
    terminal: ["$ skill use shai-research-analyst", "$ analyze material --structure", "✓ analysis scaffold ready"],
    files: { "/analysis/research-report.md": doc },
    html: `
      <h4>我会按资料分析 Skill 来处理。</h4>
      ${thinkingBoard(["提取事实", "归纳结构", "找出问题", "给行动建议"])}
      <p>你可以把资料直接粘贴过来，或者描述文件内容。我会先区分事实、判断和建议，再输出清晰的摘要与下一步。</p>
      <ol>
        <li><strong>摘要：</strong>用几句话说清资料核心。</li>
        <li><strong>关键点：</strong>列出最重要的信息和证据。</li>
        <li><strong>问题：</strong>指出矛盾、缺口、风险或不确定处。</li>
        <li><strong>建议：</strong>给可以马上执行的下一步。</li>
      </ol>
    `,
  };
}

function automationResult(text) {
  const plan = `# 自动化编排方案

## 目标
${text}

## 输入与输出
- 输入：用户提供的数据、文件、网页、表单或触发条件。
- 输出：生成结果、通知、文件、记录或后续动作。

## 流程节点
1. 接收输入
2. 校验数据
3. 执行核心处理
4. 记录结果
5. 通知用户或等待审批

## 权限与回退
- 涉及联网、文件、账号、支付、删除、发送等动作时，需要按当前审批模式确认。
- 每一步应保存日志，失败时回退到上一个稳定状态。`;
  return {
    summary: "已启用自动化编排 Skill",
    terminal: ["$ skill use shai-automation-architect", "$ plan workflow --approval-aware", "✓ automation plan ready"],
    files: { "/automation/workflow-plan.md": plan },
    html: `
      <h4>我会把它设计成可执行的自动化流程。</h4>
      ${diagram(["输入", "校验", "处理", "审批", "输出"])}
      <p>自动化最重要的是先把输入、输出、平台和风险说清楚。你补充平台后，我可以继续给脚本、快捷指令、定时任务或接口方案。</p>
      <h4>我还需要确认</h4>
      <ol>
        <li>运行平台：手机、Windows、Mac、网页后台还是服务器？</li>
        <li>输入来源：文件、表格、网页、接口还是手动输入？</li>
        <li>输出结果：生成文件、发送消息、改数据还是提醒用户？</li>
        <li>哪些动作必须先让用户确认？</li>
      </ol>
    `,
  };
}

function securityResult(text) {
  const checklist = `# 设备安全防御检查

## 场景
${text}

## 优先检查
1. 账号是否异常登录。
2. 设备是否安装未知 App、插件、描述文件或脚本。
3. 支付、验证码、通讯录、相册、定位等权限是否被滥用。
4. 是否存在钓鱼链接、假客服、远程控制或诱导转账。

## 应急处理
- 断开可疑网络或远程控制。
- 修改重要账号密码并开启双重验证。
- 检查支付记录和登录设备。
- 保留证据，必要时联系平台客服或报警。`;
  return {
    summary: "已启用设备安全防御 Skill",
    terminal: ["$ skill use shai-security-defender", "$ security checklist --defensive", "✓ defensive checklist ready"],
    files: { "/security/defense-checklist.md": checklist },
    html: `
      <h4>我会按防御和合规方向处理。</h4>
      ${thinkingBoard(["识别风险", "保护账号", "检查设备", "保留证据"])}
      <p>我可以帮你判断风险、排查异常、做防护清单和应急处理方案。涉及攻击、盗号、绕过权限或恶意软件的具体做法，我不会提供。</p>
      <ol>
        <li>先确认有没有异常登录、扣款、远程控制或陌生授权。</li>
        <li>再检查系统权限、描述文件、未知软件和浏览器插件。</li>
        <li>最后给你一份按优先级执行的安全处理步骤。</li>
      </ol>
    `,
  };
}

function modelRouterResult(text) {
  const profile = deviceProfile();
  const recommended = recommendedLocalModel(profile);
  const guide = `# 模型调度建议

## 设备
${profile.label}

## 推荐
${recommended ? recommended.name : "优先使用云端模型或轻量本地模型"}

## 选择原则
1. 普通聊天和轻办公：本地轻量模型或山海默认模型。
2. 长代码、项目开发、复杂推理：云端旗舰或高性能本地模型。
3. 隐私优先、离线使用：本地模型。
4. 质量优先、上下文很长：用户自接 API 或云端 Token。`;
  return {
    summary: "已启用模型调度 Skill",
    terminal: ["$ skill use shai-model-router", `$ inspect device --profile '${profile.label}'`, "✓ model routing advice ready"],
    files: { "/models/model-routing.md": guide },
    html: `
      <h4>我会按任务和设备帮你选模型。</h4>
      ${thinkingBoard(["看任务", "看设备", "看预算", "选模型"])}
      <p>当前设备识别为：${escapeHtml(profile.label)}</p>
      <p>推荐本地模型：<strong>${escapeHtml(recommended ? recommended.name : "暂无合适本地模型，建议用云端")}</strong></p>
      <ol>
        <li><strong>普通聊天：</strong>轻量本地模型即可。</li>
        <li><strong>代码和大型项目：</strong>优先云端旗舰或高性能本地模型。</li>
        <li><strong>隐私离线：</strong>选择本地模型，速度取决于设备。</li>
        <li><strong>国际模型：</strong>OpenAI、Claude、Gemini 等由用户自行配置兼容 API。</li>
      </ol>
    `,
  };
}

function webResult(text) {
  const project = buildWebProject(text);
  return {
    summary: `已生成网页项目文件：${project.title}`,
    terminal: [
      "$ mkdir -p /web",
      "$ write /web/index.html /web/styles.css /web/app.js",
      "$ run virtual-preview --open",
      "✓ preview ready",
    ],
    files: {
      "/web/index.html": project.html,
      "/web/styles.css": project.css,
      "/web/app.js": project.js,
      "/web/README.md": `# ${project.title}\n\n需求：${text}\n\n## 已执行\n- 生成 HTML 结构\n- 生成 CSS 视觉样式\n- 生成 JS 交互逻辑\n- 建立右侧预览\n\n## 继续迭代\n你可以继续说“加登录页”“改成科技风”“导出下载包”“加按钮动效”。`,
    },
    html: `
      <h4>已开始执行：${escapeHtml(project.title)}</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 buildWebProject(text) {
  const title = cleanTitle(text, "山海经AI生成页面");
  if (/三角|triangle|吸附|磁吸|canvas|游戏/i.test(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>${escapeHtml(title)}</title>
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <main class="shell">
      <section class="panel">
        <p>Interactive Demo</p>
        <h1>${escapeHtml(title)}</h1>
        <span>移动鼠标或手指，三角形会被吸附过去。</span>
      </section>
      <canvas id="stage" aria-label="三角形吸附演示"></canvas>
    </main>
    <script src="./app.js"></script>
  </body>
</html>`;
    const css = `*{box-sizing:border-box}body{margin:0;min-height:100vh;font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;background:#071019;color:#f4fffb;overflow:hidden}.shell{min-height:100vh;display:grid;grid-template-columns:minmax(280px,420px) 1fr;gap:20px;padding:28px;background:radial-gradient(circle at 75% 22%,rgba(47,225,204,.22),transparent 28%),linear-gradient(135deg,#071019,#122033)}.panel{align-self:end;z-index:2;padding:24px;border:1px solid rgba(47,225,204,.35);border-radius:24px;background:rgba(8,24,32,.72);box-shadow:0 24px 70px rgba(0,0,0,.35)}p{margin:0 0 10px;color:#2fe1cc;font-weight:900;letter-spacing:.04em}h1{margin:0 0 14px;font-size:clamp(30px,5vw,64px);line-height:1.05}span{color:#b8cdc9;font-weight:700}canvas{position:fixed;inset:0;width:100vw;height:100vh}@media(max-width:720px){.shell{display:block;padding:18px}.panel{margin-top:54vh}}`;
    const js = `const canvas=document.getElementById("stage");const ctx=canvas.getContext("2d");const pointer={x:0,y:0,active:false};const tri={x:0,y:0,vx:0,vy:0,angle:0};function resize(){const dpr=Math.min(2,window.devicePixelRatio||1);canvas.width=Math.floor(innerWidth*dpr);canvas.height=Math.floor(innerHeight*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);pointer.x=pointer.x||innerWidth*.62;pointer.y=pointer.y||innerHeight*.5;tri.x=tri.x||innerWidth*.35;tri.y=tri.y||innerHeight*.52}function move(e){pointer.active=true;pointer.x=e.clientX;pointer.y=e.clientY}function drawTriangle(x,y,size,angle){ctx.save();ctx.translate(x,y);ctx.rotate(angle);ctx.beginPath();ctx.moveTo(size,0);ctx.lineTo(-size*.78,-size*.64);ctx.lineTo(-size*.5,size*.72);ctx.closePath();const g=ctx.createLinearGradient(-size,-size,size,size);g.addColorStop(0,"#35f4dc");g.addColorStop(1,"#3d7bff");ctx.fillStyle=g;ctx.shadowColor="rgba(47,225,204,.55)";ctx.shadowBlur=28;ctx.fill();ctx.lineWidth=4;ctx.strokeStyle="rgba(255,255,255,.82)";ctx.stroke();ctx.restore()}function loop(){ctx.clearRect(0,0,innerWidth,innerHeight);const targetX=pointer.active?pointer.x:innerWidth*.66;const targetY=pointer.active?pointer.y:innerHeight*.45;const dx=targetX-tri.x;const dy=targetY-tri.y;tri.vx=tri.vx*.86+dx*.035;tri.vy=tri.vy*.86+dy*.035;tri.x+=tri.vx;tri.y+=tri.vy;tri.angle=Math.atan2(dy,dx);ctx.strokeStyle="rgba(47,225,204,.28)";ctx.lineWidth=2;ctx.beginPath();ctx.arc(targetX,targetY,34+Math.sin(Date.now()/180)*8,0,Math.PI*2);ctx.stroke();ctx.beginPath();ctx.moveTo(tri.x,tri.y);ctx.lineTo(targetX,targetY);ctx.strokeStyle="rgba(255,255,255,.18)";ctx.stroke();drawTriangle(tri.x,tri.y,44,tri.angle);requestAnimationFrame(loop)}addEventListener("resize",resize);addEventListener("pointermove",move);addEventListener("pointerdown",move);resize();loop();`;
    return { title: "三角形吸附交互网页", html, css, js };
  }

  const html = `<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>${escapeHtml(title)}</title>
    <link rel="stylesheet" href="./styles.css" />
  </head>
  <body>
    <main class="hero">
      <p>Generated by ShanHaiJing AI</p>
      <h1>${escapeHtml(title)}</h1>
      <section>
        <article>清晰首页</article>
        <article>响应式布局</article>
        <article>可继续迭代</article>
      </section>
      <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(38px,7vw,78px);line-height:1.05}p{color:#29d6c5;font-weight:900}section{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;max-width:760px}article{padding:18px;border:1px solid rgba(41,214,197,.35);border-radius:16px;background:rgba(255,255,255,.06);font-weight:900}button{height:54px;border:0;border-radius:14px;padding:0 24px;background:#29d6c5;color:#031412;font-weight:900}@media(max-width:720px){section{grid-template-columns:1fr}h1{font-size:38px}}`;
  const js = `document.querySelector("button").addEventListener("click",()=>alert("山海经AI生成的网页已运行，可以继续让 AI 修改。"));`;
  return { title, html, css, js };
}

function cleanTitle(text, fallback) {
  const value = String(text || "").replace(/\s+/g, " ").trim();
  return value ? value.slice(0, 42) : fallback;
}

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 hasRealModelConfigured() {
  const runtime = modelRuntimeState();
  return runtime.cloudConfigured || runtime.selectedLocalInstalled || runtime.hasAnyLocalInstalled;
}

function isLargeTask(text, intent = detectIntent(text)) {
  if (["web", "code", "product", "office", "device"].includes(intent.key)) return true;
  return /(大型|完整|项目|开发|搭建|生成.*系统|做一个|做一款|写一套|全套|自动化|批量|复杂|官网|app|应用|程序|插件|后台|数据库|接口|部署|上线|商业化|PPT|长文档)/i.test(text) || text.length > 80;
}

function modelRecommendationHtml() {
  return `
    <div class="model-notice">
      <b>大型任务可升级真实推理</b>
      <span>当前会先用内置 Agent 工作流生成文件、预览、计划和命令日志。要让它像真正 Codex 一样连续推理、读写真实项目和执行系统命令，请在“模型”里安装本地模型包，或在账户中心接入云端 API。</span>
    </div>
  `;
}

function withModelRecommendation(result, text, intent) {
  if (!result || !isLargeTask(text, intent)) return result;
  const runtime = modelRuntimeState();
  if (runtime.cloudConfigured || runtime.selectedLocalInstalled || runtime.hasAnyLocalInstalled) return result;
  return {
    ...result,
    html: `${modelRecommendationHtml()}${result.html}`,
  };
}

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>
      <p>如果这是聊天，我可以继续顺着聊；如果你想让我做事，比如写代码、做文档、设计产品、生成网页或分析资料，也可以直接把目标告诉我。</p>
      <p>复杂任务我会先给基础方案，并建议你安装本地大模型或接入 API，这样结果会更接近真正的专业 AI Agent。</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((skill) => `
    <article class="skill-card">
      <div class="skill-card-head">
        <span>${escapeHtml(skill.category)}</span>
        <b>${escapeHtml(skill.name)}</b>
      </div>
      <p>${escapeHtml(skill.desc)}</p>
      <div class="skill-flow">
        ${(skill.workflow || []).map((item) => `<em>${escapeHtml(item)}</em>`).join("")}
      </div>
      <small>输出：${escapeHtml((skill.outputs || []).join(" / "))}</small>
    </article>
  `).join("");
}

function modelCardMeta(model, compatibility, record, recommended) {
  const parts = [
    model.type === "local" ? "本地免费" : "云端 Token",
    model.type === "local" ? `约 ${model.sizeGb || "-"}GB` : "无需下载",
    model.provider ? `厂商 ${model.provider}` : "",
    model.region ? `范围 ${model.region}` : "",
    model.access === "api" ? "用户自接 API" : "",
    model.access === "platform" ? "平台服务" : "",
    model.version ? `目录版本 ${model.version}` : "",
    compatibility.label,
    recommended ? "当前推荐" : "",
  ].filter(Boolean);
  if (record.state === "installed") parts.push(installedModelNeedsUpdate(model) ? "有新版可更新" : "已安装");
  if (record.state === "downloading") parts.push(`${downloadStage(record.progress)} ${record.progress || 0}%`);
  return parts.map((part) => `<span>${escapeHtml(part)}</span>`).join("");
}

function modelProgressHtml(model, record) {
  if (model.type !== "local") return "";
  const progress = Math.max(0, Math.min(100, Number(record.progress || 0)));
  const installed = record.state === "installed";
  const downloading = record.state === "downloading";
  const idle = !installed && !downloading;
  return `
    <div class="local-model-progress ${installed ? "installed" : ""}" aria-label="模型下载安装进度">
      <div class="progress-row">
        <b>${escapeHtml(installed ? "模型包已安装" : idle ? "未安装" : downloadStage(progress))}</b>
        <span>${escapeHtml(installed ? `已接入本机 Agent 工作流 · ${record.version || model.version || ""}` : idle ? "等待下载" : `${progress}%`)}</span>
      </div>
      <div class="progress-track"><i style="width:${installed ? 100 : progress}%"></i></div>
    </div>
  `;
}

function modelActionHtml(model, compatibility, record) {
  if (model.type !== "local") return `<button class="secondary" data-select-model="${escapeHtml(model.id)}" type="button">使用云端</button>`;
  if (!compatibility.ok) {
    return `<button class="secondary" disabled type="button">硬件不足</button>`;
  }
  if (record.state === "downloading") {
    return `
      <button class="secondary" disabled type="button">${escapeHtml(downloadStage(record.progress))}</button>
      <button class="secondary danger" data-delete-model="${escapeHtml(model.id)}" type="button">取消并删除</button>
    `;
  }
  if (installedModelNeedsUpdate(model)) {
    return `
      <button class="primary" data-download-model="${escapeHtml(model.id)}" type="button">更新模型</button>
      <button class="secondary danger" data-delete-model="${escapeHtml(model.id)}" type="button">删除</button>
    `;
  }
  if (record.state === "installed") {
    return `
      <button class="secondary" data-select-model="${escapeHtml(model.id)}" type="button">切换使用</button>
      <button class="secondary danger" data-delete-model="${escapeHtml(model.id)}" type="button">删除</button>
    `;
  }
  return `<button class="primary" data-download-model="${escapeHtml(model.id)}" type="button">下载并安装</button>`;
}

function renderModels() {
  const compute = localStorage.getItem(KEY.compute) || "hybrid";
  const model = selectedModel().id;
  const profile = deviceProfile();
  const recommended = recommendedLocalModel(profile);
  $$("#computeGroup [data-compute]").forEach((button) => {
    button.classList.toggle("active", button.dataset.compute === compute);
  });
  $("#modelDeviceLabel").textContent = profile.label;
  $("#modelRecommendLabel").textContent = recommended
    ? `已为当前硬件推荐：${recommended.name}。模型目录已放开全球范围，用户可按厂商、用途和设备自行选择。`
    : "暂时没有匹配到本地模型，建议使用云端 Token 模型。";
  $("#modelList").innerHTML = groupedModelCatalog().map(({ group, items }) => `
    <section class="model-group" data-model-group="${escapeHtml(group.id)}">
      <div class="model-group-head">
        <div>
          <h3>${escapeHtml(group.title)}</h3>
          <p>${escapeHtml(group.desc)}</p>
        </div>
        <span>${items.length} 个模型</span>
      </div>
      <div class="model-group-grid">
        ${items.map((item) => {
          const compatibility = modelCompatibility(item, profile);
          const record = getLocalModelStatus(item.id);
          const isRecommended = recommended?.id === item.id;
          return `
            <article class="model-card ${item.id === model ? "active" : ""} ${isRecommended ? "recommended" : ""} ${!compatibility.ok ? "unsupported" : ""}" data-model="${escapeHtml(item.id)}">
              <div class="model-card-head">
                <h3>${escapeHtml(item.name)} <span>${escapeHtml(isRecommended ? "推荐" : item.tag)}</span></h3>
                <small>${escapeHtml(modelShortName(item))}</small>
              </div>
              <p>${escapeHtml(item.desc)}</p>
              <div class="model-meta">${modelCardMeta(item, compatibility, record, isRecommended)}</div>
              <small class="model-compat">${escapeHtml(compatibility.detail)}</small>
              ${modelProgressHtml(item, record)}
              <div class="row model-actions">
                ${modelActionHtml(item, compatibility, record)}
              </div>
            </article>
          `;
        }).join("")}
      </div>
    </section>
  `).join("");
  $("#modelList").querySelectorAll("[data-model]").forEach((node) => {
    node.addEventListener("click", (event) => {
      if (event.target.closest("button")) return;
      selectModel(node.dataset.model);
    });
  });
  $("#modelList").querySelectorAll("[data-select-model]").forEach((button) => {
    button.addEventListener("click", () => selectModel(button.dataset.selectModel));
  });
  $("#modelList").querySelectorAll("[data-download-model]").forEach((button) => {
    button.addEventListener("click", () => startModelDownload(button.dataset.downloadModel));
  });
  $("#modelList").querySelectorAll("[data-delete-model]").forEach((button) => {
    button.addEventListener("click", () => deleteLocalModel(button.dataset.deleteModel));
  });
  $("#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 使用云端模型。";
  const runtime = modelRuntimeState();
  $("#apiStatus").textContent = runtime.cloudConfigured
    ? `已配置：${localStorage.getItem(KEY.endpoint)}`
    : runtime.hasAnyLocalInstalled
      ? `已安装本地模型包：${runtime.installedLocalModel.name}。当前使用本机 Agent 工作流。`
      : "未配置云端服务时使用本机 Agent 工作流；安装模型包后会自动切入本地模式。";
}

function selectModel(modelId) {
  const item = models.find((entry) => entry.id === modelId);
  if (!item) return;
  if (item.type === "local") {
    const record = getLocalModelStatus(item.id);
    if (record.state !== "installed") {
      startModelDownload(item.id);
      return;
    }
    localStorage.setItem(KEY.compute, "local");
  } else {
    localStorage.setItem(KEY.compute, "cloud");
  }
  localStorage.setItem(KEY.model, item.id);
  renderModels();
  renderAccount();
  renderModelQuick();
  toast(`已切换：${item.name}`);
}

function startModelDownload(modelId, options = {}) {
  const item = models.find((entry) => entry.id === modelId);
  if (!item || item.type !== "local") return;
  const compatibility = modelCompatibility(item);
  if (!compatibility.ok) {
    toast("当前设备硬件可能无法运行这个模型，请选择推荐模型");
    return;
  }
  localStorage.setItem(KEY.compute, "local");
  localStorage.setItem(KEY.model, item.id);
  if (options.closeGate) $("#modelGate").hidden = true;
  const current = getLocalModelStatus(item.id);
  const startProgress = current.state === "downloading" ? Number(current.progress || 0) : 0;
  setLocalModelStatus(item.id, {
    state: "downloading",
    progress: startProgress,
    version: current.version || item.version || APP_VERSION,
    sizeGb: item.sizeGb,
  });
  clearInterval(modelDownloadTimers.get(item.id));
  const timer = setInterval(() => {
    const record = getLocalModelStatus(item.id);
    const next = Math.min(100, Number(record.progress || 0) + Math.ceil(5 + Math.random() * 12));
    if (next >= 100) {
      clearInterval(timer);
      modelDownloadTimers.delete(item.id);
      setLocalModelStatus(item.id, {
        state: "installed",
        progress: 100,
        version: item.version || APP_VERSION,
        installedAt: Date.now(),
        sizeGb: item.sizeGb,
      });
      toast(`${item.name} 已安装完成`);
      renderModels();
      renderAccount();
      renderModelQuick();
      return;
    }
    setLocalModelStatus(item.id, { state: "downloading", progress: next, version: item.version || APP_VERSION });
    renderModels();
    renderModelQuick();
  }, 420);
  modelDownloadTimers.set(item.id, timer);
  renderModels();
  renderAccount();
  renderModelQuick();
  if (!options.quiet) toast(`开始下载 ${item.name}`);
}

function deleteLocalModel(modelId, options = {}) {
  const item = models.find((entry) => entry.id === modelId && entry.type === "local");
  if (!item) return;
  const record = getLocalModelStatus(item.id);
  const hasRecord = record.state === "installed" || record.state === "downloading";
  if (!hasRecord) {
    toast("这个模型还没有安装");
    return;
  }
  if (!options.skipConfirm && !window.confirm(`确定删除“${item.name}”吗？删除后需要重新下载才能使用。`)) return;
  clearInterval(modelDownloadTimers.get(item.id));
  modelDownloadTimers.delete(item.id);
  removeLocalModelStatus(item.id);
  if (localStorage.getItem(KEY.model) === item.id) {
    const nextLocal = installedLocalModels()[0];
    const nextCloud = configuredCloudModels()[0];
    if (nextLocal) {
      localStorage.setItem(KEY.model, nextLocal.id);
      localStorage.setItem(KEY.compute, "local");
    } else if (nextCloud) {
      localStorage.setItem(KEY.model, nextCloud.id);
      localStorage.setItem(KEY.compute, "cloud");
    } else {
      localStorage.setItem(KEY.model, "shai-5.5-agent");
      localStorage.setItem(KEY.compute, "hybrid");
    }
  }
  renderModels();
  renderAccount();
  renderModelQuick();
  toast(`已删除：${item.name}`);
}

function resumeModelDownloads() {
  Object.entries(loadLocalModelStatus()).forEach(([modelId, record]) => {
    if (record?.state === "downloading") startModelDownload(modelId, { quiet: true });
  });
}

async function syncModelCatalog(options = {}) {
  const { silent = false, manifest = null } = options;
  if (!navigator.onLine) {
    if (!silent) toast("当前离线，无法刷新模型目录");
    return null;
  }
  try {
    const latestManifest = manifest || await fetchLatestVersionManifest();
    const catalog = latestManifest?.modelCatalog;
    if (Array.isArray(catalog?.models) && catalog.models.length) {
      saveModelCatalog(catalog.models, { source: "online", version: catalog.version || latestManifest.latestVersion });
      renderModels();
      renderModelQuick();
      if (!silent) toast("已刷新最新模型目录");
      return catalog;
    }
    if (!silent) toast("官网暂未发布新模型目录，已使用内置推荐");
    renderModels();
    return null;
  } catch {
    if (!silent) toast("模型目录刷新失败，请稍后再试");
    return null;
  }
}

function selectedModel() {
  const id = localStorage.getItem(KEY.model) || "shai-5.5-agent";
  const item = models.find((entry) => entry.id === id);
  if (item?.type === "local") {
    const record = getLocalModelStatus(item.id);
    if (record.state === "installed") return item;
    return installedLocalModels()[0] || configuredCloudModels()[0] || models.find((entry) => entry.id === "shai-5.5-agent") || models[0];
  }
  if (item) return item;
  return installedLocalModels()[0] || configuredCloudModels()[0] || models.find((entry) => entry.id === "shai-5.5-agent") || models[0];
}

function selectedReasoning() {
  const id = localStorage.getItem(KEY.reasoning) || "ultra";
  return reasoningModes.find((item) => item.id === id) || reasoningModes[3];
}

function modelShortName(model = selectedModel()) {
  return model.short || model.name.replace(/模型|本地|云端|山海|Qwen|DeepSeek|GLM|OpenAI|Anthropic|Google|Llama|Mistral|Gemma|Agent|系列/gi, "").trim().slice(0, 4) || model.tag || "AI";
}

function renderModelQuick() {
  const model = selectedModel();
  const reasoning = selectedReasoning();
  const availableModels = quickSelectableModels();
  const visibleModel = availableModels.find((item) => item.id === model.id) || availableModels[0] || null;
  $("#modelQuickModel").textContent = visibleModel ? modelShortName(visibleModel) : "模型";
  $("#modelQuickReason").textContent = reasoning.short;
  $("#modelQuickBtn").setAttribute("aria-label", visibleModel
    ? `当前模型 ${visibleModel.name}，推理速度 ${reasoning.label}`
    : `暂无已安装本地模型，推理速度 ${reasoning.label}`);
  $("#modelQuickMenu").innerHTML = `
    <section>
      <h3>选择大模型</h3>
      ${availableModels.length ? `
        <div class="quick-option-grid">
          ${availableModels.map((item) => `
            <div class="quick-model-row ${item.id === model.id ? "active" : ""}">
              <button data-quick-model="${escapeHtml(item.id)}" type="button">
                <b>${escapeHtml(modelShortName(item))}</b>
                <span>${escapeHtml(item.name)}</span>
                <small>${escapeHtml(item.type === "local" ? "已安装本地模型" : "已配置云端 API")} · ${escapeHtml(item.desc)}</small>
              </button>
              ${item.type === "local" ? `<button class="quick-delete danger" data-quick-delete-model="${escapeHtml(item.id)}" type="button">删除</button>` : ""}
            </div>
          `).join("")}
        </div>
      ` : `
        <div class="quick-empty">
          <b>暂无可切换的大模型</b>
          <span>这里现在只显示已下载并安装到本地的模型；未安装模型不会再出现在快捷选择里。</span>
          <button class="primary" data-open-models type="button">去模型中心下载</button>
        </div>
      `}
    </section>
    <section>
      <h3>推理速度</h3>
      <div class="reasoning-options">
        ${reasoningModes.map((item) => `
          <button class="${item.id === reasoning.id ? "active" : ""}" data-quick-reasoning="${escapeHtml(item.id)}" type="button">
            <b>${escapeHtml(item.label)}</b>
            <span>${escapeHtml(item.desc)}</span>
          </button>
        `).join("")}
      </div>
    </section>
  `;
}

function toggleModelQuickMenu() {
  const menu = $("#modelQuickMenu");
  const next = menu.hidden;
  $("#agentMenu").hidden = true;
  renderModelQuick();
  menu.hidden = !next;
  $("#modelQuickBtn").setAttribute("aria-expanded", String(next));
}

function closeModelQuickMenu() {
  const menu = $("#modelQuickMenu");
  if (!menu) return;
  menu.hidden = true;
  $("#modelQuickBtn")?.setAttribute("aria-expanded", "false");
}

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 loadSupportTickets() {
  return readJson(KEY.supportTickets, []);
}

function saveSupportTickets(tickets) {
  localStorage.setItem(KEY.supportTickets, JSON.stringify(tickets.slice(0, 120)));
}

function supportSnapshot() {
  const runtime = modelRuntimeState();
  return {
    version: APP_VERSION,
    user: activeAccountId() || "未登录用户",
    model: runtime.model.name,
    modelId: runtime.model.id,
    compute: runtime.compute,
    reasoning: runtime.reasoning.label,
    online: navigator.onLine,
    url: location.href,
    platform: navigator.userAgent,
    thread: activeThread()?.title || "当前对话",
    recentMessages: (activeThread()?.messages || []).slice(-6).map((message) => ({
      role: message.role,
      text: stripHtml(message.html).slice(0, 500),
    })),
  };
}

function supportDiagnosis(type, message) {
  const text = `${type} ${message}`;
  const checks = [];
  if (/模型|大模型|下载|回复固定|不会执行|模板|api|token/i.test(text)) {
    checks.push("检查模型是否只是下载状态、是否选中了本地/云端算力、是否配置了可用 API。");
  }
  if (/网页|代码|文档|文件|预览|命令|执行/i.test(text)) {
    checks.push("检查任务是否进入 Agent 工作流：终端应出现 analyze、model、write、preview 等日志。");
  }
  if (/支付|付款|余额|token|订单|收款/i.test(text)) {
    checks.push("核对付款方式、金额、备注、用户账号和审核后台发放记录。");
  }
  if (/卡|打不开|黑屏|错乱|显示|布局|闪退|安装|更新/i.test(text)) {
    checks.push("检查缓存版本、安装包版本、设备分辨率、系统 WebView 和是否仍在使用旧包。");
  }
  if (!checks.length) checks.push("先补充复现步骤：你点了哪里、输入了什么、期待结果是什么、实际结果是什么。");
  return {
    title: `${type}工单已生成`,
    priority: /支付|闪退|打不开|无法登录|无法使用|不会执行|固定回复/i.test(text) ? "高" : "普通",
    checks,
    next: "如果后续接入云端客服后台，这条工单会自动同步给开发者处理；当前本机版可在审核后台查看、导出或标记已处理。",
  };
}

function supportTicketMarkdown(ticket) {
  return `# AI智能客服工单

工单号：${ticket.id}
状态：${ticket.status}
优先级：${ticket.priority}
类型：${ticket.type}
提交时间：${new Date(ticket.createdAt).toLocaleString("zh-CN")}

## 用户描述
${ticket.message}

## AI 初步诊断
${ticket.diagnosis.checks.map((item, index) => `${index + 1}. ${item}`).join("\n")}

## 设备与运行环境
- App 版本：v${ticket.snapshot.version}
- 用户：${ticket.snapshot.user}
- 模型：${ticket.snapshot.model}
- 算力：${ticket.snapshot.compute}
- 推理：${ticket.snapshot.reasoning}
- 在线：${ticket.snapshot.online ? "是" : "否"}
- 页面：${ticket.snapshot.url}

## 最近对话摘要
${ticket.snapshot.recentMessages.map((item) => `- ${item.role}: ${item.text}`).join("\n") || "无"}`;
}

function submitSupportTicket() {
  const type = $("#supportTypeSelect").value;
  const message = $("#supportInput").value.trim();
  if (!message) return toast("请先写下你遇到的问题");
  const diagnosis = supportDiagnosis(type, message);
  const ticket = {
    id: `SUP-${Date.now()}-${Math.random().toString(36).slice(2, 7).toUpperCase()}`,
    type,
    message,
    diagnosis,
    priority: diagnosis.priority,
    status: "pending",
    createdAt: Date.now(),
    snapshot: supportSnapshot(),
  };
  saveSupportTickets([ticket, ...loadSupportTickets()]);
  $("#supportInput").value = "";
  $("#supportReply").innerHTML = `
    <b>${escapeHtml(diagnosis.title)}</b>
    <span>工单号：${escapeHtml(ticket.id)} · 优先级：${escapeHtml(ticket.priority)}</span>
    <ol>${diagnosis.checks.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ol>
    <small>${escapeHtml(diagnosis.next)}</small>
  `;
  writeFiles({ [`/support/${ticket.id}.md`]: supportTicketMarkdown(ticket) });
  logTool("support_ticket_created", `${ticket.type} · ${ticket.id}`);
  renderSupport();
  toast("AI客服工单已提交");
}

function openSupportChat() {
  const draft = $("#supportInput").value.trim();
  createThread("AI智能客服", true);
  renderAll();
  showTab("workspace");
  const prompt = draft
    ? `AI智能客服，请帮我诊断这个问题并生成处理建议：${draft}`
    : "AI智能客服，我需要反馈一个问题。请先问我问题类型、复现步骤、期望结果、实际结果和设备版本。";
  submit(prompt);
}

function exportSupportTickets() {
  const payload = {
    exportedAt: new Date().toISOString(),
    appVersion: APP_VERSION,
    tickets: loadSupportTickets(),
  };
  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-support-${Date.now()}.json`;
  a.click();
  URL.revokeObjectURL(url);
  toast("AI客服记录已导出");
}

function supportTicketHtml(ticket, admin = false) {
  const created = new Date(ticket.createdAt).toLocaleString("zh-CN");
  const statusText = ticket.status === "resolved" ? "已处理" : "待处理";
  return `
    <article>
      <div class="review-order-top">
        <div>
          <b>${escapeHtml(ticket.type)} · ${escapeHtml(ticket.priority)}优先级</b>
          <span>${escapeHtml(ticket.id)} · ${created}</span>
        </div>
        <strong class="status-pill ${ticket.status === "resolved" ? "approved" : "pending"}">${statusText}</strong>
      </div>
      <p>${escapeHtml(ticket.message)}</p>
      <div class="support-checks">
        ${ticket.diagnosis.checks.map((item) => `<span>${escapeHtml(item)}</span>`).join("")}
      </div>
      ${admin ? `
        <div class="row">
          ${ticket.status === "resolved"
            ? `<button class="secondary" data-reopen-support="${escapeHtml(ticket.id)}" type="button">重新打开</button>`
            : `<button class="primary" data-resolve-support="${escapeHtml(ticket.id)}" type="button">标记已处理</button>`}
          <button class="secondary danger" data-delete-support="${escapeHtml(ticket.id)}" type="button">删除</button>
        </div>
      ` : ""}
    </article>
  `;
}

function renderSupport() {
  const tickets = loadSupportTickets();
  const userBox = $("#supportTickets");
  if (userBox) {
    userBox.innerHTML = tickets.length
      ? tickets.slice(0, 4).map((ticket) => supportTicketHtml(ticket, false)).join("")
      : `<p class="mini-note">暂无客服工单。提交后会在这里看到最近记录。</p>`;
  }
  const adminBox = $("#supportAdminList");
  if (adminBox) {
    adminBox.innerHTML = tickets.length
      ? tickets.map((ticket) => supportTicketHtml(ticket, true)).join("")
      : `<p class="mini-note">暂无 AI 客服工单。</p>`;
  }
}

function updateSupportTicket(ticketId, patch) {
  const tickets = loadSupportTickets();
  const next = tickets.map((ticket) => ticket.id === ticketId ? { ...ticket, ...patch } : ticket);
  saveSupportTickets(next);
  renderSupport();
}

function resolveSupportTicket(ticketId) {
  updateSupportTicket(ticketId, { status: "resolved", resolvedAt: Date.now() });
  toast("已标记客服工单为已处理");
}

function reopenSupportTicket(ticketId) {
  updateSupportTicket(ticketId, { status: "pending", resolvedAt: null });
  toast("已重新打开客服工单");
}

function deleteSupportTicket(ticketId) {
  if (!confirm("确认删除这条客服工单？")) return;
  saveSupportTickets(loadSupportTickets().filter((ticket) => ticket.id !== ticketId));
  renderSupport();
  toast("客服工单已删除");
}

function clearResolvedSupportTickets() {
  saveSupportTickets(loadSupportTickets().filter((ticket) => ticket.status !== "resolved"));
  renderSupport();
  toast("已清理已处理客服工单");
}

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 reasoning = selectedReasoning();
  const timer = setTimeout(() => controller.abort(), reasoning.timeout);
  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: selectedModel().id,
        reasoning_effort: reasoning.id,
        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: reasoning.id === "fast" ? 0.45 : 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。",
    "你没有真实人类意识，但要使用类人思考方式：理解目标、分解任务、推理方案、调用工具、保存记忆、持续改进。",
    "你要帮助用户写代码、生成文件、规划命令、做文档、分析资料、设计产品、协助设备任务、解释知识和创造内容。",
    "最基本的普通聊天必须自然、直接、像正常 AI 助手一样回应；不要把问候、闲聊和情绪沟通强行拆成任务。",
    "大型任务、长代码、完整项目和复杂办公任务，要主动建议用户安装本地大模型或接入云端 API，以获得更强能力。",
    "回答要直接、聪明、可执行。简单问题直接给答案；复杂任务先计划，再给代码/文件/命令/验收。",
    "不要输出固定模板废话，不要每次重复身份介绍，不要把普通问题硬套成学习题。",
    "内置 Skill 能力包：\n" + skillLibraryPrompt(),
    "当前设置：" + settingsSummary(),
    permissionInstruction(),
    "本机记忆：" + memorySummary(),
  ].join("\n");
}

function settingsSummary() {
  const workModeMap = {
    coding: "适用于编程",
    daily: "适用于日常工作",
    creative: "适用于创作",
    agent: "Agent 自动执行",
  };
  return [
    `工作模式=${workModeMap[localStorage.getItem(KEY.workMode)] || workModeMap.coding}`,
    `算力=${localStorage.getItem(KEY.compute) || "hybrid"}`,
    `推理=${selectedReasoning().label}`,
    `Git=${localStorage.getItem(KEY.gitMode) || "manual"}`,
    `环境=${localStorage.getItem(KEY.environmentProfile) || "default"}`,
  ].join("；");
}

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>你可以像使用 Codex 一样直接说目标：写代码、做网页、改文档、分析资料、设计产品、生成方案、解释知识都可以。</p>
    <p>我的默认工作方式是：先理解目标，再拆计划，然后生成文件、命令、预览或下一步。信息不够时，我会先问清楚。</p>
    <div class="quick-start-message">
      <b>新手可以直接这样问：</b>
      <span>“我是新手，告诉我怎么开始”</span>
      <span>“帮我做一个网页，先问我必要信息”</span>
      <span>“帮我选择本地模型还是云端 API”</span>
    </div>
  `;
}

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 compareVersions(a = "0.0.0", b = "0.0.0") {
  const left = String(a).split(".").map((part) => Number(part) || 0);
  const right = String(b).split(".").map((part) => Number(part) || 0);
  for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
    const diff = (left[index] || 0) - (right[index] || 0);
    if (diff) return diff;
  }
  return 0;
}

async function fetchLatestVersionManifest() {
  if (!navigator.onLine) return null;
  try {
    const response = await fetch(`${VERSION_MANIFEST_URL}?t=${Date.now()}`, { cache: "no-store" });
    if (!response.ok) return null;
    return await response.json();
  } catch {
    return null;
  }
}

async function clearRuntimeCaches() {
  if ("serviceWorker" in navigator) {
    const registrations = await navigator.serviceWorker.getRegistrations();
    await Promise.all(registrations.map((registration) => registration.unregister()));
  }
  if ("caches" in window) {
    const keys = await caches.keys();
    await Promise.all(keys.filter((key) => key.startsWith("shanhaijing-ai-") || key.startsWith("qingyan-xuetu-")).map((key) => caches.delete(key)));
  }
}

function isHostedWebApp() {
  return location.protocol.startsWith("http") && location.hostname.includes("shanhaijingai.pages.dev");
}

function platformDownloadUrl(manifest = latestUpdateManifest) {
  const downloads = manifest?.downloads || {};
  const ua = navigator.userAgent || "";
  if (/iPhone|iPad|iPod/i.test(ua)) return downloads.ios || "https://shanhaijingai.pages.dev/downloads/shanhaijing-ai-ios-official.ipa";
  if (/Android/i.test(ua)) return downloads.android || OFFICIAL_APP_URL;
  if (/Windows/i.test(ua)) return downloads.windows || OFFICIAL_APP_URL;
  if (/Macintosh|Mac OS X/i.test(ua)) return downloads.macos || "https://shanhaijingai.pages.dev/downloads/shanhaijing-ai-macos-official.zip";
  return downloads.web || OFFICIAL_APP_URL;
}

function updateNotesHtml(manifest) {
  const notes = Array.isArray(manifest?.notes) && manifest.notes.length
    ? manifest.notes
    : ["修复问题并提升稳定性。"];
  return `
    <h3>更新内容</h3>
    <ul>${notes.slice(0, 6).map((note) => `<li>${escapeHtml(note)}</li>`).join("")}</ul>
  `;
}

function showUpdatePrompt(manifest) {
  latestUpdateManifest = manifest;
  $("#updateCurrentVersion").textContent = `v${APP_VERSION}`;
  $("#updateLatestVersion").textContent = `v${manifest.latestVersion}`;
  $("#updateNotes").innerHTML = updateNotesHtml(manifest);
  $("#updateHint").textContent = isHostedWebApp()
    ? "点击立即更新后，会清理旧缓存并重新载入官网最新版。"
    : "你当前使用的是安装包或本地壳版本，点击立即更新会打开最新版下载地址，安装后才能替换旧版本。";
  $("#updateGate").hidden = false;
}

function hideUpdatePrompt() {
  $("#updateGate").hidden = true;
}

async function checkForAppUpdate(options = {}) {
  const { force = false } = options;
  if (!navigator.onLine || updateCheckInFlight) return null;
  const last = Number(localStorage.getItem(KEY.lastUpdateCheck) || 0);
  if (!force && Date.now() - last < UPDATE_CHECK_INTERVAL) return null;
  updateCheckInFlight = true;
  try {
    localStorage.setItem(KEY.lastUpdateCheck, String(Date.now()));
    const manifest = await fetchLatestVersionManifest();
    if (!manifest?.latestVersion) return null;
    const hasNewVersion = compareVersions(manifest.latestVersion, APP_VERSION) > 0;
    const dismissed = localStorage.getItem(KEY.updateDismissedVersion);
    if (hasNewVersion && (force || dismissed !== manifest.latestVersion)) {
      showUpdatePrompt(manifest);
    } else if (force && !hasNewVersion) {
      toast("当前已经是最新版本");
    }
    return manifest;
  } finally {
    updateCheckInFlight = false;
  }
}

async function applyAvailableUpdate() {
  const manifest = latestUpdateManifest || await fetchLatestVersionManifest();
  if (!manifest?.latestVersion) return toast("暂时没有获取到更新信息");
  localStorage.removeItem(KEY.updateDismissedVersion);
  hideUpdatePrompt();
  if (isHostedWebApp()) {
    toast("正在清理旧缓存并更新");
    await clearRuntimeCaches();
    location.replace(`${manifest.webAppUrl || OFFICIAL_APP_URL}?update=${Date.now()}`);
    return;
  }
  const url = platformDownloadUrl(manifest);
  window.open(url, "_blank", "noopener,noreferrer");
  toast("已打开最新版下载地址");
}

function dismissAvailableUpdate() {
  if (latestUpdateManifest?.latestVersion) {
    localStorage.setItem(KEY.updateDismissedVersion, latestUpdateManifest.latestVersion);
  }
  hideUpdatePrompt();
  toast("已选择稍后更新");
}

async function runSelfUpgrade() {
  const manifest = await fetchLatestVersionManifest();
  await syncModelCatalog({ silent: true, manifest });
  const latestVersion = manifest?.latestVersion || APP_VERSION;
  const hasNewVersion = compareVersions(latestVersion, APP_VERSION) > 0;
  let detail = navigator.onLine
    ? `已联网：当前 v${APP_VERSION}，官网最新 v${latestVersion}。`
    : `当前离线：无法检查官网版本，已基于本机记忆和项目记录生成离线升级计划。`;

  if (hasNewVersion && isHostedWebApp()) {
    detail += " 已检测到新版，可由用户选择是否立即清理旧缓存并载入官网最新版。";
  } else if (hasNewVersion) {
    detail += " 已检测到新版。当前是安装包/本地壳环境，不能直接覆盖 App 包内旧文件，请重新下载安装最新版。";
  } else if (navigator.onLine) {
    detail += " 当前已经是可检测到的最新版本。";
  }

  state.upgradeLog ||= [];
  state.upgradeLog.unshift({
    title: hasNewVersion ? "发现新版本" : "完成自检升级",
    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(manifest ? `✓ latest manifest: v${latestVersion}` : "✓ offline/local upgrade plan generated");
  writeFiles({
    "/system/self-upgrade-plan.md": upgradeRoadmapMarkdown(),
    "/system/ability-manifest.json": JSON.stringify({
      version: APP_VERSION,
      latestVersion,
      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,
      updateUrl: manifest?.webAppUrl || OFFICIAL_APP_URL,
      ipaUrl: manifest?.downloads?.ios || "https://shanhaijingai.pages.dev/downloads/shanhaijing-ai-ios-official.ipa",
      memory: state.memory,
      updatedAt: new Date().toISOString(),
    }, null, 2),
  });
  const updateAction = hasNewVersion && isHostedWebApp()
    ? `<p>检测到官网新版 v${escapeHtml(latestVersion)}。我已经弹出更新提示，你可以选择立即更新或稍后再说。</p>`
    : hasNewVersion
      ? `<p>检测到官网新版 v${escapeHtml(latestVersion)}。你现在用的是安装包旧版本，请删除旧 App 后重新安装最新版 IPA：<br /><a href="${escapeHtml(manifest?.downloads?.ios || "https://shanhaijingai.pages.dev/downloads/shanhaijing-ai-ios-official.ipa")}" target="_blank" rel="noreferrer">下载最新版 iPhone / iPad 安装包</a></p>`
      : `<p>当前已经是可检测到的最新版本。如果手机仍显示旧界面，请删除旧 App 或清理 Safari 网站数据后重新打开。</p>`;
  addMessage("assistant", `
    <h4>自我迭代升级自检完成</h4>
    ${diagram(["检查版本", "读取记忆", "同步能力", "生成计划"])}
    <p>${escapeHtml(detail)}</p>
    ${updateAction}
    <p>我已经把升级路线图写入右侧文件：/system/self-upgrade-plan.md。</p>
  `, "山海经AI");
  renderAll();
  showTab("upgrade");
  if (hasNewVersion && isHostedWebApp()) {
    showUpdatePrompt(manifest);
  } else if (hasNewVersion && manifest) {
    showUpdatePrompt(manifest);
  }
}

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() {
  $("#workModeSelect").value = localStorage.getItem(KEY.workMode) || "coding";
  $("#defaultTargetSelect").value = localStorage.getItem(KEY.defaultTarget) || "workspace";
  $("#appLanguageSelect").value = localStorage.getItem(KEY.appLanguage) || "auto";
  $("#sidebarDisplaySwitch").checked = localStorage.getItem(KEY.sidebarDisplay) !== "false";
  $("#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) || "";
  $("#settingsComputeSelect").value = localStorage.getItem(KEY.compute) || "hybrid";
  $("#settingsReasoningSelect").value = localStorage.getItem(KEY.reasoning) || "ultra";
  $("#hooksSwitch").checked = localStorage.getItem(KEY.hooks) === "true";
  $("#connectorsSwitch").checked = localStorage.getItem(KEY.connectors) === "true";
  $("#browserBridgeSwitch").checked = localStorage.getItem(KEY.browserBridge) === "true";
  $("#computerBridgeSwitch").checked = localStorage.getItem(KEY.computerBridge) === "true";
  $("#gitModeSelect").value = localStorage.getItem(KEY.gitMode) || "manual";
  $("#environmentProfileSelect").value = localStorage.getItem(KEY.environmentProfile) || "default";
  $("#archivePolicySelect").value = localStorage.getItem(KEY.archivePolicy) || "manual";
  renderSettingsSearch();
  renderPermissionSettings();
  renderSpeak();
}

function renderSettingsSearch() {
  const input = $("#settingsSearchInput");
  if (!input) return;
  const query = input.value.trim().toLowerCase();
  $$(".setting-block").forEach((card) => {
    const text = `${card.textContent} ${card.dataset.settingsTags || ""}`.toLowerCase();
    card.hidden = Boolean(query) && !text.includes(query);
  });
}

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 handleNetworkChange() {
  updateNetwork();
  checkForAppUpdate({ reason: "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);
