import { Hono } from 'hono'
import JSZip from 'jszip'

const app = new Hono()

const SUPER_ADMIN_EMAIL = '494161546@qq.com'
const SUPER_ADMIN_PHONE = '13136092523'
const PURPOSES = ['create', 'evaluate', 'run', 'flowchart', 'api']

function nowSec() {
  return Math.floor(Date.now() / 1000)
}

function genId(prefix) {
  const random = crypto.randomUUID().replace(/-/g, '').slice(0, 6)
  return `${prefix}_${Date.now().toString(36)}${random}`.slice(0, prefix.length + 13)
}

function id(c) {
  return c.get('identity') || {}
}

function isSuperAdmin(identity) {
  return identity?.email === SUPER_ADMIN_EMAIL || identity?.phone === SUPER_ADMIN_PHONE
}

function requireIdentity(c) {
  const identity = id(c)
  if (!identity.phone && !identity.email) {
    return c.json({ error: 'unauthenticated', message: '缺少登录态：X-User-Phone 或 X-User-Email' }, 401)
  }
  return null
}

function requireAdmin(c) {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  if (!isSuperAdmin(id(c))) return c.json({ error: 'forbidden', message: '仅超级管理员可操作' }, 403)
  return null
}

function ownerOrAdmin(row, identity) {
  return isSuperAdmin(identity) ||
    (identity?.phone && row?.creator_phone && identity.phone === row.creator_phone) ||
    (identity?.email && row?.creator_email && identity.email === row.creator_email)
}

function jsonText(value, fallback = '{}') {
  try { return JSON.stringify(value ?? {}) } catch { return fallback }
}

function safeJsonArray(value, fallback = []) {
  if (Array.isArray(value)) return value.map(String).filter(Boolean)
  try {
    const parsed = JSON.parse(String(value || ''))
    return Array.isArray(parsed) ? parsed.map(String).filter(Boolean) : fallback
  } catch {
    return fallback
  }
}

function splitOrigins(value) {
  return String(value || '*').split(',').map(s => s.trim()).filter(Boolean)
}

function corsOrigin(c) {
  const configured = splitOrigins(c.env?.ALLOW_ORIGINS || '*')
  if (configured.includes('*')) return '*'
  const origin = c.req.header('Origin') || ''
  if (origin && configured.includes(origin)) return origin
  return configured[0] || origin || '*'
}

function sanitizeFilename(name) {
  return String(name || 'package.zip').replace(/[^\w.\-\u4e00-\u9fff]+/g, '_').slice(0, 120) || 'package.zip'
}

function safePackageKey(key) {
  const normalized = String(key || '').replace(/\\/g, '/').replace(/^\/+/, '')
  if (!normalized || normalized.includes('..') || normalized.split('/').some(part => !part || part === '.')) return ''
  return normalized
}

function textFromInput(body) {
  if (typeof body?.input === 'string') return body.input
  if (body?.input !== undefined) return jsonText(body.input, String(body.input || ''))
  const last = Array.isArray(body?.messages) ? [...body.messages].reverse().find(m => m?.role === 'user') : null
  if (last?.content) return String(last.content)
  return jsonText(body || {}, '')
}

function num(value) {
  const n = Number(value ?? 0)
  return Number.isFinite(n) ? n : 0
}

function round(value, digits = 1) {
  const n = Number(value)
  if (!Number.isFinite(n)) return 0
  const m = 10 ** digits
  return Math.round(n * m) / m
}

async function hashHex(text) {
  const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(String(text || '')))
  return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join('')
}

function randomKey() {
  const bytes = new Uint8Array(24)
  crypto.getRandomValues(bytes)
  let s = ''
  for (const b of bytes) s += String.fromCharCode(b)
  return `sk_live_${btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')}`
}

async function audit(c, auditData) {
  if (!auditData) return
  const identity = id(c)
  try {
    await c.env.DB.prepare(`
      INSERT INTO audit_logs (action, actor_name, actor_phone, actor_email, target_type, target_id, target_name, ip, ua, meta)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).bind(
      auditData.action || `${c.req.method} ${c.req.path}`,
      auditData.actor_name || '',
      identity.phone || '',
      identity.email || '',
      auditData.target_type || auditData.targetType || '',
      auditData.target_id || auditData.targetId || '',
      auditData.target_name || auditData.targetName || '',
      c.req.header('CF-Connecting-IP') || c.req.header('X-Forwarded-For') || '',
      c.req.header('User-Agent') || '',
      auditData.meta ? jsonText(auditData.meta) : null,
    ).run()
  } catch (e) {
    console.warn('[audit]', e?.message || e)
  }
}

function secretPreview(value) {
  const s = String(value || '').trim()
  if (!s) return null
  return s.length <= 4 ? `...${s}` : `...${s.slice(-4)}`
}

function isPublicEndpoint(url) {
  return /^https?:\/\//i.test(String(url || '').trim())
}

function missingAgentRun(row = {}) {
  const missing = []
  if (!isPublicEndpoint(row.endpoint_public_url)) missing.push('endpoint_public_url')
  if (!String(row.api_key || '').trim()) missing.push('api_key')
  return missing
}

function agentRunReady(row = {}) {
  return !!row.enabled && missingAgentRun(row).length === 0
}

function rowToAgentRun(row = {}) {
  const endpointName = String(row.endpoint_name || 'default').trim() || 'default'
  const invocationKey = String(row.api_key || '').trim()
  const accessKeyId = String(row.access_key_id || '').trim()
  const accessKeySecret = String(row.access_key_secret || '').trim()
  const missing = missingAgentRun(row)
  return {
    code: row.code || 'agentrun',
    name: row.name || '阿里云 AgentRun',
    icon: row.icon || 'Cloud',
    protocol: 'agentrun',
    baseUrl: row.endpoint_public_url || '',
    defaultModel: endpointName,
    models: [{ value: endpointName, label: endpointName }],
    enabled: !!row.enabled,
    isActive: !!row.is_active,
    hasKey: !!invocationKey,
    keyPreview: secretPreview(invocationKey),
    hasInvocationKey: !!invocationKey,
    invocationKeyPreview: secretPreview(invocationKey),
    hasAccessKeyId: !!accessKeyId,
    accessKeyIdPreview: secretPreview(accessKeyId),
    hasAccessKeySecret: !!accessKeySecret,
    hasSecurityToken: !!String(row.security_token || '').trim(),
    accountId: row.account_id || '',
    regionId: row.region_id || 'cn-hangzhou',
    runtimeName: row.runtime_name || '',
    endpointName,
    runtimeId: row.runtime_id || '',
    endpointId: row.endpoint_id || '',
    endpointPublicUrl: row.endpoint_public_url || '',
    runtimeStatus: row.runtime_status || '',
    endpointStatus: row.endpoint_status || '',
    purposeScopes: safeJsonArray(row.purpose_scopes_json, ['create', 'evaluate', 'run', 'api']),
    timeoutMs: Number(row.timeout_ms || 300000),
    notes: row.notes || '',
    missing,
    ready: agentRunReady(row),
  }
}

function rowToRuntime(row = {}) {
  if (row.protocol === 'agentrun') return rowToAgentRun(row)
  const args = safeJsonArray(row.local_args_json, [])
  const healthcheckArgs = safeJsonArray(row.local_healthcheck_args_json, ['--version'])
  const command = String(row.local_command || '').trim()
  return {
    code: row.code,
    name: row.name || row.code,
    icon: row.icon || 'Terminal',
    protocol: row.protocol,
    enabled: !!row.enabled,
    isActive: !!row.is_active,
    hasKey: false,
    command,
    args,
    argsText: args.join(' '),
    healthcheckArgs,
    healthcheckArgsText: healthcheckArgs.join(' '),
    workingDir: row.local_working_dir || '',
    env: {},
    envText: row.local_env_json || '',
    purposeScopes: safeJsonArray(row.purpose_scopes_json, []),
    timeoutMs: Number(row.timeout_ms || 300000),
    notes: row.notes || '',
    missing: command ? [] : ['local_command'],
    ready: !!command,
  }
}

function rowToProvider(row = {}) {
  return {
    code: row.code,
    name: row.name,
    icon: row.icon,
    protocol: row.protocol,
    baseUrl: row.base_url,
    defaultModel: row.default_model,
    models: safeJsonArray(row.models_json, []),
    enabled: !!row.enabled,
    isActive: !!row.is_active,
    hasKey: !!row.api_key,
    keyPreview: secretPreview(row.api_key),
  }
}

function rowToAgent(row = {}) {
  return {
    id: row.id,
    name: row.name || '',
    description: row.description || '',
    prompt: row.prompt || '',
    systemSkillKind: row.system_skill_kind || '',
    skillId: row.skill_id || '',
    skillName: row.skill_name || '',
    runtimeCode: row.runtime_code || '',
    appliesTo: safeJsonArray(row.applies_to_json, []),
    enabled: !!row.enabled,
    updatedAt: row.updated_at || null,
  }
}

function sanitizeAgentRunPatch(body = {}, existing = {}) {
  const patch = {}
  const copyString = (from, to = from) => {
    if (typeof body[from] === 'string') patch[to] = body[from].trim()
  }
  const copySecret = (from, to = from) => {
    if (typeof body[from] === 'string' && body[from].trim()) patch[to] = body[from].trim()
  }

  copySecret('accessKeyId', 'access_key_id')
  copySecret('accessKeySecret', 'access_key_secret')
  copySecret('securityToken', 'security_token')
  copySecret('invocationKey', 'api_key')
  copyString('accountId', 'account_id')
  copyString('regionId', 'region_id')
  copyString('runtimeName', 'runtime_name')
  copyString('endpointName', 'endpoint_name')
  copyString('defaultModel', 'endpoint_name')
  copyString('runtimeId', 'runtime_id')
  copyString('endpointId', 'endpoint_id')
  copyString('endpointPublicUrl', 'endpoint_public_url')
  copyString('baseUrl', 'endpoint_public_url')
  if (Array.isArray(body.purposeScopes)) {
    patch.purpose_scopes_json = JSON.stringify(body.purposeScopes.filter(p => PURPOSES.includes(p)))
  }
  if (typeof body.timeoutMs === 'number' || typeof body.timeoutMs === 'string') {
    patch.timeout_ms = Math.max(5000, Math.min(60 * 60 * 1000, Number(body.timeoutMs) || 300000))
  }
  copyString('notes', 'notes')
  if (typeof body.enabled === 'boolean') patch.enabled = body.enabled ? 1 : 0
  if (typeof body.isActive === 'boolean') patch.is_active = body.isActive ? 1 : 0

  const merged = { ...existing, ...patch }
  if (!merged.region_id) merged.region_id = 'cn-hangzhou'
  if (!merged.endpoint_name) merged.endpoint_name = 'default'
  return { patch, merged }
}

function stringifyList(list) {
  return JSON.stringify((Array.isArray(list) ? list : []).map(String).map(s => s.trim()).filter(Boolean))
}

function splitArgsText(text) {
  const out = []
  const re = /"([^"]*)"|'([^']*)'|(\S+)/g
  let match
  while ((match = re.exec(String(text || '')))) {
    out.push(match[1] ?? match[2] ?? match[3] ?? '')
  }
  return out.filter(Boolean)
}

function sanitizeLocalRuntimePatch(body = {}) {
  const patch = {}
  const copy = (field, column) => {
    if (typeof body[field] === 'string') patch[column] = body[field].trim()
  }
  copy('name', 'name')
  copy('icon', 'icon')
  copy('command', 'local_command')
  copy('workingDir', 'local_working_dir')
  copy('notes', 'notes')
  if (Array.isArray(body.args)) patch.local_args_json = stringifyList(body.args)
  if (typeof body.argsText === 'string') patch.local_args_json = stringifyList(splitArgsText(body.argsText))
  if (Array.isArray(body.healthcheckArgs)) patch.local_healthcheck_args_json = stringifyList(body.healthcheckArgs)
  if (typeof body.healthcheckArgsText === 'string') patch.local_healthcheck_args_json = stringifyList(splitArgsText(body.healthcheckArgsText))
  if (Array.isArray(body.purposeScopes)) patch.purpose_scopes_json = JSON.stringify(body.purposeScopes.filter(p => PURPOSES.includes(p)))
  if (typeof body.envText === 'string') patch.local_env_json = body.envText
  if (typeof body.timeoutMs === 'number' || typeof body.timeoutMs === 'string') {
    patch.timeout_ms = Math.max(5000, Math.min(60 * 60 * 1000, Number(body.timeoutMs) || 300000))
  }
  if (typeof body.enabled === 'boolean') patch.enabled = body.enabled ? 1 : 0
  return patch
}

function providerModelsUrl(row = {}) {
  let url = String(row.base_url || '').trim()
  if (!url) return ''
  if (/\/chat\/completions\/?$/i.test(url)) return url.replace(/\/chat\/completions\/?$/i, '/models')
  return url.replace(/\/?$/, '/models')
}

async function testLlmProvider(row = {}) {
  if (!row.api_key) return { ok: false, message: '未配置 API Key' }
  const started = Date.now()
  if (row.protocol === 'anthropic') {
    const res = await fetch(row.base_url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': row.api_key,
        'anthropic-version': '2023-06-01',
      },
      body: JSON.stringify({
        model: row.default_model,
        max_tokens: 16,
        messages: [{ role: 'user', content: 'Hi' }],
      }),
    })
    const data = await res.json().catch(async () => ({ raw: await res.text().catch(() => '') }))
    if (!res.ok) return { ok: false, message: data?.error?.message || data?.raw || `HTTP ${res.status}` }
    return {
      ok: true,
      model: data.model || row.default_model,
      latencyMs: Date.now() - started,
      content: String(data.content?.[0]?.text || '').slice(0, 80),
    }
  }

  const res = await fetch(row.base_url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${row.api_key}`,
    },
    body: JSON.stringify({
      model: row.default_model,
      messages: [{ role: 'user', content: 'Hi' }],
      max_tokens: 16,
    }),
  })
  const data = await res.json().catch(async () => ({ raw: await res.text().catch(() => '') }))
  if (!res.ok) return { ok: false, message: data?.error?.message || data?.message || data?.raw || `HTTP ${res.status}` }
  return {
    ok: true,
    model: data.model || row.default_model,
    latencyMs: Date.now() - started,
    content: extractText(data).slice(0, 80),
  }
}

async function activeAgentRun(c) {
  return await c.env.DB.prepare(
    "SELECT * FROM runtime_engines WHERE code = 'agentrun' AND enabled = 1 AND is_active = 1 LIMIT 1"
  ).first()
}

function chatUrl(row) {
  const base = String(row.endpoint_public_url || '').trim().replace(/\/+$/, '')
  if (/\/chat\/completions$/i.test(base)) return base
  if (/\/v1$/i.test(base)) return `${base}/chat/completions`
  return `${base}/v1/chat/completions`
}

function extractText(data) {
  if (!data) return ''
  if (typeof data === 'string') return data
  if (data.output) return String(data.output)
  if (data.content) return String(data.content)
  if (data.choices?.[0]?.message?.content) return String(data.choices[0].message.content)
  if (data.choices?.[0]?.delta?.content) return String(data.choices[0].delta.content)
  if (data.text) return String(data.text)
  return ''
}

async function callAgentRun(row, messages, { stream = false } = {}) {
  if (!agentRunReady(row)) {
    throw new Error(`AgentRun 配置未就绪：${missingAgentRun(row).join(', ')}`)
  }
  const res = await fetch(chatUrl(row), {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${row.api_key}`,
    },
    body: JSON.stringify({
      model: row.endpoint_name || 'default',
      messages,
      stream,
    }),
  })
  if (stream && res.ok && res.body) return res
  const text = await res.text()
  let data = null
  try { data = JSON.parse(text) } catch {}
  if (!res.ok) {
    throw new Error(data?.error?.message || data?.message || text || `AgentRun HTTP ${res.status}`)
  }
  return extractText(data) || text
}

async function readSkillPackageText(c, skill) {
  const fallback = `# ${skill.name || '未命名 Skill'}\n\n${skill.description || ''}`.trim()
  const key = safePackageKey(skill.package_key)
  if (!key) return fallback
  const buf = await getPackageArrayBuffer(c, key).catch(() => null)
  if (!buf) return fallback
  const filename = String(skill.package_name || key).toLowerCase()
  if (filename.endsWith('.zip') || key.toLowerCase().endsWith('.zip')) {
    const zip = await JSZip.loadAsync(buf)
    const entries = []
    zip.forEach((path, entry) => {
      if (entry.dir || path.startsWith('__MACOSX/')) return
      if (/\.(md|markdown|txt)$/i.test(path)) entries.push({ path, entry })
    })
    if (!entries.length) return fallback
    const score = (p) => {
      const base = p.split('/').pop().toLowerCase()
      if (base === 'skill.md') return 0
      if (base === 'readme.md') return 1
      if (/\.md$/i.test(base)) return 2
      return 3
    }
    entries.sort((a, b) => score(a.path) - score(b.path) || a.path.length - b.path.length)
    return await entries[0].entry.async('string')
  }
  return new TextDecoder().decode(buf)
}

const D1_FILE_LIMIT = 900 * 1024

function asArrayBuffer(value) {
  if (!value) return null
  if (value instanceof ArrayBuffer) return value
  if (value instanceof Uint8Array) {
    return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength)
  }
  return null
}

async function getPackageArrayBuffer(c, key) {
  if (c.env.R2) {
    const obj = await c.env.R2.get(key).catch(() => null)
    if (!obj) return null
    return await obj.arrayBuffer()
  }
  const row = await c.env.DB.prepare('SELECT content FROM file_blobs WHERE r2_key = ? LIMIT 1').bind(key).first()
  return asArrayBuffer(row?.content)
}

async function savePackageBytes(c, key, bytes, metadata = {}) {
  if (c.env.R2) {
    await c.env.R2.put(key, bytes, {
      httpMetadata: { contentType: metadata.mime || 'application/octet-stream' },
      customMetadata: metadata.custom || {},
    })
    return
  }
  const buffer = asArrayBuffer(bytes) || bytes
  if (buffer.byteLength > D1_FILE_LIMIT) {
    throw new Error(`d1_file_too_large:${buffer.byteLength}`)
  }
  await c.env.DB.prepare('INSERT OR REPLACE INTO file_blobs (r2_key, content) VALUES (?, ?)')
    .bind(key, buffer)
    .run()
}

async function createZipInStorage(c, skillId, name, description) {
  if (!description) return { package_key: null, package_size: null, package_name: null }
  const safeName = sanitizeFilename(name || 'skill').replace(/\.[^.]+$/, '')
  const fileId = genId('file')
  const today = new Date().toISOString().slice(0, 10)
  const mdName = `${safeName}.md`
  const zipName = `${fileId}_${safeName}.zip`
  const key = `pkg/${today}/${zipName}`
  const zip = new JSZip()
  zip.file(mdName, description)
  const bytes = await zip.generateAsync({ type: 'uint8array', compression: 'DEFLATE' })
  await savePackageBytes(c, key, bytes, {
    mime: 'application/zip',
    custom: { skill_id: skillId, source: 'create' },
  })
  const identity = id(c)
  await c.env.DB.prepare(`
    INSERT INTO files (id, r2_key, filename, size, mime, uploader_phone, uploader_email, bound_skill_id)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?)
  `).bind(fileId, key, zipName, bytes.byteLength, 'application/zip', identity.phone || null, identity.email || null, skillId).run()
  return { package_key: key, package_size: bytes.byteLength, package_name: zipName }
}

function domainSql(alias = 's') {
  return `CASE
    WHEN ${alias}.node_path IS NULL OR ${alias}.node_path = '' THEN '未归类'
    WHEN instr(${alias}.node_path, ' / ') > 0 THEN substr(${alias}.node_path, 1, instr(${alias}.node_path, ' / ') - 1)
    ELSE ${alias}.node_path
  END`
}

app.use('*', async (c, next) => {
  c.header('Access-Control-Allow-Origin', corsOrigin(c))
  c.header('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS')
  c.header('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-User-Phone,X-User-Email,X-User-Name')
  c.header('Access-Control-Max-Age', '600')
  if (c.req.method === 'OPTIONS') return c.body(null, 204)
  await next()
})

app.use('*', async (c, next) => {
  c.set('identity', {
    phone: (c.req.header('X-User-Phone') || '').trim(),
    email: (c.req.header('X-User-Email') || '').trim(),
    loginName: (c.req.header('X-User-Name') || '').trim(),
  })
  await next()
})

app.get('/api/health', (c) => c.json({ ok: true, runtime: 'cloudflare-lite', ts: Date.now() }))

app.post('/api/users/sync', async (c) => {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  const identity = id(c)
  let user = null
  if (identity.email) user = await c.env.DB.prepare('SELECT * FROM users WHERE email = ? LIMIT 1').bind(identity.email).first()
  if (!user && identity.phone) user = await c.env.DB.prepare('SELECT * FROM users WHERE phone = ? LIMIT 1').bind(identity.phone).first()
  if (user) {
    await c.env.DB.prepare('UPDATE users SET last_login_at = datetime(\'now\'), name = COALESCE(NULLIF(?, \'\'), name) WHERE id = ?')
      .bind(identity.loginName || '', user.id).run()
    const fresh = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(user.id).first()
    return c.json({ data: fresh })
  }
  const userId = genId('user')
  await c.env.DB.prepare(`
    INSERT INTO users (id, email, phone, name, role, created_at, last_login_at)
    VALUES (?, ?, ?, ?, 'user', datetime('now'), datetime('now'))
  `).bind(userId, identity.email || null, identity.phone || null, identity.loginName || null).run()
  return c.json({ data: await c.env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first() }, 201)
})

app.get('/api/users', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const q = c.req.query('q') || ''
  const args = []
  const where = ['1=1']
  if (q) {
    where.push('(email LIKE ? OR phone LIKE ? OR name LIKE ?)')
    args.push(`%${q}%`, `%${q}%`, `%${q}%`)
  }
  const { results } = await c.env.DB.prepare(`
    SELECT id, email, phone, name, role, created_at, last_login_at
    FROM users WHERE ${where.join(' AND ')} ORDER BY created_at DESC
  `).bind(...args).all()
  return c.json({ data: results || [] })
})

app.put('/api/users/:userId/role', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const role = (await c.req.json().catch(() => ({}))).role
  if (!['user', 'admin'].includes(role)) return c.json({ error: 'role 必须是 user 或 admin' }, 422)
  const userId = c.req.param('userId')
  const user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first()
  if (!user) return c.json({ error: 'not_found' }, 404)
  if (user.email === SUPER_ADMIN_EMAIL || user.phone === SUPER_ADMIN_PHONE) {
    return c.json({ error: '不允许修改超级管理员角色' }, 403)
  }
  await c.env.DB.prepare('UPDATE users SET role = ? WHERE id = ?').bind(role, userId).run()
  await audit(c, { action: 'user.role.update', target_type: 'user', target_id: userId, meta: { role } })
  return c.json({ data: await c.env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first() })
})

app.delete('/api/users/:userId', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const userId = c.req.param('userId')
  const user = await c.env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(userId).first()
  if (!user) return c.json({ error: 'not_found' }, 404)
  if (user.email === SUPER_ADMIN_EMAIL || user.phone === SUPER_ADMIN_PHONE) {
    return c.json({ error: '不允许删除超级管理员' }, 403)
  }
  await c.env.DB.prepare('DELETE FROM users WHERE id = ?').bind(userId).run()
  await audit(c, { action: 'user.delete', target_type: 'user', target_id: userId, meta: { email: user.email, phone: user.phone } })
  return c.json({ ok: true })
})

app.get('/api/categories', async (c) => {
  c.header('Cache-Control', 'no-store')
  const { results } = await c.env.DB.prepare(`
    SELECT id, parent_id, code, name, level, path, status, sort
    FROM categories ORDER BY sort ASC
  `).all()
  return c.json({ data: results || [] })
})

app.get('/api/categories/tree', async (c) => {
  c.header('Cache-Control', 'no-store')
  const { results } = await c.env.DB.prepare(`
    SELECT id, parent_id, code, name, level, path, status, sort
    FROM categories ORDER BY sort ASC
  `).all()
  const map = new Map()
  const roots = []
  for (const row of results || []) {
    const node = { ...row, children: [] }
    map.set(node.id, node)
  }
  for (const node of map.values()) {
    if (node.parent_id && map.has(node.parent_id)) map.get(node.parent_id).children.push(node)
    else roots.push(node)
  }
  return c.json({ data: roots })
})

app.get('/api/categories/:categoryId', async (c) => {
  const row = await c.env.DB.prepare('SELECT * FROM categories WHERE id = ? LIMIT 1').bind(c.req.param('categoryId')).first()
  if (!row) return c.json({ error: 'not_found' }, 404)
  return c.json({ data: row })
})

app.post('/api/categories', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const body = await c.req.json().catch(() => ({}))
  const name = String(body.name || '').trim()
  const parentId = body.parent_id || null
  const sort = Number.isFinite(Number(body.sort)) ? Number(body.sort) : 0
  if (!name) return c.json({ error: 'name 必填' }, 400)
  let level = 1
  let path = name
  if (parentId) {
    const parent = await c.env.DB.prepare('SELECT id, level, path FROM categories WHERE id = ?').bind(parentId).first()
    if (!parent) return c.json({ error: '父节点不存在' }, 400)
    level = Number(parent.level || 0) + 1
    path = `${parent.path} / ${name}`
  }
  const dup = await c.env.DB.prepare('SELECT id FROM categories WHERE path = ? LIMIT 1').bind(path).first()
  if (dup) return c.json({ error: '同级下已存在同名节点' }, 409)
  const categoryId = genId('cat')
  await c.env.DB.prepare(`
    INSERT INTO categories (id, parent_id, code, name, level, path, status, sort, created_at)
    VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?)
  `).bind(categoryId, parentId, '', name, level, path, sort, nowSec()).run()
  await audit(c, { action: 'category.create', target_type: 'category', target_id: categoryId, target_name: name })
  return c.json({ data: await c.env.DB.prepare('SELECT * FROM categories WHERE id = ?').bind(categoryId).first() }, 201)
})

app.put('/api/categories/:categoryId', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const categoryId = c.req.param('categoryId')
  const body = await c.req.json().catch(() => ({}))
  const name = String(body.name || '').trim()
  if (!name) return c.json({ error: 'name 必填' }, 400)
  const cur = await c.env.DB.prepare('SELECT * FROM categories WHERE id = ?').bind(categoryId).first()
  if (!cur) return c.json({ error: 'not_found' }, 404)
  let newPath = name
  if (cur.parent_id) {
    const parent = await c.env.DB.prepare('SELECT path FROM categories WHERE id = ?').bind(cur.parent_id).first()
    if (!parent) return c.json({ error: '父节点不存在' }, 400)
    newPath = `${parent.path} / ${name}`
  }
  const oldPath = cur.path
  await c.env.DB.prepare('UPDATE categories SET name = ?, path = ? WHERE id = ?').bind(name, newPath, categoryId).run()
  if (oldPath !== newPath) {
    await c.env.DB.prepare("UPDATE categories SET path = ? || substr(path, length(?) + 1) WHERE path LIKE ? || ' / %'")
      .bind(newPath, oldPath, oldPath).run()
    await c.env.DB.prepare('UPDATE skills SET node_path = ? WHERE node_path = ?').bind(newPath, oldPath).run()
    await c.env.DB.prepare("UPDATE skills SET node_path = ? || substr(node_path, length(?) + 1) WHERE node_path LIKE ? || ' / %'")
      .bind(newPath, oldPath, oldPath).run()
  }
  await audit(c, { action: 'category.update', target_type: 'category', target_id: categoryId, target_name: name })
  return c.json({ data: await c.env.DB.prepare('SELECT * FROM categories WHERE id = ?').bind(categoryId).first() })
})

app.delete('/api/categories/:categoryId', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const categoryId = c.req.param('categoryId')
  const cur = await c.env.DB.prepare('SELECT id FROM categories WHERE id = ?').bind(categoryId).first()
  if (!cur) return c.json({ error: 'not_found' }, 404)
  const used = await c.env.DB.prepare(`
    WITH RECURSIVE descendants(id) AS (
      SELECT id FROM categories WHERE id = ?
      UNION ALL
      SELECT c.id FROM categories c JOIN descendants d ON c.parent_id = d.id
    )
    SELECT COUNT(*) AS n FROM skills WHERE node_id IN (SELECT id FROM descendants)
  `).bind(categoryId).first()
  if (num(used?.n) > 0) return c.json({ error: '节点下仍有关联 Skill，不能删除' }, 409)
  await c.env.DB.prepare(`
    WITH RECURSIVE descendants(id) AS (
      SELECT id FROM categories WHERE id = ?
      UNION ALL
      SELECT c.id FROM categories c JOIN descendants d ON c.parent_id = d.id
    )
    DELETE FROM categories WHERE id IN (SELECT id FROM descendants)
  `).bind(categoryId).run()
  await audit(c, { action: 'category.delete', target_type: 'category', target_id: categoryId })
  return c.json({ ok: true })
})

app.post('/api/uploads', async (c) => {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  const form = await c.req.parseBody().catch(() => null)
  const file = form?.file
  if (!file || typeof file === 'string') return c.json({ error: 'no_file', message: '缺少 file 字段' }, 400)
  const filename = sanitizeFilename(file.name || 'package.zip')
  const size = Number(file.size || 0)
  const mime = file.type || 'application/octet-stream'
  const ext = (filename.split('.').pop() || '').toLowerCase()
  if (!['md', 'markdown', 'txt', 'zip'].includes(ext)) {
    return c.json({ error: 'invalid_file_type', message: '仅支持 md / markdown / txt / zip 文件' }, 422)
  }
  if (size > 50 * 1024 * 1024) return c.json({ error: 'file_too_large', message: '文件过大，上限 50 MB' }, 413)
  if (!c.env.R2 && size > D1_FILE_LIMIT) {
    return c.json({
      error: 'file_too_large_without_r2',
      message: '当前 Cloudflare 账号尚未开通 R2，临时 D1 存储仅支持 900 KB 以内的小包。',
    }, 413)
  }
  const fileId = genId('file')
  const today = new Date().toISOString().slice(0, 10)
  const key = `pkg/${today}/${fileId}_${filename}`
  if (c.env.R2) {
    await savePackageBytes(c, key, file.stream(), { mime, custom: { filename } })
  } else {
    await savePackageBytes(c, key, await file.arrayBuffer(), { mime, custom: { filename } })
  }
  const identity = id(c)
  await c.env.DB.prepare(`
    INSERT INTO files (id, r2_key, filename, size, mime, uploader_phone, uploader_email)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `).bind(fileId, key, filename, size, mime, identity.phone || null, identity.email || null).run()
  await audit(c, { action: 'file.upload', target_type: 'file', target_id: fileId, target_name: filename, meta: { r2_key: key, size, mime } })
  return c.json({ data: { file_id: fileId, r2_key: key, filename, size, mime } }, 201)
})

app.get('/api/uploads/download', async (c) => {
  const key = safePackageKey(c.req.query('key') || '')
  if (!key) return c.json({ error: 'invalid_key' }, 400)
  const meta = await c.env.DB.prepare('SELECT filename, mime FROM files WHERE r2_key = ? LIMIT 1').bind(key).first().catch(() => null)
  if (c.env.R2) {
    const obj = await c.env.R2.get(key)
    if (!obj) return c.json({ error: 'not_found' }, 404)
    const filename = meta?.filename || key.split('/').pop() || 'package.zip'
    return new Response(obj.body, {
      headers: {
        'Content-Type': obj.httpMetadata?.contentType || meta?.mime || 'application/octet-stream',
        'Content-Disposition': `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`,
        'Cache-Control': 'no-store',
      },
    })
  }
  const buf = await getPackageArrayBuffer(c, key)
  if (!buf) return c.json({ error: 'not_found' }, 404)
  const filename = meta?.filename || key.split('/').pop() || 'package.zip'
  return new Response(buf, {
    headers: {
      'Content-Type': meta?.mime || 'application/octet-stream',
      'Content-Disposition': `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`,
      'Cache-Control': 'no-store',
    },
  })
})

app.get('/api/uploads/orphans', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const { results } = await c.env.DB.prepare(`
    SELECT id, r2_key, filename, size, uploader_phone, uploader_email, created_at
    FROM files WHERE bound_skill_id IS NULL ORDER BY created_at DESC LIMIT 500
  `).all()
  return c.json({ data: results || [] })
})

app.get('/api/skills', async (c) => {
  const source = c.req.query('source') || ''
  const nodeId = c.req.query('node_id') || ''
  const status = c.req.query('status') || ''
  const q = c.req.query('q') || ''
  const page = Math.max(1, parseInt(c.req.query('page') || '1', 10))
  const size = Math.min(200, Math.max(1, parseInt(c.req.query('size') || '50', 10)))
  const where = ['1=1']
  const args = []
  if (source) { where.push('source = ?'); args.push(source) }
  if (nodeId) { where.push('node_id = ?'); args.push(nodeId) }
  if (status) { where.push('status = ?'); args.push(status) }
  if (q) { where.push('(name LIKE ? OR description LIKE ?)'); args.push(`%${q}%`, `%${q}%`) }
  const identity = id(c)
  if (!isSuperAdmin(identity)) {
    const hasIdentity = !!(identity.phone || identity.email)
    if (hasIdentity) {
      where.push("(visibility = 'public' OR (visibility = 'private' AND ((? <> '' AND creator_phone = ?) OR (? <> '' AND creator_email = ?))))")
      args.push(identity.phone || '', identity.phone || '', identity.email || '', identity.email || '')
    } else {
      where.push("visibility = 'public'")
    }
  }
  const total = await c.env.DB.prepare(`SELECT COUNT(*) AS n FROM skills WHERE ${where.join(' AND ')}`).bind(...args).first()
  const { results } = await c.env.DB.prepare(`
    SELECT id, name, description, node_id, node_path, source, status, visibility,
           creator_name, creator_phone, creator_email, package_key, package_size, package_name,
           download_count, like_count, dislike_count, created_at, updated_at
    FROM skills
    WHERE ${where.join(' AND ')}
    ORDER BY (COALESCE(like_count, 0) + COALESCE(download_count, 0) * 2) DESC, created_at DESC
    LIMIT ? OFFSET ?
  `).bind(...args, size, (page - 1) * size).all()
  return c.json({ data: results || [], total: total?.n || 0, page, size })
})

app.post('/api/skills', async (c) => {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  const body = await c.req.json().catch(() => ({}))
  const identity = id(c)
  const name = String(body.name || '').trim()
  const nodeId = String(body.node_id || '').trim()
  const source = String(body.source || '').trim()
  const creatorName = String(body.creator_name || '').trim()
  const errors = []
  if (!name) errors.push('name 必填')
  if (!nodeId) errors.push('node_id 必填（强制归位流程节点）')
  if (!['upload', 'create'].includes(source)) errors.push('source 必填（upload | create）')
  if (!creatorName) errors.push('creator_name 必填（创建人姓名）')
  if (source === 'upload' && !body.package_key) errors.push('source=upload 时 package_key 必填')
  if (errors.length) return c.json({ error: 'validation_failed', details: errors }, 422)

  const node = await c.env.DB.prepare('SELECT id, path, status FROM categories WHERE id = ? LIMIT 1').bind(nodeId).first()
  if (!node) return c.json({ error: 'invalid_node', message: 'node_id 不存在' }, 422)
  if (node.status === 'archived') return c.json({ error: 'node_archived', message: '该节点已归档不可选' }, 422)

  const skillId = genId('skill')
  const desc = String(body.description || '')
  let pkg = {
    package_key: body.package_key || null,
    package_size: body.package_size || null,
    package_name: body.package_name || null,
  }
  if (source === 'create' && desc && !pkg.package_key) {
    pkg = await createZipInStorage(c, skillId, name, desc)
  }
  const visibility = body.visibility === 'private' ? 'private' : 'public'
  await c.env.DB.prepare(`
    INSERT INTO skills
      (id, name, description, node_id, node_path, source, status, creator_name, creator_phone, creator_email,
       package_key, package_size, package_name, visibility, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, 'draft', ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).bind(
    skillId, name, desc, nodeId, node.path, source, creatorName, identity.phone || null, identity.email || null,
    pkg.package_key, pkg.package_size, pkg.package_name, visibility, nowSec(), nowSec(),
  ).run()
  if (pkg.package_key) {
    await c.env.DB.prepare('UPDATE files SET bound_skill_id = ? WHERE r2_key = ?').bind(skillId, pkg.package_key).run().catch(() => {})
  }
  await audit(c, { action: 'skill.create', actor_name: creatorName, target_type: 'skill', target_id: skillId, target_name: name, meta: { source, node_id: nodeId, node_path: node.path } })
  return c.json({ data: await c.env.DB.prepare('SELECT * FROM skills WHERE id = ?').bind(skillId).first() }, 201)
})

app.get('/api/skills/:skillId', async (c) => {
  const skill = await c.env.DB.prepare('SELECT * FROM skills WHERE id = ? LIMIT 1').bind(c.req.param('skillId')).first()
  if (!skill) return c.json({ error: 'not_found' }, 404)
  if (skill.visibility === 'private' && !ownerOrAdmin(skill, id(c))) return c.json({ error: 'not_found' }, 404)
  return c.json({ data: skill })
})

app.put('/api/skills/:skillId', async (c) => {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  const skillId = c.req.param('skillId')
  const body = await c.req.json().catch(() => ({}))
  const existing = await c.env.DB.prepare('SELECT * FROM skills WHERE id = ?').bind(skillId).first()
  if (!existing) return c.json({ error: 'not_found' }, 404)
  if (!ownerOrAdmin(existing, id(c))) return c.json({ error: 'forbidden', message: '只能编辑自己创建的 Skill' }, 403)
  const fields = []
  const args = []
  for (const k of ['name', 'description', 'status', 'creator_name']) {
    if (body[k] !== undefined) { fields.push(`${k} = ?`); args.push(body[k]) }
  }
  if (body.visibility !== undefined) {
    fields.push('visibility = ?')
    args.push(body.visibility === 'private' ? 'private' : 'public')
  }
  if (body.node_id) {
    const node = await c.env.DB.prepare('SELECT path, status FROM categories WHERE id = ?').bind(body.node_id).first()
    if (!node) return c.json({ error: 'invalid_node' }, 422)
    if (node.status === 'archived') return c.json({ error: 'node_archived' }, 422)
    fields.push('node_id = ?', 'node_path = ?')
    args.push(body.node_id, node.path)
  }
  if (!fields.length) return c.json({ data: existing })
  fields.push('updated_at = ?')
  args.push(nowSec(), skillId)
  await c.env.DB.prepare(`UPDATE skills SET ${fields.join(', ')} WHERE id = ?`).bind(...args).run()
  await audit(c, { action: 'skill.update', actor_name: body.creator_name || existing.creator_name, target_type: 'skill', target_id: skillId, target_name: body.name || existing.name, meta: { changed: Object.keys(body) } })
  return c.json({ data: await c.env.DB.prepare('SELECT * FROM skills WHERE id = ?').bind(skillId).first() })
})

app.delete('/api/skills/:skillId', async (c) => {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  const skillId = c.req.param('skillId')
  const existing = await c.env.DB.prepare('SELECT * FROM skills WHERE id = ?').bind(skillId).first()
  if (!existing) return c.json({ error: 'not_found' }, 404)
  if (!ownerOrAdmin(existing, id(c))) return c.json({ error: 'forbidden', message: '只能删除自己创建的 Skill' }, 403)
  await c.env.DB.prepare('DELETE FROM skills WHERE id = ?').bind(skillId).run()
  await c.env.DB.prepare('UPDATE files SET bound_skill_id = NULL WHERE bound_skill_id = ?').bind(skillId).run().catch(() => {})
  await audit(c, { action: 'skill.delete', actor_name: existing.creator_name, target_type: 'skill', target_id: skillId, target_name: existing.name })
  return c.json({ ok: true })
})

app.post('/api/skills/:skillId/download', async (c) => {
  const skillId = c.req.param('skillId')
  const skill = await c.env.DB.prepare('SELECT id FROM skills WHERE id = ?').bind(skillId).first()
  if (!skill) return c.json({ error: 'not_found' }, 404)
  const identity = id(c)
  const userKey = identity.email || identity.phone || ''
  let already = false
  if (userKey) {
    const prev = await c.env.DB.prepare('SELECT id FROM skill_downloads WHERE skill_id = ? AND user_key = ? LIMIT 1').bind(skillId, userKey).first()
    if (prev) already = true
    else {
      await c.env.DB.prepare('INSERT INTO skill_downloads (id, skill_id, user_key) VALUES (?, ?, ?)').bind(genId('dl'), skillId, userKey).run()
      await c.env.DB.prepare('UPDATE skills SET download_count = COALESCE(download_count, 0) + 1 WHERE id = ?').bind(skillId).run()
    }
  } else {
    await c.env.DB.prepare('UPDATE skills SET download_count = COALESCE(download_count, 0) + 1 WHERE id = ?').bind(skillId).run()
  }
  const fresh = await c.env.DB.prepare('SELECT download_count FROM skills WHERE id = ?').bind(skillId).first()
  return c.json({ ok: true, download_count: fresh?.download_count ?? 0, already })
})

app.get('/api/skills/:skillId/my-vote', async (c) => {
  const identity = id(c)
  const userKey = identity.email || identity.phone || 'anonymous'
  const row = await c.env.DB.prepare('SELECT vote FROM skill_votes WHERE skill_id = ? AND user_key = ? LIMIT 1')
    .bind(c.req.param('skillId'), userKey).first().catch(() => null)
  return c.json({ vote: row?.vote || null })
})

app.post('/api/skills/:skillId/vote', async (c) => {
  const skillId = c.req.param('skillId')
  const body = await c.req.json().catch(() => ({}))
  const vote = body.vote
  if (!['like', 'dislike'].includes(vote)) return c.json({ error: 'invalid_vote', message: 'vote 必须是 like 或 dislike' }, 422)
  const skill = await c.env.DB.prepare('SELECT id FROM skills WHERE id = ?').bind(skillId).first()
  if (!skill) return c.json({ error: 'not_found' }, 404)
  const identity = id(c)
  const userKey = identity.email || identity.phone || 'anonymous'
  const prev = await c.env.DB.prepare('SELECT vote FROM skill_votes WHERE skill_id = ? AND user_key = ? LIMIT 1').bind(skillId, userKey).first()
  if (!prev) {
    await c.env.DB.prepare('INSERT INTO skill_votes (id, skill_id, user_key, vote) VALUES (?, ?, ?, ?)').bind(genId('vote'), skillId, userKey, vote).run()
    await c.env.DB.prepare(`UPDATE skills SET ${vote}_count = COALESCE(${vote}_count, 0) + 1 WHERE id = ?`).bind(skillId).run()
  } else if (prev.vote === vote) {
    await c.env.DB.prepare('DELETE FROM skill_votes WHERE skill_id = ? AND user_key = ?').bind(skillId, userKey).run()
    await c.env.DB.prepare(`UPDATE skills SET ${vote}_count = MAX(0, COALESCE(${vote}_count, 0) - 1) WHERE id = ?`).bind(skillId).run()
    const fresh = await c.env.DB.prepare('SELECT like_count, dislike_count FROM skills WHERE id = ?').bind(skillId).first()
    return c.json({ ok: true, vote: null, like_count: fresh?.like_count ?? 0, dislike_count: fresh?.dislike_count ?? 0 })
  } else {
    const old = prev.vote
    await c.env.DB.prepare('UPDATE skill_votes SET vote = ? WHERE skill_id = ? AND user_key = ?').bind(vote, skillId, userKey).run()
    await c.env.DB.prepare(`UPDATE skills SET ${vote}_count = COALESCE(${vote}_count, 0) + 1, ${old}_count = MAX(0, COALESCE(${old}_count, 0) - 1) WHERE id = ?`).bind(skillId).run()
  }
  const fresh = await c.env.DB.prepare('SELECT like_count, dislike_count FROM skills WHERE id = ?').bind(skillId).first()
  return c.json({ ok: true, vote, like_count: fresh?.like_count ?? 0, dislike_count: fresh?.dislike_count ?? 0 })
})

app.get('/api/apikeys', async (c) => {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  const identity = id(c)
  const filters = []
  const args = []
  if (identity.email) { filters.push('k.created_by_email = ?'); args.push(identity.email) }
  if (identity.phone) { filters.push('k.created_by_phone = ?'); args.push(identity.phone) }
  const { results } = await c.env.DB.prepare(`
    SELECT k.id, k.skill_id, k.name, k.key_prefix, k.status, k.daily_limit, k.created_at,
           s.name AS skill_name,
           (SELECT COUNT(*) FROM skill_invocations WHERE api_key_id = k.id) AS total_calls
    FROM api_keys k
    LEFT JOIN skills s ON s.id = k.skill_id
    WHERE ${filters.join(' OR ')}
    ORDER BY k.created_at DESC
  `).bind(...args).all()
  return c.json({ data: results || [] })
})

app.post('/api/apikeys', async (c) => {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  const identity = id(c)
  const body = await c.req.json().catch(() => ({}))
  if (!body.skill_id) return c.json({ error: 'skill_id is required' }, 400)
  const skill = await c.env.DB.prepare('SELECT id, name, status, creator_phone, creator_email FROM skills WHERE id = ?').bind(body.skill_id).first()
  if (!skill) return c.json({ error: 'skill not found' }, 404)
  if (skill.status !== 'published') return c.json({ error: 'skill_not_published', message: '只有已上线 Skill 才能创建 API Key' }, 409)
  if (!ownerOrAdmin(skill, identity)) return c.json({ error: '无权为此 Skill 创建 API Key' }, 403)
  const raw = randomKey()
  const keyId = genId('key')
  const hash = await hashHex(raw)
  const prefix = `${raw.slice(0, 12)}...`
  await c.env.DB.prepare(`
    INSERT INTO api_keys (id, skill_id, name, key_hash, key_prefix, created_by_email, created_by_phone)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `).bind(keyId, skill.id, body.name || 'default', hash, prefix, identity.email || '', identity.phone || '').run()
  await audit(c, { action: 'create_api_key', target_type: 'api_key', target_id: keyId, target_name: body.name || 'default', meta: { skill_id: skill.id, skill_name: skill.name } })
  return c.json({
    id: keyId,
    skill_id: skill.id,
    skill_name: skill.name,
    name: body.name || 'default',
    key: raw,
    key_prefix: prefix,
    daily_limit: 1000,
    message: '请妥善保存此 API Key，它不会再次显示',
  }, 201)
})

app.delete('/api/apikeys/:keyId', async (c) => {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  const keyId = c.req.param('keyId')
  const record = await c.env.DB.prepare('SELECT * FROM api_keys WHERE id = ?').bind(keyId).first()
  if (!record) return c.json({ error: 'not found' }, 404)
  const identity = id(c)
  const owns = (identity.email && record.created_by_email === identity.email) || (identity.phone && record.created_by_phone === identity.phone)
  if (!owns && !isSuperAdmin(identity)) return c.json({ error: 'forbidden' }, 403)
  await c.env.DB.prepare('UPDATE api_keys SET status = ? WHERE id = ?').bind('revoked', keyId).run()
  await audit(c, { action: 'revoke_api_key', target_type: 'api_key', target_id: keyId, target_name: record.name })
  return c.json({ message: 'API Key 已吊销' })
})

app.get('/api/apikeys/:keyId/stats', async (c) => {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  const keyId = c.req.param('keyId')
  const record = await c.env.DB.prepare('SELECT * FROM api_keys WHERE id = ?').bind(keyId).first()
  if (!record) return c.json({ error: 'not found' }, 404)
  const identity = id(c)
  const owns = (identity.email && record.created_by_email === identity.email) || (identity.phone && record.created_by_phone === identity.phone)
  if (!owns && !isSuperAdmin(identity)) return c.json({ error: 'forbidden' }, 403)
  const todayStart = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000)
  const total = await c.env.DB.prepare('SELECT COUNT(*) AS n FROM skill_invocations WHERE api_key_id = ?').bind(keyId).first()
  const today = await c.env.DB.prepare('SELECT COUNT(*) AS n FROM skill_invocations WHERE api_key_id = ? AND ts >= ?').bind(keyId, todayStart).first()
  const errors = await c.env.DB.prepare("SELECT COUNT(*) AS n FROM skill_invocations WHERE api_key_id = ? AND response_status != 'success'").bind(keyId).first()
  const latency = await c.env.DB.prepare('SELECT AVG(latency_ms) AS avg_ms FROM skill_invocations WHERE api_key_id = ?').bind(keyId).first()
  const { results: recent } = await c.env.DB.prepare('SELECT ts, skill_name, response_status, latency_ms FROM skill_invocations WHERE api_key_id = ? ORDER BY ts DESC LIMIT 10').bind(keyId).all()
  return c.json({
    total_calls: total?.n || 0,
    today_calls: today?.n || 0,
    error_count: errors?.n || 0,
    success_rate: total?.n > 0 ? `${(((total.n - (errors?.n || 0)) / total.n) * 100).toFixed(1)}%` : '100%',
    avg_latency_ms: Math.round(latency?.avg_ms || 0),
    recent_calls: recent || [],
  })
})

app.get('/api/runtimes', async (c) => {
  const { results: rows } = await c.env.DB.prepare(`
    SELECT * FROM runtime_engines
    ORDER BY CASE code WHEN 'agentrun' THEN 1 ELSE 50 END, name
  `).all()
  const { results: agents } = await c.env.DB.prepare(`
    SELECT a.*, s.name AS skill_name
    FROM runtime_agents a
    LEFT JOIN skills s ON s.id = a.skill_id
    ORDER BY CASE a.id WHEN 'agent-skill-creator' THEN 1 WHEN 'agent-skill-evaluator' THEN 2 WHEN 'agent-skill-runner' THEN 3 ELSE 50 END, a.name
  `).all().catch(() => ({ results: [] }))
  return c.json({ runtimes: (rows || []).map(rowToRuntime), agents: (agents || []).map(rowToAgent) })
})

app.patch('/api/runtimes/:code', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const code = c.req.param('code')
  const existing = await c.env.DB.prepare('SELECT * FROM runtime_engines WHERE code = ?').bind(code).first()
  if (!existing) return c.json({ error: '运行时不存在' }, 404)
  const body = await c.req.json().catch(() => ({}))
  let patch = {}
  if (existing.protocol === 'agentrun') {
    const result = sanitizeAgentRunPatch(body, existing)
    patch = result.patch
    if (body.enabled === true && missingAgentRun(result.merged).length > 0) {
      return c.json({ error: `启用 AgentRun 前，请补齐：${missingAgentRun(result.merged).join('、')}` }, 422)
    }
  } else {
    patch = sanitizeLocalRuntimePatch(body)
  }
  const keys = Object.keys(patch)
  if (keys.length) {
    const sets = keys.map(k => `${k} = ?`)
    const vals = keys.map(k => patch[k])
    sets.push('updated_at = ?')
    vals.push(nowSec(), code)
    await c.env.DB.prepare(`UPDATE runtime_engines SET ${sets.join(', ')} WHERE code = ?`).bind(...vals).run()
  }
  const fresh = await c.env.DB.prepare('SELECT * FROM runtime_engines WHERE code = ?').bind(code).first()
  return c.json({ ok: true, runtime: rowToRuntime(fresh) })
})

app.post('/api/runtimes/:code/active', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const code = c.req.param('code')
  const row = await c.env.DB.prepare('SELECT * FROM runtime_engines WHERE code = ?').bind(code).first()
  if (!row) return c.json({ error: '运行时不存在' }, 404)
  if (row.protocol !== 'agentrun') {
    return c.json({
      error: 'Cloudflare 线上不能把本地 CLI 设为当前运行时；配置可保留，本地 Node 后端可执行。',
    }, 422)
  }
  if (!agentRunReady({ ...row, enabled: 1 })) return c.json({ error: `请先补齐运行时配置：${missingAgentRun(row).join('、')}` }, 400)
  await c.env.DB.prepare('UPDATE runtime_engines SET is_active = 0').run()
  await c.env.DB.prepare('UPDATE runtime_engines SET is_active = 1, enabled = 1, updated_at = ? WHERE code = ?').bind(nowSec(), code).run()
  return c.json({ ok: true })
})

app.post('/api/runtimes/:code/test', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const row = await c.env.DB.prepare('SELECT * FROM runtime_engines WHERE code = ?').bind(c.req.param('code')).first()
  if (!row) return c.json({ ok: false, message: '运行时不存在' }, 404)
  if (row.protocol !== 'agentrun') {
    return c.json({
      ok: false,
      message: '本地 CLI 运行时已保留配置，但 Cloudflare Worker 无法执行客户电脑上的本地命令。请在本地 Node 后端测试 Codex/Qoder。'
    }, 422)
  }
  const t0 = Date.now()
  try {
    const content = await callAgentRun(row, [{ role: 'user', content: 'Hi' }])
    return c.json({ ok: true, model: row.endpoint_name || 'default', latencyMs: Date.now() - t0, content: String(content || 'AgentRun Runtime 已响应').slice(0, 80) })
  } catch (e) {
    return c.json({ ok: false, message: e?.message || String(e) })
  }
})

app.put('/api/runtimes/agents/:agentId', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const agentId = String(c.req.param('agentId') || '').replace(/[^a-z0-9_-]/gi, '').slice(0, 80) || genId('agent')
  const body = await c.req.json().catch(() => ({}))
  const existing = await c.env.DB.prepare('SELECT * FROM runtime_agents WHERE id = ?').bind(agentId).first().catch(() => null)
  const agent = {
    id: agentId,
    name: String(body.name || existing?.name || '运行智能体').trim() || '运行智能体',
    description: String(body.description || existing?.description || '').trim(),
    prompt: String(body.prompt || existing?.prompt || '').trim(),
    system_skill_kind: String(body.systemSkillKind || body.system_skill_kind || existing?.system_skill_kind || '').trim() || null,
    skill_id: String(body.skillId || body.skill_id || existing?.skill_id || '').trim() || null,
    runtime_code: String(body.runtimeCode || body.runtime_code || existing?.runtime_code || '').trim() || 'agentrun',
    applies_to_json: JSON.stringify(Array.isArray(body.appliesTo) ? body.appliesTo.filter(p => PURPOSES.includes(p)) : safeJsonArray(existing?.applies_to_json, [])),
    enabled: typeof body.enabled === 'boolean' ? (body.enabled ? 1 : 0) : (existing?.enabled ?? 1),
  }
  if (existing) {
    await c.env.DB.prepare(`
      UPDATE runtime_agents SET name = ?, description = ?, prompt = ?, system_skill_kind = ?, skill_id = ?,
        runtime_code = ?, applies_to_json = ?, enabled = ?, updated_at = ?
      WHERE id = ?
    `).bind(agent.name, agent.description, agent.prompt, agent.system_skill_kind, agent.skill_id, agent.runtime_code, agent.applies_to_json, agent.enabled, nowSec(), agent.id).run()
  } else {
    await c.env.DB.prepare(`
      INSERT INTO runtime_agents (id, name, description, prompt, system_skill_kind, skill_id, runtime_code, applies_to_json, enabled, updated_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).bind(agent.id, agent.name, agent.description, agent.prompt, agent.system_skill_kind, agent.skill_id, agent.runtime_code, agent.applies_to_json, agent.enabled, nowSec()).run()
  }
  const row = await c.env.DB.prepare('SELECT * FROM runtime_agents WHERE id = ?').bind(agent.id).first()
  return c.json({ ok: true, agent: rowToAgent(row) })
})

app.get('/api/providers', async (c) => {
  const { results: rows } = await c.env.DB.prepare("SELECT * FROM llm_providers WHERE protocol IN ('openai','anthropic') ORDER BY name").all().catch(() => ({ results: [] }))
  const providers = (rows || []).map(rowToProvider)
  const agentrun = await c.env.DB.prepare("SELECT * FROM runtime_engines WHERE code = 'agentrun'").first().catch(() => null)
  if (agentrun) providers.push(rowToAgentRun(agentrun))
  return c.json({ list: providers })
})

app.patch('/api/providers/:code', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const code = c.req.param('code')
  if (code === 'agentrun') {
    const existing = await c.env.DB.prepare("SELECT * FROM runtime_engines WHERE code = 'agentrun'").first()
    if (!existing) return c.json({ error: '运行时不存在' }, 404)
    const body = await c.req.json().catch(() => ({}))
    const { patch, merged } = sanitizeAgentRunPatch(body, existing)
    if (body.enabled === true && missingAgentRun(merged).length > 0) {
      return c.json({ error: `启用 AgentRun 前，请补齐：${missingAgentRun(merged).join('、')}` }, 422)
    }
    const keys = Object.keys(patch)
    if (keys.length) {
      const sets = keys.map(k => `${k} = ?`)
      const vals = keys.map(k => patch[k])
      sets.push('updated_at = ?')
      vals.push(nowSec(), 'agentrun')
      await c.env.DB.prepare(`UPDATE runtime_engines SET ${sets.join(', ')} WHERE code = ?`).bind(...vals).run()
    }
    const fresh = await c.env.DB.prepare("SELECT * FROM runtime_engines WHERE code = 'agentrun'").first()
    return c.json({ ok: true, provider: rowToAgentRun(fresh) })
  }
  const existing = await c.env.DB.prepare('SELECT * FROM llm_providers WHERE code = ?').bind(code).first()
  if (!existing) return c.json({ error: '服务商不存在' }, 404)
  const body = await c.req.json().catch(() => ({}))
  const fields = []
  const args = []
  if (typeof body.apiKey === 'string' && body.apiKey.trim()) {
    fields.push('api_key = ?')
    args.push(body.apiKey.trim())
  }
  if (typeof body.baseUrl === 'string') {
    fields.push('base_url = ?')
    args.push(body.baseUrl.trim())
  }
  if (typeof body.defaultModel === 'string') {
    fields.push('default_model = ?')
    args.push(body.defaultModel.trim())
  }
  if (typeof body.enabled === 'boolean') {
    fields.push('enabled = ?')
    args.push(body.enabled ? 1 : 0)
  }
  if (!fields.length) return c.json({ ok: true, provider: rowToProvider(existing) })
  fields.push('updated_at = ?')
  args.push(nowSec(), code)
  await c.env.DB.prepare(`UPDATE llm_providers SET ${fields.join(', ')} WHERE code = ?`).bind(...args).run()
  const fresh = await c.env.DB.prepare('SELECT * FROM llm_providers WHERE code = ?').bind(code).first()
  return c.json({ ok: true, provider: rowToProvider(fresh) })
})

app.post('/api/providers/:code/active', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const code = c.req.param('code')
  if (code === 'agentrun') {
    const row = await c.env.DB.prepare("SELECT * FROM runtime_engines WHERE code = 'agentrun'").first()
    if (!row) return c.json({ error: 'AgentRun 配置不存在' }, 404)
    if (!agentRunReady(row)) return c.json({ error: `请先补齐 AgentRun 配置：${missingAgentRun(row).join('、')}` }, 400)
    await c.env.DB.prepare('UPDATE runtime_engines SET is_active = 0').run()
    await c.env.DB.prepare("UPDATE runtime_engines SET is_active = 1, enabled = 1, updated_at = ? WHERE code = 'agentrun'").bind(nowSec()).run()
    return c.json({ ok: true })
  }
  const row = await c.env.DB.prepare('SELECT * FROM llm_providers WHERE code = ?').bind(code).first()
  if (!row) return c.json({ error: '服务商不存在' }, 404)
  if (!row.api_key) return c.json({ error: '请先配置 API Key' }, 400)
  await c.env.DB.prepare("UPDATE llm_providers SET is_active = 0 WHERE protocol IN ('openai','anthropic')").run()
  await c.env.DB.prepare('UPDATE llm_providers SET is_active = 1, enabled = 1, updated_at = ? WHERE code = ?').bind(nowSec(), code).run()
  return c.json({ ok: true })
})

app.post('/api/providers/:code/test', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  if (c.req.param('code') !== 'agentrun') {
    const row = await c.env.DB.prepare('SELECT * FROM llm_providers WHERE code = ?').bind(c.req.param('code')).first()
    if (!row) return c.json({ error: '服务商不存在' }, 404)
    try {
      return c.json(await testLlmProvider(row))
    } catch (e) {
      return c.json({ ok: false, message: e?.message || String(e) })
    }
  }
  const row = await c.env.DB.prepare("SELECT * FROM runtime_engines WHERE code = 'agentrun'").first()
  try {
    const content = await callAgentRun(row, [{ role: 'user', content: 'Hi' }])
    return c.json({ ok: true, model: row.endpoint_name || 'default', content: String(content || 'AgentRun Runtime 已响应').slice(0, 80) })
  } catch (e) {
    return c.json({ ok: false, message: e?.message || String(e) })
  }
})

app.get('/api/providers/:code/models', async (c) => {
  const code = c.req.param('code')
  if (code === 'agentrun') {
    const row = await c.env.DB.prepare("SELECT * FROM runtime_engines WHERE code = 'agentrun'").first().catch(() => null)
    const endpoint = row?.endpoint_name || 'default'
    return c.json({ models: [{ value: endpoint, label: endpoint }] })
  }
  const row = await c.env.DB.prepare('SELECT * FROM llm_providers WHERE code = ?').bind(code).first()
  if (!row) return c.json({ models: [], error: '服务商不存在' })
  if (!row.api_key) return c.json({ models: [] })
  if (row.protocol === 'anthropic') {
    return c.json({
      models: [
        'claude-sonnet-4-20250514',
        'claude-3-5-sonnet-20241022',
        'claude-3-5-haiku-20241022',
      ].map(m => ({ value: m, label: m })),
    })
  }
  try {
    const res = await fetch(providerModelsUrl(row), {
      headers: { 'Authorization': `Bearer ${row.api_key}` },
    })
    const data = await res.json().catch(async () => ({ raw: await res.text().catch(() => '') }))
    if (!res.ok) return c.json({ models: [], error: data?.error?.message || data?.raw || `HTTP ${res.status}` })
    const models = (Array.isArray(data.data) ? data.data : [])
      .map(m => ({ value: m.id, label: m.id }))
      .sort((a, b) => a.label.localeCompare(b.label))
    return c.json({ models })
  } catch (e) {
    return c.json({ models: [], error: e?.message || String(e) })
  }
})

app.get('/api/skill-run', async (c) => {
  const row = await activeAgentRun(c)
  const ready = agentRunReady(row || {})
  return c.json({
    hasEngine: ready,
    engineKind: ready ? 'agentrun' : null,
    agentRun: row ? rowToAgentRun(row) : null,
    message: ready ? 'AgentRun 已就绪' : '请先在智能体配置中填写 AgentRun 公网集成 URL 与调用凭证',
  })
})

function sse(events) {
  const encoder = new TextEncoder()
  return new Response(new ReadableStream({
    start(controller) {
      for (const event of events) {
        controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`))
      }
      controller.enqueue(encoder.encode('data: [DONE]\n\n'))
      controller.close()
    },
  }), {
    headers: {
      'Content-Type': 'text/event-stream; charset=utf-8',
      'Cache-Control': 'no-store',
    },
  })
}

app.post('/api/skill-run/stream', async (c) => {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  const body = await c.req.json().catch(() => ({}))
  const sessionId = body.sessionId || genId('sess')
  const runtime = await activeAgentRun(c)
  if (!agentRunReady(runtime || {})) {
    return sse([
      { t: 'session', sessionId },
      { t: 'error', v: 'AgentRun 未配置。请在智能体配置中填写公网集成 URL 与调用凭证。', sessionId },
    ])
  }

  const mode = body.mode || 'run'
  const userInput = textFromInput(body)
  let systemPrompt = '你是 AIPROS Lite 的 Skill 工作流智能体。请用清晰、可执行的中文输出结果。'
  if (mode === 'create') {
    systemPrompt = [
      '你是 Skill 创建智能体。请根据用户需求输出一个可上传的标准 Skill 包。',
      '必须包含三个 Markdown 文件内容：SKILL.md、examples.md、rubric.md。',
      '每个文件用独立 Markdown 代码块表示，代码块标题写文件名。',
      'SKILL.md 应包含名称、目标、输入、输出、步骤、边界和质量标准。',
    ].join('\n')
  } else if (mode === 'evaluate') {
    systemPrompt = '你是 Skill 评测智能体。请按完整性、可运行性、边界、输出质量给出评分、结论和修改建议。'
  } else if (body.skillId || body.packageKey) {
    const skill = body.skillId
      ? await c.env.DB.prepare('SELECT * FROM skills WHERE id = ?').bind(body.skillId).first().catch(() => null)
      : null
    if (skill) systemPrompt = await readSkillPackageText(c, skill)
  }

  try {
    const content = await callAgentRun(runtime, [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: userInput || '请继续。' },
    ])
    return sse([
      { t: 'session', sessionId },
      { t: 'delta', v: String(content || ''), sessionId },
      { t: 'done', ok: true, sessionId },
    ])
  } catch (e) {
    return sse([
      { t: 'session', sessionId },
      { t: 'error', v: e?.message || String(e), sessionId },
    ])
  }
})

app.get('/api/skill-run/sessions/:sessionId/status', async (c) => c.json({ sessionId: c.req.param('sessionId'), status: 'done', events: [] }))
app.post('/api/skill-run/sessions/:sessionId/repack', async (c) => c.json({ error: 'Cloudflare Lite 不保存会话文件，请直接上传前端生成的 ZIP 包' }, 410))
app.get('/api/skill-run/outputs/:sessionId/:filename', async (c) => c.json({ error: 'not_found' }, 404))

app.get('/api/eval', async (c) => {
  const row = await activeAgentRun(c)
  return c.json({ hasKey: agentRunReady(row || {}), provider: row ? 'agentrun' : null, message: row ? '' : '未配置 AgentRun' })
})

app.get('/api/audit', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  const action = c.req.query('action') || ''
  const actorPhone = c.req.query('actor_phone') || ''
  const page = Math.max(1, parseInt(c.req.query('page') || '1', 10))
  const size = Math.min(100, Math.max(1, parseInt(c.req.query('size') || '50', 10)))
  const where = ['1=1']
  const args = []
  if (action) { where.push('action LIKE ?'); args.push(`%${action}%`) }
  if (actorPhone) { where.push('actor_phone LIKE ?'); args.push(`%${actorPhone}%`) }
  const total = await c.env.DB.prepare(`SELECT COUNT(*) AS n FROM audit_logs WHERE ${where.join(' AND ')}`).bind(...args).first()
  const { results } = await c.env.DB.prepare(`
    SELECT * FROM audit_logs WHERE ${where.join(' AND ')}
    ORDER BY ts DESC LIMIT ? OFFSET ?
  `).bind(...args, size, (page - 1) * size).all()
  return c.json({ data: results || [], total: total?.n || 0, page, size })
})

app.get('/api/stats', async (c) => {
  const total = await c.env.DB.prepare('SELECT COUNT(*) AS n FROM skills').first()
  const published = await c.env.DB.prepare("SELECT COUNT(*) AS n FROM skills WHERE status = 'published'").first()
  return c.json({ totalSkills: total?.n || 0, publishedSkills: published?.n || 0 })
})

app.get('/api/ops/rankings', async (c) => {
  const unauth = requireIdentity(c)
  if (unauth) return unauth
  const limit = Math.min(50, Math.max(5, Number(c.req.query('limit') || 20)))
  const summary = await c.env.DB.prepare(`
    SELECT COUNT(*) AS total_skills,
           SUM(CASE WHEN status = 'published' THEN 1 ELSE 0 END) AS published_skills,
           SUM(COALESCE(download_count, 0)) AS downloads,
           SUM(COALESCE(like_count, 0)) AS likes,
           SUM(COALESCE(dislike_count, 0)) AS dislikes
    FROM skills
  `).first()
  const inv = await c.env.DB.prepare(`
    SELECT COUNT(*) AS api_calls,
           SUM(CASE WHEN response_status = 'success' THEN 1 ELSE 0 END) AS success_calls,
           SUM(CASE WHEN response_status != 'success' THEN 1 ELSE 0 END) AS failed_calls,
           AVG(latency_ms) AS avg_latency_ms
    FROM skill_invocations
  `).first()
  const cte = `
    WITH skill_metrics AS (
      SELECT s.id, s.name, s.description, s.node_id, s.node_path, s.status, s.creator_name, s.package_key,
             COALESCE(s.download_count, 0) AS downloads,
             COALESCE(s.like_count, 0) AS likes,
             COALESCE(s.dislike_count, 0) AS dislikes,
             ${domainSql('s')} AS domain,
             COUNT(i.id) AS api_calls,
             SUM(CASE WHEN i.response_status = 'success' THEN 1 ELSE 0 END) AS success_calls,
             SUM(CASE WHEN i.id IS NOT NULL AND i.response_status != 'success' THEN 1 ELSE 0 END) AS failed_calls,
             AVG(i.latency_ms) AS avg_latency_ms,
             (COALESCE(s.download_count, 0) * 3 + COALESCE(s.like_count, 0) * 2 - COALESCE(s.dislike_count, 0) + COUNT(i.id) * 5 + SUM(CASE WHEN i.response_status = 'success' THEN 1 ELSE 0 END) * 2) AS hot_score
      FROM skills s
      LEFT JOIN skill_invocations i ON i.skill_id = s.id
      GROUP BY s.id
    )
  `
  const { results: domains } = await c.env.DB.prepare(`${cte}
    SELECT domain, COUNT(*) AS total_skills,
           SUM(CASE WHEN status = 'published' THEN 1 ELSE 0 END) AS published_skills,
           SUM(downloads) AS downloads, SUM(likes) AS likes, SUM(dislikes) AS dislikes,
           SUM(api_calls) AS api_calls, SUM(success_calls) AS success_calls, SUM(failed_calls) AS failed_calls,
           AVG(avg_latency_ms) AS avg_latency_ms, SUM(hot_score) AS hot_score
    FROM skill_metrics
    GROUP BY domain
    ORDER BY hot_score DESC, api_calls DESC, downloads DESC, total_skills DESC
    LIMIT ?
  `).bind(limit).all()
  const { results: topSkills } = await c.env.DB.prepare(`${cte}
    SELECT * FROM skill_metrics
    ORDER BY hot_score DESC, api_calls DESC, downloads DESC, likes DESC
    LIMIT ?
  `).bind(limit).all()
  const apiCalls = num(inv?.api_calls)
  const successCalls = num(inv?.success_calls)
  const mapDomain = (row) => {
    const calls = num(row.api_calls)
    const ok = num(row.success_calls)
    return {
      domain: row.domain || '未归类',
      totalSkills: num(row.total_skills),
      publishedSkills: num(row.published_skills),
      downloads: num(row.downloads),
      likes: num(row.likes),
      dislikes: num(row.dislikes),
      apiCalls: calls,
      successCalls: ok,
      failedCalls: num(row.failed_calls),
      successRate: calls ? round((ok / calls) * 100, 1) : 100,
      avgLatencyMs: round(row.avg_latency_ms, 0),
      hotScore: num(row.hot_score),
    }
  }
  const mapSkill = (row) => {
    const calls = num(row.api_calls)
    const ok = num(row.success_calls)
    return {
      id: row.id,
      name: row.name,
      description: row.description || '',
      domain: row.domain || '未归类',
      nodePath: row.node_path || '',
      status: row.status || 'draft',
      creatorName: row.creator_name || '',
      hasPackage: !!row.package_key,
      downloads: num(row.downloads),
      likes: num(row.likes),
      dislikes: num(row.dislikes),
      apiCalls: calls,
      successCalls: ok,
      failedCalls: num(row.failed_calls),
      successRate: calls ? round((ok / calls) * 100, 1) : 100,
      avgLatencyMs: round(row.avg_latency_ms, 0),
      hotScore: num(row.hot_score),
    }
  }
  return c.json({
    summary: {
      totalSkills: num(summary?.total_skills),
      publishedSkills: num(summary?.published_skills),
      downloads: num(summary?.downloads),
      likes: num(summary?.likes),
      dislikes: num(summary?.dislikes),
      apiCalls,
      successCalls,
      failedCalls: num(inv?.failed_calls),
      successRate: apiCalls ? round((successCalls / apiCalls) * 100, 1) : 100,
      avgLatencyMs: round(inv?.avg_latency_ms, 0),
    },
    domains: (domains || []).map(mapDomain),
    topSkills: (topSkills || []).map(mapSkill),
  })
})

app.post('/v1/skills/:skillId/invoke', async (c) => {
  const started = Date.now()
  const skillId = c.req.param('skillId')
  const token = (c.req.header('Authorization') || '').replace(/^Bearer\s+/i, '').trim()
  if (!token) return c.json({ error: 'missing_api_key', message: '请在 Authorization 头中提供 Bearer Token' }, 401)
  const keyHash = await hashHex(token)
  const key = await c.env.DB.prepare('SELECT * FROM api_keys WHERE key_hash = ? AND status = ?').bind(keyHash, 'active').first()
  if (!key) return c.json({ error: 'invalid_api_key', message: 'API Key 无效或已吊销' }, 401)
  if (key.skill_id !== skillId) return c.json({ error: 'api_key_skill_mismatch', message: 'API Key 不属于当前 Skill' }, 403)
  const todayStart = Math.floor(new Date().setHours(0, 0, 0, 0) / 1000)
  const usage = await c.env.DB.prepare('SELECT COUNT(*) AS n FROM skill_invocations WHERE api_key_id = ? AND ts >= ?').bind(key.id, todayStart).first()
  if (num(usage?.n) >= num(key.daily_limit || 1000)) {
    return c.json({ error: 'rate_limit_exceeded', message: `日调用上限 ${key.daily_limit} 次已达到`, limit: key.daily_limit, used: usage?.n || 0 }, 429)
  }
  const skill = await c.env.DB.prepare('SELECT * FROM skills WHERE id = ?').bind(skillId).first()
  if (!skill || skill.status !== 'published') return c.json({ error: 'skill_not_available', message: 'Skill 不存在或未上线' }, 404)
  const body = await c.req.json().catch(() => ({}))
  const runtime = await activeAgentRun(c)
  const invArgs = {
    skill_id: skillId,
    skill_name: skill.name,
    api_key_id: key.id,
    caller_ip: c.req.header('CF-Connecting-IP') || '',
    request_input: textFromInput(body).slice(0, 2000),
  }
  try {
    const systemPrompt = await readSkillPackageText(c, skill)
    const output = await callAgentRun(runtime, [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: `请严格按照当前 Skill 执行下面的用户输入，并只输出最终可交付结果。\n\n## 用户输入\n${textFromInput(body)}` },
    ])
    await c.env.DB.prepare(`
      INSERT INTO skill_invocations (skill_id, skill_name, api_key_id, caller_ip, request_input, response_output, response_status, latency_ms)
      VALUES (?, ?, ?, ?, ?, ?, 'success', ?)
    `).bind(invArgs.skill_id, invArgs.skill_name, invArgs.api_key_id, invArgs.caller_ip, invArgs.request_input, String(output || '').slice(0, 4000), Date.now() - started).run()
    return c.json({ skill_id: skillId, output, latency_ms: Date.now() - started })
  } catch (e) {
    await c.env.DB.prepare(`
      INSERT INTO skill_invocations (skill_id, skill_name, api_key_id, caller_ip, request_input, response_status, latency_ms, error_message)
      VALUES (?, ?, ?, ?, ?, 'error', ?, ?)
    `).bind(invArgs.skill_id, invArgs.skill_name, invArgs.api_key_id, invArgs.caller_ip, invArgs.request_input, Date.now() - started, e?.message || String(e)).run()
    return c.json({ error: 'invoke_failed', message: e?.message || String(e) }, 500)
  }
})

app.get('/api/download/source', async (c) => {
  const admin = requireAdmin(c)
  if (admin) return admin
  return c.json({ error: 'source_zip_external', message: 'Cloudflare Lite 源码包由课程目录中的 ZIP 交付，不在 Worker 内动态生成。' }, 410)
})

app.notFound((c) => c.json({ error: 'not_found', path: c.req.path }, 404))
app.onError((err, c) => {
  console.error('[cloudflare-lite]', err)
  return c.json({ error: 'internal_error', message: err?.message || '服务器内部错误' }, 500)
})

export default app
