Bläddra i källkod

feat(v0.6.0): personal dashboard + admin web console + personal HTML

- AdminDashboard → PersonalDashboard (个人用量: 概览/趋势/明细)
- admin.html — Admin 管理台(团队/模型/用量/导出)
- personal.html — 个人用量独立页面
- admin-server.ts — 全量 API (auth/users/teams/models/usage)
- Sidebar: shield → user icon, '团队管理' → '个人中心'
KirinCode 1 månad sedan
förälder
incheckning
c16996e73e
38 ändrade filer med 641 tillägg och 381 borttagningar
  1. 1 1
      packages/app/package.json
  2. 2 2
      packages/app/src/app.tsx
  3. 66 219
      packages/app/src/pages/admin/AdminDashboard.tsx
  4. 1 1
      packages/app/src/pages/layout.tsx
  5. 1 1
      packages/app/src/pages/layout/sidebar-shell.tsx
  6. 1 1
      packages/cli/package.json
  7. 1 1
      packages/codemode/package.json
  8. 1 1
      packages/console/app/package.json
  9. 1 1
      packages/console/core/package.json
  10. 1 1
      packages/console/function/package.json
  11. 1 1
      packages/console/mail/package.json
  12. 1 1
      packages/console/support/package.json
  13. 1 1
      packages/core/package.json
  14. 206 0
      packages/desktop/admin.html
  15. 1 1
      packages/desktop/package.json
  16. 184 0
      packages/desktop/personal.html
  17. 150 127
      packages/desktop/src/main/admin-server.ts
  18. 1 1
      packages/effect-drizzle-sqlite/package.json
  19. 1 1
      packages/effect-sqlite-node/package.json
  20. 1 1
      packages/enterprise/package.json
  21. 1 1
      packages/function/package.json
  22. 1 1
      packages/http-recorder/package.json
  23. 1 1
      packages/kirincode-cloud/package.json
  24. 1 1
      packages/kirincode-workflow/package.json
  25. 1 1
      packages/kirincode/package.json
  26. 1 1
      packages/llm/package.json
  27. 1 1
      packages/mobile/package.json
  28. 1 1
      packages/plugin/package.json
  29. 1 1
      packages/sdk/js/package.json
  30. 1 1
      packages/server/package.json
  31. 1 1
      packages/session-ui/package.json
  32. 1 1
      packages/slack/package.json
  33. 1 1
      packages/stats/app/package.json
  34. 1 1
      packages/stats/core/package.json
  35. 1 1
      packages/stats/server/package.json
  36. 1 1
      packages/tui/package.json
  37. 1 1
      packages/ui/package.json
  38. 1 1
      packages/web/package.json

+ 1 - 1
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/app",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "description": "",
   "type": "module",
   "exports": {

+ 2 - 2
packages/app/src/app.tsx

@@ -58,7 +58,7 @@ import { createSessionLineage } from "@/pages/session/session-lineage"
 
 import { SessionPage, SessionRouteErrorBoundary, TargetSessionRouteContent } from "@/pages/session"
 import { NewHome, LegacyHome } from "@/pages/home"
-import { AdminDashboard } from "@/pages/admin/AdminDashboard"
+import { PersonalDashboard } from "@/pages/admin/AdminDashboard"
 
 const NewSession = lazy(() => import("@/pages/new-session"))
 
@@ -609,7 +609,7 @@ function Routes(props: { serverScoped?: JSX.Element }) {
         <Route path="/server/:serverKey/session/:id" component={TargetSessionRoute} />
       </Show>
       <Route path="/new-session" component={DraftRoute} />
-      <Route path="/admin" component={AdminDashboard} />
+      <Route path="/admin" component={PersonalDashboard} />
     </>
   )
 }

+ 66 - 219
packages/app/src/pages/admin/AdminDashboard.tsx

@@ -1,45 +1,27 @@
-import { createSignal, createResource, onMount, For, Show, createEffect, createMemo } from "solid-js"
+import { createSignal, createResource, onMount, Show, For } from "solid-js"
 
 const API_BASE = "http://localhost:4097/api/v1"
-const LS_TOKEN_KEY = "kirincode_admin_token"
+const LS_KEY = "kirincode_personal_token"
 
-// Simple fetch wrapper
 async function api(path: string, method = "GET", body?: any): Promise<any> {
-  const token = localStorage.getItem(LS_TOKEN_KEY)
+  const token = localStorage.getItem(LS_KEY)
   const headers: Record<string, string> = { "Content-Type": "application/json" }
   if (token) headers["Authorization"] = `Bearer ${token}`
   const res = await fetch(`${API_BASE}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined })
   return res.json()
 }
 
-type Tab = "summary" | "trend" | "users" | "detail" | "teams"
-type TimeRange = "24h" | "7d" | "30d" | "all"
-
-function timeRange(r: TimeRange): { from: string; to: string } {
-  const now = new Date()
-  const from = new Date(now)
-  if (r === "24h") from.setHours(from.getHours() - 24)
-  else if (r === "7d") from.setDate(from.getDate() - 7)
-  else if (r === "30d") from.setDate(from.getDate() - 30)
-  else from.setFullYear(2000)
-  return { from: from.toISOString(), to: now.toISOString() }
-}
-
-export function AdminDashboard() {
-  const [token, setToken] = createSignal(localStorage.getItem(LS_TOKEN_KEY) || "")
+export function PersonalDashboard() {
+  const [token, setToken] = createSignal(localStorage.getItem(LS_KEY) || "")
   const [loggedIn, setLoggedIn] = createSignal(!!token())
   const [email, setEmail] = createSignal("")
   const [password, setPassword] = createSignal("")
   const [displayName, setDisplayName] = createSignal("")
   const [isRegister, setIsRegister] = createSignal(false)
-  const [tab, setTab] = createSignal<Tab>("summary")
-  const [timeRange, setTimeRange] = createSignal<TimeRange>("24h")
-  const [userFilter, setUserFilter] = createSignal("")
-  const [modelFilter, setModelFilter] = createSignal("")
-  const [detailPage, setDetailPage] = createSignal(1)
-  const [teamName, setTeamName] = createSignal("")
-  const [inviteEmail, setInviteEmail] = createSignal("")
   const [error, setError] = createSignal("")
+  const [user, setUser] = createSignal<any>(null)
+  const [tab, setTab] = createSignal<"summary" | "trend" | "detail">("summary")
+  const [detailPage, setDetailPage] = createSignal(1)
 
   async function doAuth() {
     setError("")
@@ -48,286 +30,151 @@ export function AdminDashboard() {
     try {
       const data = await api(endpoint, "POST", body)
       if (data.error) { setError(data.error); return }
-      localStorage.setItem(LS_TOKEN_KEY, data.token)
+      localStorage.setItem(LS_KEY, data.token)
       setToken(data.token)
+      setUser(data.user)
       setLoggedIn(true)
-    } catch { setError("Connection failed — start admin server on port 4097") }
+    } catch { setError("连接失败 — 请确认 Admin Server 已启动 (port 4097)") }
   }
 
   function logout() {
-    localStorage.removeItem(LS_TOKEN_KEY)
-    setToken(""); setLoggedIn(false)
+    localStorage.removeItem(LS_KEY)
+    setToken(""); setLoggedIn(false); setUser(null)
   }
 
-  // Data fetching
-  const tr = createMemo(() => timeRange())
-  const fromTo = createMemo(() => timeRange(tr()))
-  const fetcher = (path: string) => api(path)
+  const [summary] = createResource(loggedIn, () => api("/personal/summary"))
+  const [trend] = createResource(() => loggedIn() && tab() === "trend", () => api("/personal/trend"))
+  const [details] = createResource(() => loggedIn() && tab() === "detail", () => api(`/personal/detail?page=${detailPage()}`))
 
-  const [summary] = createResource(loggedIn, () => api(`/admin/usage/summary?from=${fromTo().from}&to=${fromTo().to}`))
-  const [trend] = createResource(() => loggedIn() && tab() === "trend", () => api(`/admin/usage/trend`))
-  const [users] = createResource(() => loggedIn() && (tab() === "users" || tab() === "summary"), () => {
-    const p: string[] = []
-    if (userFilter()) p.push(`user_id=${userFilter()}`)
-    if (modelFilter()) p.push(`model=${modelFilter()}`)
-    return api(`/admin/usage/users?from=${fromTo().from}&to=${fromTo().to}${p.length ? "&" + p.join("&") : ""}`)
-  })
-  const [details] = createResource(() => loggedIn() && tab() === "detail", () => {
-    const p: string[] = [`page=${detailPage()}`]
-    if (userFilter()) p.push(`user_id=${userFilter()}`)
-    if (modelFilter()) p.push(`model=${modelFilter()}`)
-    return api(`/admin/usage/detail?from=${fromTo().from}&to=${fromTo().to}&${p.join("&")}`)
-  })
-  const [teams] = createResource(() => loggedIn() && tab() === "teams", () => api("/admin/teams"))
-
-  // Auto-refresh
   onMount(() => {
-    const i = setInterval(() => {
-      if (loggedIn()) {
-        api(`/admin/usage/summary?from=${fromTo().from}&to=${fromTo().to}`)
-      }
-    }, 30000)
+    const i = setInterval(() => { if (loggedIn() && tab() === "summary") api("/personal/summary") }, 30000)
     return () => clearInterval(i)
   })
 
   if (!loggedIn()) {
     return (
       <div style={styles.loginPage}>
-        <h1 style={styles.loginTitle}>🛡 KirinCode 管理</h1>
-        <div style={{ display: "flex", "flex-direction": "column", gap: "12px", width: "320px" }}>
-          <input type="email" placeholder="邮箱" value={email()} onInput={e => setEmail(e.currentTarget.value)} style={styles.input} />
+        <h1 style={styles.title}>KirinCode 个人中心</h1>
+        <div style={{ display: "flex", "flex-direction": "column", gap: "12px", width: "340px" }}>
+          <input type="email" placeholder="企业邮箱" value={email()} onInput={e => setEmail(e.currentTarget.value)} style={styles.input} />
           <input type="password" placeholder="密码" value={password()} onInput={e => setPassword(e.currentTarget.value)} style={styles.input} onKeyDown={e => e.key === "Enter" && doAuth()} />
           <Show when={isRegister()}>
             <input type="text" placeholder="显示名称" value={displayName()} onInput={e => setDisplayName(e.currentTarget.value)} style={styles.input} />
           </Show>
-          <button onClick={doAuth} style={styles.btn}>{isRegister() ? "注册" : "登录"}</button>
-          <div style={{ color: "#e74c3c", "font-size": "13px", "text-align": "center" }}>{error()}</div>
-          <a onClick={() => { setIsRegister(!isRegister()); setError("") }} style={{ color: "#7f8c8d", "text-align": "center", cursor: "pointer", "font-size": "13px" }}>
+          <button onClick={doAuth} style={styles.btn}>{isRegister() ? "注 册" : "登 录"}</button>
+          <Show when={error()}><div style={styles.error}>{error()}</div></Show>
+          <button onClick={() => { setIsRegister(!isRegister()); setError("") }} style={{ ...styles.btn, background: "transparent", border: "1px solid #2c3e50", color: "#7f8c8d" }}>
             {isRegister() ? "已有账号?登录" : "没有账号?注册"}
-          </a>
+          </button>
         </div>
       </div>
     )
   }
 
-  const tabs: { key: Tab; label: string }[] = [
-    { key: "summary", label: "今日概览" },
-    { key: "trend", label: "趋势图表" },
-    { key: "users", label: "用户排行" },
-    { key: "detail", label: "分钟明细" },
-    { key: "teams", label: "团队管理" },
+  const tabs = [
+    { key: "summary" as const, label: "今日概览" },
+    { key: "trend" as const, label: "趋势图表" },
+    { key: "detail" as const, label: "用量明细" },
   ]
 
   return (
     <div style={styles.page}>
-      {/* Header */}
       <div style={styles.header}>
-        <span style={styles.headerTitle}>🛡 KirinCode 管理</span>
-        <div style={{ display: "flex", gap: "8px" }}>
+        <span style={styles.headerTitle}>👤 {user()?.display_name || user()?.email || "个人中心"}</span>
+        <div style={{ display: "flex", gap: "4px" }}>
           {tabs.map(t => (
             <button onClick={() => setTab(t.key)} style={{ ...styles.tabBtn, background: tab() === t.key ? "#e74c3c" : "transparent", color: tab() === t.key ? "#fff" : "#7f8c8d" }}>
               {t.label}
             </button>
           ))}
         </div>
-        <button onClick={logout} style={styles.logoutBtn}>退出</button>
+        <div style={{ "margin-left": "auto", display: "flex", gap: "8px", "align-items": "center" }}>
+          <a href="http://localhost:4097/personal.html" target="_blank" style={{ color: "#7f8c8d", "font-size": "12px", "text-decoration": "none" }}>📄 独立页面</a>
+          <button onClick={logout} style={styles.logoutBtn}>退出</button>
+        </div>
       </div>
-
-      {/* Content */}
       <div style={styles.content}>
-        {/* Summary / Overview */}
         <Show when={tab() === "summary"}>
-          <h2 style={styles.sectionTitle}>Token 消耗概览</h2>
+          <h2 style={styles.sectionTitle}>今日Token消耗</h2>
           <div style={styles.cards}>
-            <Card label=" Token" value={((summary()?.input || 0) + (summary()?.output || 0)).toLocaleString()} />
+            <Card label="今日 Token" value={((summary()?.input || 0) + (summary()?.output || 0)).toLocaleString()} />
             <Card label="请求次数" value={(summary()?.requests || 0).toLocaleString()} />
             <Card label="今日费用" value={`$${(summary()?.cost || 0).toFixed(4)}`} />
-            <Card label="活跃用户" value={String(summary()?.users || 0)} />
           </div>
-
-          {/* Mini trend chart preview */}
-          <Show when={users()}>
-            <h3 style={{ ...styles.sectionTitle, "margin-top": "24px" }}>用户用量排行</h3>
-            <UserTable data={(users() || []).slice(0, 5)} />
-          </Show>
         </Show>
-
-        {/* Trend Chart */}
         <Show when={tab() === "trend"}>
-          <h2 style={styles.sectionTitle}>分钟级 Token 消耗趋势</h2>
+          <h2 style={styles.sectionTitle}>分钟级消耗趋势(近24小时)</h2>
           <div style={styles.chart}>
-            <Show when={trend() && (trend() as any[]).length > 0} fallback={<div style={{ color: "#7f8c8d", padding: "40px" }}>暂无数据</div>}>
-              <TrendChart data={trend() || []} />
-            </Show>
-          </div>
-        </Show>
-
-        {/* Users */}
-        <Show when={tab() === "users"}>
-          <div style={{ display: "flex", "justify-content": "space-between", "align-items": "center" }}>
-            <h2 style={styles.sectionTitle}>用户用量排行</h2>
-            <select value={timeRange()} onInput={e => setTimeRange(e.currentTarget.value as TimeRange)} style={styles.select}>
-              <option value="24h">最近24小时</option><option value="7d">最近7天</option><option value="30d">最近30天</option><option value="all">全部</option>
-            </select>
+            <TrendChart data={trend() || []} />
           </div>
-          <UserTable data={users() || []} />
         </Show>
-
-        {/* Detail */}
         <Show when={tab() === "detail"}>
-          <div style={{ display: "flex", "justify-content": "space-between", "align-items": "center", "flex-wrap": "wrap", gap: "12px" }}>
-            <h2 style={styles.sectionTitle}>分钟级明细</h2>
-            <div style={{ display: "flex", gap: "8px" }}>
-              <input type="text" placeholder="筛选用户ID" value={userFilter()} onInput={e => setUserFilter(e.currentTarget.value)} style={styles.filterInput} />
-              <input type="text" placeholder="筛选模型" value={modelFilter()} onInput={e => setModelFilter(e.currentTarget.value)} style={styles.filterInput} />
-              <select value={timeRange()} onInput={e => setTimeRange(e.currentTarget.value as TimeRange)} style={styles.select}>
-                <option value="24h">24h</option><option value="7d">7天</option><option value="30d">30天</option><option value="all">全部</option>
-              </select>
-            </div>
-          </div>
+          <h2 style={styles.sectionTitle}>用量明细</h2>
           <DetailTable data={details()?.data || []} />
           <Show when={details()}>
             <div style={{ display: "flex", "justify-content": "center", gap: "8px", "margin-top": "16px" }}>
               <button disabled={detailPage() <= 1} onClick={() => setDetailPage(p => p - 1)} style={{ ...styles.tabBtn, opacity: detailPage() <= 1 ? 0.5 : 1 }}>上一页</button>
-              <span style={{ color: "#7f8c8d", padding: "6px" }}>第 {details()?.page}/{details()?.pages} 页 (共 {details()?.total} 条)</span>
+              <span style={{ color: "#7f8c8d", padding: "6px" }}>第 {details()?.page}/{details()?.pages} 页</span>
               <button disabled={detailPage() >= (details()?.pages || 1)} onClick={() => setDetailPage(p => p + 1)} style={{ ...styles.tabBtn, opacity: detailPage() >= (details()?.pages || 1) ? 0.5 : 1 }}>下一页</button>
             </div>
           </Show>
         </Show>
-
-        {/* Teams */}
-        <Show when={tab() === "teams"}>
-          <h2 style={styles.sectionTitle}>团队管理</h2>
-          <div style={{ display: "flex", gap: "12px", "margin-bottom": "16px" }}>
-            <input type="text" placeholder="团队名称" value={teamName()} onInput={e => setTeamName(e.currentTarget.value)} style={styles.input} />
-            <button onClick={async () => { await api("/admin/teams", "POST", { name: teamName() }); setTeamName("") }} style={{ ...styles.btn, width: "auto", padding: "8px 16px" }}>创建团队</button>
-          </div>
-          <div style={{ display: "flex", gap: "12px", "margin-bottom": "16px" }}>
-            <input type="email" placeholder="成员邮箱" value={inviteEmail()} onInput={e => setInviteEmail(e.currentTarget.value)} style={styles.input} />
-            <button onClick={async () => { await api("/admin/teams/invite", "POST", { email: inviteEmail() }); setInviteEmail("") }} style={{ ...styles.btn, width: "auto", padding: "8px 16px", background: "#27ae60" }}>邀请成员</button>
-          </div>
-          <div style={{ display: "grid", gap: "12px" }}>
-            <For each={(teams() as any[]) || []}>{(team: any) => (
-              <div style={{ background: "#1a1a2e", padding: "16px", "border-radius": "10px", border: "1px solid #2c3e50", display: "flex", "justify-content": "space-between", "align-items": "center" }}>
-                <div><span style={{ "font-weight": "600" }}>{team.name}</span><span style={{ color: "#7f8c8d", "margin-left": "12px", "font-size": "12px" }}>ID: {team.id?.slice(0, 8)}</span></div>
-              </div>
-            )}</For>
-          </div>
-        </Show>
       </div>
     </div>
   )
 }
 
-// Sub-components
-function Card(props: { label: string; value: string }) {
-  return (
-    <div style={{ background: "#1a1a2e", padding: "20px", "border-radius": "12px", border: "1px solid #2c3e50", "min-width": "160px" }}>
-      <div style={{ color: "#7f8c8d", "font-size": "12px", "margin-bottom": "8px" }}>{props.label}</div>
-      <div style={{ "font-size": "22px", "font-weight": "700", color: "#f39c12" }}>{props.value}</div>
-    </div>
-  )
-}
-
-function UserTable(props: { data: any[] }) {
-  return (
-    <table style={{ width: "100%", "border-collapse": "collapse" }}>
-      <thead>
-        <tr style={{ color: "#7f8c8d", "font-size": "11px", "text-align": "left" }}>
-          <th style={{ padding: "10px 8px" }}>用户</th><th style={{ padding: "10px 8px" }}>邮箱</th><th style={{ padding: "10px 8px", "text-align": "right" }}>Token(入)</th><th style={{ padding: "10px 8px", "text-align": "right" }}>Token(出)</th><th style={{ padding: "10px 8px", "text-align": "right" }}>请求</th><th style={{ padding: "10px 8px", "text-align": "right" }}>费用</th>
-        </tr>
-      </thead>
-      <For each={props.data}>{(row: any) => (
-        <tr style={{ "border-top": "1px solid #1e293b", "font-size": "13px" }}>
-          <td style={{ padding: "10px 8px" }}>{row.display_name || row.user_id?.slice(0, 8)}</td>
-          <td style={{ padding: "10px 8px", color: "#7f8c8d" }}>{row.email}</td>
-          <td style={{ padding: "10px 8px", "text-align": "right" }}>{(row.input || row.input_tokens || 0).toLocaleString()}</td>
-          <td style={{ padding: "10px 8px", "text-align": "right" }}>{(row.output || row.output_tokens || 0).toLocaleString()}</td>
-          <td style={{ padding: "10px 8px", "text-align": "right" }}>{row.requests || 0}</td>
-          <td style={{ padding: "10px 8px", "text-align": "right", color: "#f39c12" }}>${(row.cost || row.cost_usd || 0).toFixed(4)}</td>
-        </tr>
-      )}</For>
-    </table>
-  )
+function Card(p: { label: string; value: string }) {
+  return <div style={styles.card}><div style={styles.cardLabel}>{p.label}</div><div style={styles.cardValue}>{p.value}</div></div>
 }
 
-function DetailTable(props: { data: any[] }) {
+function DetailTable(p: { data: any[] }) {
   return (
     <table style={{ width: "100%", "border-collapse": "collapse" }}>
-      <thead>
-        <tr style={{ color: "#7f8c8d", "font-size": "11px", "text-align": "left" }}>
-          <th style={{ padding: "8px" }}>时间</th><th style={{ padding: "8px" }}>用户</th><th style={{ padding: "8px" }}>模型</th><th style={{ padding: "8px", "text-align": "right" }}>输入</th><th style={{ padding: "8px", "text-align": "right" }}>输出</th><th style={{ padding: "8px", "text-align": "right" }}>费用</th>
-        </tr>
-      </thead>
-      <For each={props.data}>{(row: any) => (
+      <thead><tr style={{ color: "#7f8c8d", "font-size": "11px", "text-align": "left" }}><th style={{ padding: "10px 8px" }}>时间</th><th style={{ padding: "10px 8px" }}>模型</th><th style={{ padding: "10px 8px", "text-align": "right" }}>Token(入)</th><th style={{ padding: "10px 8px", "text-align": "right" }}>Token(出)</th><th style={{ padding: "10px 8px", "text-align": "right" }}>费用</th></tr></thead>
+      <For each={p.data}>{(row: any) => (
         <tr style={{ "border-top": "1px solid #1e293b", "font-size": "12px" }}>
           <td style={{ padding: "8px", "font-size": "11px", color: "#7f8c8d" }}>{new Date(row.created_at).toLocaleString()}</td>
-          <td style={{ padding: "8px" }}>{row.display_name || row.user_id?.slice(0, 8)}</td>
           <td style={{ padding: "8px" }}>{row.provider}/{row.model}</td>
-          <td style={{ padding: "8px", "text-align": "right" }}>{(row.input_tokens || 0).toLocaleString()}</td>
-          <td style={{ padding: "8px", "text-align": "right" }}>{(row.output_tokens || 0).toLocaleString()}</td>
-          <td style={{ padding: "8px", "text-align": "right", color: "#f39c12" }}>${(row.cost_usd || 0).toFixed(5)}</td>
+          <td style={{ padding: "8px", "text-align": "right" }}>{row.input_tokens?.toLocaleString()}</td>
+          <td style={{ padding: "8px", "text-align": "right" }}>{row.output_tokens?.toLocaleString()}</td>
+          <td style={{ padding: "8px", "text-align": "right", color: "#f39c12" }}>${row.cost_usd?.toFixed(5)}</td>
         </tr>
       )}</For>
     </table>
   )
 }
 
-function TrendChart(props: { data: any[] }) {
-  if (props.data.length < 2) return <div style={{ color: "#7f8c8d", "text-align": "center", padding: "40px" }}>数据不足(需要至少2个数据点)</div>
-
-  const maxVal = Math.max(...props.data.map((d: any) => d.input + d.output), 1)
-  const h = 200
-  const w = 700
-  const points = props.data.map((d: any, i: number) => {
-    const x = (i / (props.data.length - 1)) * w
-    const y = h - ((d.input + d.output) / maxVal) * h
-    return `${x},${y}`
-  }).join(" ")
-
-  // Grid lines
-  const gridLines = []
-  for (let i = 0; i <= 4; i++) {
-    const y = (i / 4) * h
-    gridLines.push(<line x1={0} y1={y} x2={w} y2={y} stroke="#1e293b" stroke-width="0.5" /> as any)
-    const label = Math.round(maxVal * (1 - i / 4))
-    gridLines.push(<text x={4} y={y - 4} fill="#7f8c8d" font-size="10">{label.toLocaleString()}</text> as any)
-  }
-
+function TrendChart(p: { data: any[] }) {
+  if (p.data.length < 2) return <div style={{ color: "#7f8c8d", "text-align": "center", padding: "40px" }}>数据不足</div>
+  const max = Math.max(...p.data.map((d: any) => d.input + d.output), 1)
+  const w = 700; const h = 200
+  const pts = p.data.map((d: any, i: number) => `${(i / (p.data.length - 1)) * w},${h - ((d.input + d.output) / max) * h}`).join(" ")
   return (
     <svg viewBox={`0 0 ${w} ${h + 24}`} style={{ width: "100%", height: "auto" }}>
-      {gridLines}
-      <polyline points={points} fill="none" stroke="#e74c3c" stroke-width="2" />
-      <polygon points={`0,${h} ${points} ${w},${h}`} fill="url(#grad)" opacity="0.2" />
-      <defs>
-        <linearGradient id="grad" x1="0" y1="0" x2="0" y2="1">
-          <stop offset="0%" stop-color="#e74c3c" />
-          <stop offset="100%" stop-color="#e74c3c" stop-opacity="0" />
-        </linearGradient>
-      </defs>
-      {/* Time labels */}
-      {props.data.filter((_: any, i: number) => i % Math.max(1, Math.floor(props.data.length / 6)) === 0).map((d: any, i: number) => {
-        const x = (i * Math.max(1, Math.floor(props.data.length / 6)) / Math.max(1, props.data.length - 1)) * w
-        return <text x={x} y={h + 20} fill="#7f8c8d" font-size="10" text-anchor="middle">{new Date(d.minute).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</text>
-      })}
+      <polyline points={pts} fill="none" stroke="#e74c3c" stroke-width="2" />
+      <polygon points={`0,${h} ${pts} ${w},${h}`} fill="url(#g)" opacity="0.15" />
+      <defs><linearGradient id="g"><stop offset="0%" stop-color="#e74c3c" /><stop offset="100%" stop-color="#e74c3c" stop-opacity="0" /></linearGradient></defs>
     </svg>
   )
 }
 
 const styles: Record<string, any> = {
   loginPage: { display: "flex", "flex-direction": "column", "align-items": "center", "justify-content": "center", height: "100vh", background: "#0f172a", color: "#ecf0f1", "font-family": "system-ui, sans-serif" },
-  loginTitle: { color: "#e74c3c", "margin-bottom": "24px" },
-  input: { padding: "10px", "border-radius": "8px", border: "1px solid #2c3e50", background: "#1a1a2e", color: "#ecf0f1", "font-size": "14px", width: "100%", "box-sizing": "border-box" },
+  title: { color: "#e74c3c", "margin-bottom": "24px" },
+  input: { padding: "10px 14px", "border-radius": "8px", border: "1px solid #2c3e50", background: "#1a1a2e", color: "#ecf0f1", "font-size": "14px" },
   btn: { padding: "10px", "border-radius": "8px", border: "none", background: "#e74c3c", color: "#fff", "font-size": "14px", cursor: "pointer", width: "100%" },
+  error: { color: "#e74c3c", "font-size": "13px", "text-align": "center" },
   page: { display: "flex", "flex-direction": "column", height: "100vh", background: "#0f172a", color: "#ecf0f1", "font-family": "system-ui, sans-serif", overflow: "auto" },
   header: { display: "flex", "align-items": "center", padding: "12px 24px", background: "#1a1a2e", "border-bottom": "1px solid #2c3e50", gap: "16px" },
-  headerTitle: { "font-size": "18px", "font-weight": "700", color: "#e74c3c", "min-width": "180px" },
+  headerTitle: { "font-size": "18px", "font-weight": "700", color: "#e74c3c" },
   tabBtn: { padding: "6px 14px", "border-radius": "6px", border: "none", "font-size": "13px", cursor: "pointer", whiteSpace: "nowrap" },
-  logoutBtn: { "margin-left": "auto", padding: "6px 14px", "border-radius": "6px", border: "1px solid #2c3e50", background: "transparent", color: "#7f8c8d", cursor: "pointer", "font-size": "12px" },
+  logoutBtn: { padding: "6px 14px", "border-radius": "6px", border: "1px solid #2c3e50", background: "transparent", color: "#7f8c8d", cursor: "pointer", "font-size": "12px" },
   content: { flex: 1, padding: "24px", overflow: "auto" },
   sectionTitle: { "font-size": "16px", "font-weight": "600", "margin-bottom": "16px" },
-  cards: { display: "grid", "grid-template-columns": "repeat(auto-fit, minmax(160px, 1fr))", gap: "16px" },
-  chart: { background: "#1a1a2e", padding: "16px", "border-radius": "10px", border: "1px solid #2c3e50", overflow: "auto" },
-  select: { padding: "6px 10px", "border-radius": "6px", border: "1px solid #2c3e50", background: "#1a1a2e", color: "#ecf0f1", "font-size": "12px", cursor: "pointer" },
-  filterInput: { padding: "6px 10px", "border-radius": "6px", border: "1px solid #2c3e50", background: "#1a1a2e", color: "#ecf0f1", "font-size": "12px", width: "140px" },
+  cards: { display: "grid", "grid-template-columns": "repeat(auto-fit, minmax(160px, 1fr))", gap: "16px", "margin-bottom": "24px" },
+  card: { background: "#1a1a2e", padding: "20px", "border-radius": "12px", border: "1px solid #2c3e50" },
+  cardLabel: { color: "#7f8c8d", "font-size": "12px", "margin-bottom": "8px" },
+  cardValue: { "font-size": "22px", "font-weight": "700", color: "#f39c12" },
+  chart: { background: "#1a1a2e", padding: "16px", "border-radius": "10px", border: "1px solid #2c3e50" },
 }

+ 1 - 1
packages/app/src/pages/layout.tsx

@@ -2241,7 +2241,7 @@ export default function LegacyLayout(props: ParentProps) {
       onOpenSettings={openSettings}
       helpLabel={() => language.t("sidebar.help")}
       onOpenHelp={() => platform.openLink("https://kailin.com.cn/feedback")}
-      adminLabel={() => "团队管理"}
+      adminLabel={() => "个人中心"}
       onOpenAdmin={openAdmin}
       renderPanel={() =>
         mobile ? <SidebarPanel project={currentProject} mobile /> : <SidebarPanel project={currentProject} merged />

+ 1 - 1
packages/app/src/pages/layout/sidebar-shell.tsx

@@ -94,7 +94,7 @@ export const SidebarContent = (props: {
         <div class="shrink-0 w-full pt-3 pb-6 flex flex-col items-center gap-2">
           <Tooltip placement={placement()} value={props.adminLabel()}>
             <IconButton
-              icon="shield-check"
+              icon="user"
               variant="ghost"
               size="large"
               onClick={props.onOpenAdmin}

+ 1 - 1
packages/cli/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/cli",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "type": "module",
   "license": "MIT",
   "bin": {

+ 1 - 1
packages/codemode/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/codemode",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "description": "Effect-native confined code execution over schema-described tools",
   "private": true,
   "type": "module",

+ 1 - 1
packages/console/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/console-app",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/console/core/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/console-core",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/console/function/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/console-function",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "type": "module",

+ 1 - 1
packages/console/mail/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/console-mail",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "dependencies": {
     "@jsx-email/all": "2.2.3",
     "@jsx-email/cli": "1.4.3",

+ 1 - 1
packages/console/support/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/console-support",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "name": "@kirincode-ai/core",
   "type": "module",
   "license": "MIT",

+ 206 - 0
packages/desktop/admin.html

@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
+<title>KirinCode Admin</title>
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:system-ui,sans-serif;background:#0f172a;color:#ecf0f1;min-height:100vh}
+header{background:#1a1a2e;padding:12px 24px;border-bottom:1px solid #2c3e50;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
+h1{font-size:18px;color:#e74c3c;margin-right:24px}
+.tabs{display:flex;gap:4px;flex-wrap:wrap}
+.tab{padding:6px 14px;border-radius:6px;border:none;font-size:13px;cursor:pointer;background:transparent;color:#7f8c8d}
+.tab.active{background:#e74c3c;color:#fff}
+main{padding:24px;max-width:1200px;margin:0 auto}
+table{width:100%;border-collapse:collapse;margin-top:12px}
+th{color:#7f8c8d;font-size:11px;text-align:left;padding:10px 8px;border-bottom:1px solid #2c3e50}
+td{padding:10px 8px;font-size:13px;border-bottom:1px solid #1e293b}
+tr:hover{background:#1a1a2e22}
+.input{padding:8px 12px;border-radius:6px;border:1px solid #2c3e50;background:#1a1a2e;color:#ecf0f1;font-size:13px}
+.select{padding:6px 10px;border-radius:6px;border:1px solid #2c3e50;background:#1a1a2e;color:#ecf0f1;font-size:12px;cursor:pointer}
+.btn{padding:8px 16px;border-radius:6px;border:none;background:#e74c3c;color:#fff;font-size:13px;cursor:pointer}
+.btn:hover{opacity:.9}.btn-sm{padding:5px 10px;font-size:12px}
+.btn-green{background:#27ae60}.btn-orange{background:#e67e22}.btn-red{background:#c0392b}
+.login-page{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh}
+.login-box{width:340px;display:flex;flex-direction:column;gap:12px}
+.logout-btn{margin-left:auto;padding:6px 14px;border-radius:6px;border:1px solid #2c3e50;background:transparent;color:#7f8c8d;cursor:pointer;font-size:12px}
+.error{color:#e74c3c;font-size:13px;text-align:center}
+.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:16px;margin-bottom:24px}
+.card{background:#1a1a2e;padding:20px;border-radius:12px;border:1px solid #2c3e50}
+.card-label{color:#7f8c8d;font-size:12px;margin-bottom:8px}
+.card-value{font-size:22px;font-weight:700;color:#f39c12}
+.pagination{display:flex;justify-content:center;gap:8px;margin-top:16px;align-items:center}
+.pagination button{padding:6px 14px;border-radius:6px;border:none;cursor:pointer;font-size:13px;background:transparent;color:#7f8c8d}
+.pagination button:disabled{opacity:.5}
+.flex-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px}
+.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:100}
+.modal{background:#1a1a2e;padding:24px;border-radius:12px;border:1px solid #2c3e50;width:400px;max-width:90vw;display:flex;flex-direction:column;gap:12px}
+</style>
+</head>
+<body>
+<div id="login" style="display:none">
+  <div class="login-page">
+    <div class="login-box">
+      <h1 style="color:#e74c3c;text-align:center">🛡 KirinCode Admin</h1>
+      <input type="email" id="lemail" class="input" placeholder="管理员邮箱">
+      <input type="password" id="lpass" class="input" placeholder="密码">
+      <button class="btn" onclick="doLogin()">登 录</button>
+      <div class="error" id="lerror"></div>
+    </div>
+  </div>
+</div>
+
+<div id="app" style="display:none">
+  <header>
+    <h1>🛡 KirinCode Admin</h1>
+    <div class="tabs">
+      <button class="tab active" data-tab="teams">团队管理</button>
+      <button class="tab" data-tab="models">模型配置</button>
+      <button class="tab" data-tab="usage">用量统计</button>
+    </div>
+    <button class="logout-btn" onclick="logout()">退出</button>
+  </header>
+  <main id="content"></main>
+</div>
+
+<script>
+const API = "http://localhost:4097/api/v1"
+let currentTab = "teams", usagePage = 1
+
+async function api(path, opt?: any) {
+  const t = localStorage.getItem("kc_admin_token")
+  const r = await fetch(`${API}${path}`, {headers:{Authorization:`Bearer ${t}`,...(opt?.json?{"Content-Type":"application/json"}:{})},method:opt?.method||"GET",body:opt?.json?JSON.stringify(opt.json):undefined})
+  return r.json()
+}
+
+async function doLogin() {
+  const body = {email:document.getElementById("lemail").value,password:document.getElementById("lpass").value}
+  try {
+    const d = await api("/auth/login",{method:"POST",json:body})
+    if (d.error) { document.getElementById("lerror").textContent = d.error; return }
+    if (d.user.role !== "admin") { document.getElementById("lerror").textContent = "仅限管理员登录"; return }
+    localStorage.setItem("kc_admin_token", d.token)
+    localStorage.setItem("kc_admin_user", JSON.stringify(d.user))
+    showApp()
+  } catch(e) { document.getElementById("lerror").textContent = "连接失败" }
+}
+
+async function loadTeams() {
+  const [users, teams] = await Promise.all([api("/admin/users"), api("/admin/teams")])
+  let html = `<div class="flex-row"><input class="input" id="newEmail" placeholder="成员邮箱"><input class="input" id="newName" placeholder="显示名称"><button class="btn" onclick="addMember()">添加成员</button></div>`
+  html += `<table><thead><tr><th>邮箱</th><th>名称</th><th>角色</th><th>Token</th><th>请求</th><th>费用</th><th>状态</th><th>操作</th></tr></thead><tbody>`
+  for (const u of users) {
+    html += `<tr><td>${u.email}</td><td>${u.display_name}</td>
+      <td><select class="select" onchange="updateUser('${u.id}','role',this.value)" value="${u.role}"><option ${u.role==='member'?'selected':''}>member</option><option ${u.role==='admin'?'selected':''}>admin</option></select></td>
+      <td style="text-align:right">${(u.usage.input+u.usage.output).toLocaleString()}</td>
+      <td style="text-align:right">${u.usage.requests}</td>
+      <td style="text-align:right;color:#f39c12">$${u.usage.cost.toFixed(4)}</td>
+      <td>${u.status||'active'}</td>
+      <td>
+        <button class="btn btn-sm ${u.status==='disabled'?'btn-green':'btn-orange'}" onclick="updateUser('${u.id}','status','${u.status==='disabled'?'active':'disabled'}')">${u.status==='disabled'?'启用':'禁用'}</button>
+      </td></tr>`
+  }
+  html += `</tbody></table>`
+  document.getElementById("content").innerHTML = html
+}
+
+function addMember() {
+  alert("请通过注册页面让成员先注册账号,然后管理员通过 PUT /api/v1/admin/users/:id 分配 team_id")
+}
+
+async function updateUser(id, field, value) {
+  await api(`/admin/users/${id}`, {method:"PUT", json:{[field]:value}})
+  loadTeams()
+}
+
+async function loadModels() {
+  const models = await api("/admin/models")
+  let html = `<div class="flex-row">
+    <input class="input" id="mProvider" placeholder="Provider (如 anthropic)"><input class="input" id="mModel" placeholder="Model (如 claude-sonnet-4-5)"><input class="input" id="mKey" placeholder="API Key"><button class="btn" onclick="addModel()">添加模型</button></div>`
+  html += `<table><thead><tr><th>Provider</th><th>Model</th><th>API Key</th><th>创建时间</th><th>操作</th></tr></thead><tbody>`
+  for (const m of models) {
+    html += `<tr><td>${m.provider}</td><td>${m.model}</td><td>${"*".repeat(m.api_key?.length||0)||"未设置"}</td><td>${new Date(m.created_at).toLocaleDateString()}</td>
+      <td><button class="btn btn-sm btn-red" onclick="delModel('${m.id}')">删除</button></td></tr>`
+  }
+  html += `</tbody></table>`
+  document.getElementById("content").innerHTML = html
+}
+
+async function addModel() {
+  const p = document.getElementById("mProvider").value
+  const m = document.getElementById("mModel").value
+  const k = document.getElementById("mKey").value
+  if (!p||!m) return alert("请填写 Provider 和 Model")
+  await api("/admin/models", {method:"POST", json:{provider:p,model:m,api_key:k}})
+  loadModels()
+}
+
+async function delModel(id) {
+  await api("/admin/models/"+id, {method:"DELETE"})
+  loadModels()
+}
+
+async function loadUsage() {
+  const s = await api("/admin/usage/summary")
+  let html = `<div class="cards">
+    <div class="card"><div class="card-label">全队 Token</div><div class="card-value">${((s.input||0)+(s.output||0)).toLocaleString()}</div></div>
+    <div class="card"><div class="card-label">请求次数</div><div class="card-value">${(s.requests||0).toLocaleString()}</div></div>
+    <div class="card"><div class="card-label">今日费用</div><div class="card-value">$${(s.cost||0).toFixed(4)}</div></div>
+    <div class="card"><div class="card-label">活跃用户</div><div class="card-value">${s.users||0}</div></div></div>`
+  html += `<div class="flex-row"><input class="input" id="uFilter" placeholder="筛选用户ID" style="width:160px"><input class="input" id="mFilter" placeholder="筛选模型" style="width:140px"><button class="btn" onclick="loadUsagePage(1)">筛选</button><button class="btn btn-green" onclick="exportCSV()">导出CSV</button></div>`
+  html += `<table><thead><tr><th>时间</th><th>用户</th><th>邮箱</th><th>模型</th><th>Token(入)</th><th>Token(出)</th><th>费用</th></tr></thead><tbody id="usageBody"></tbody></table>`
+  html += `<div class="pagination" id="usagePage"></div>`
+  document.getElementById("content").innerHTML = html
+  loadUsagePage(1)
+}
+
+async function loadUsagePage(p) {
+  usagePage = p
+  const uid = document.getElementById("uFilter")?.value || ""
+  const mdl = document.getElementById("mFilter")?.value || ""
+  const qs = [`page=${p}`,uid?`user_id=${uid}`:"",mdl?`model=${mdl}`:""].filter(Boolean).join("&")
+  const d = await api(`/admin/usage/detail?${qs}`)
+  document.getElementById("usageBody").innerHTML = d.data.map(e => `
+    <tr><td style="font-size:11px;color:#7f8c8d">${new Date(e.created_at).toLocaleString()}</td>
+    <td>${e.display_name}</td><td style="color:#7f8c8d">${e.email}</td><td>${e.provider}/${e.model}</td>
+    <td style="text-align:right">${e.input_tokens.toLocaleString()}</td><td style="text-align:right">${e.output_tokens.toLocaleString()}</td>
+    <td style="text-align:right;color:#f39c12">$${e.cost_usd.toFixed(5)}</td></tr>`).join("")
+  document.getElementById("usagePage").innerHTML = `
+    <button ${p<=1?"disabled":""} onclick="loadUsagePage(${p-1})">上一页</button>
+    <span style="color:#7f8c8d;font-size:13px">${d.page}/${d.pages} (${d.total}条)</span>
+    <button ${p>=d.pages?"disabled":""} onclick="loadUsagePage(${p+1})">下一页</button>`
+}
+
+async function exportCSV() {
+  const t = localStorage.getItem("kc_admin_token")
+  const r = await fetch(`${API}/admin/usage/export`, {headers:{Authorization:`Bearer ${t}`}})
+  const blob = await r.blob()
+  const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "kirincode-usage.csv"; a.click()
+}
+
+function loadTab(tab) {
+  currentTab = tab
+  document.querySelectorAll(".tab").forEach(e => e.classList.toggle("active", e.dataset.tab === tab))
+  if (tab === "teams") loadTeams()
+  else if (tab === "models") loadModels()
+  else if (tab === "usage") loadUsage()
+}
+
+function showApp() {
+  document.getElementById("login").style.display = "none"
+  document.getElementById("app").style.display = "block"
+  loadTab("teams")
+}
+
+function logout() {
+  localStorage.removeItem("kc_admin_token"); localStorage.removeItem("kc_admin_user")
+  location.reload()
+}
+
+document.querySelectorAll(".tab").forEach(e => e.onclick = () => loadTab(e.dataset.tab))
+
+if (localStorage.getItem("kc_admin_token")) showApp()
+else document.getElementById("login").style.display = "block"
+</script>
+</body>
+</html>

+ 1 - 1
packages/desktop/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@kirincode-ai/desktop",
   "private": true,
-  "version": "0.5.2",
+  "version": "0.6.0",
   "type": "module",
   "license": "MIT",
   "homepage": "https://kirincode.ai",

+ 184 - 0
packages/desktop/personal.html

@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
+<title>KirinCode — 个人使用</title>
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:system-ui,sans-serif;background:#0f172a;color:#ecf0f1;min-height:100vh}
+header{background:#1a1a2e;padding:12px 24px;border-bottom:1px solid #2c3e50;display:flex;align-items:center;gap:16px}
+h1{font-size:18px;color:#e74c3c}
+.tabs{display:flex;gap:4px;margin-left:24px}
+.tab{padding:6px 14px;border-radius:6px;border:none;font-size:13px;cursor:pointer;background:transparent;color:#7f8c8d}
+.tab.active{background:#e74c3c;color:#fff}
+main{padding:24px;max-width:1000px;margin:0 auto}
+.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:16px;margin-bottom:24px}
+.card{background:#1a1a2e;padding:20px;border-radius:12px;border:1px solid #2c3e50}
+.card-label{color:#7f8c8d;font-size:12px;margin-bottom:8px}
+.card-value{font-size:22px;font-weight:700;color:#f39c12}
+.chart{background:#1a1a2e;padding:16px;border-radius:10px;border:1px solid #2c3e50;margin-bottom:24px}
+table{width:100%;border-collapse:collapse}
+th{color:#7f8c8d;font-size:11px;text-align:left;padding:10px 8px;border-bottom:1px solid #2c3e50}
+td{padding:10px 8px;font-size:13px;border-bottom:1px solid #1e293b}
+.login-page{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100vh}
+.login-box{width:340px;display:flex;flex-direction:column;gap:12px}
+.input{padding:10px 14px;border-radius:8px;border:1px solid #2c3e50;background:#1a1a2e;color:#ecf0f1;font-size:14px;width:100%}
+.btn{padding:10px;border-radius:8px;border:none;background:#e74c3c;color:#fff;font-size:14px;cursor:pointer;width:100%}
+.btn:hover{opacity:.9}
+.link{color:#7f8c8d;text-align:center;cursor:pointer;font-size:13px;margin-top:4px}
+.logout-btn{margin-left:auto;padding:6px 14px;border-radius:6px;border:1px solid #2c3e50;background:transparent;color:#7f8c8d;cursor:pointer;font-size:12px}
+.pagination{display:flex;justify-content:center;gap:8px;margin-top:16px;align-items:center}
+.pagination button{padding:6px 14px;border-radius:6px;border:none;cursor:pointer;font-size:13px;background:transparent;color:#7f8c8d}
+.pagination button:disabled{opacity:.5}
+.error{color:#e74c3c;font-size:13px;text-align:center}
+</style>
+</head>
+<body>
+<div id="login" style="display:none">
+  <div class="login-page">
+    <div class="login-box">
+      <h1 style="color:#e74c3c;text-align:center;margin-bottom:12px">KirinCode 个人中心</h1>
+      <input type="email" id="lemail" class="input" placeholder="企业邮箱">
+      <input type="password" id="lpass" class="input" placeholder="密码">
+      <input type="text" id="lname" class="input" placeholder="显示名称(注册)" style="display:none">
+      <button class="btn" id="lbtn">登 录</button>
+      <div class="error" id="lerror"></div>
+      <div class="link" id="ltoggle">没有账号?注册</div>
+    </div>
+  </div>
+</div>
+
+<div id="app" style="display:none">
+  <header>
+    <h1 id="dispName">KirinCode 个人中心</h1>
+    <div class="tabs">
+      <button class="tab active" data-tab="summary">今日概览</button>
+      <button class="tab" data-tab="trend">趋势图表</button>
+      <button class="tab" data-tab="detail">用量明细</button>
+    </div>
+    <button class="logout-btn" onclick="logout()">退出</button>
+  </header>
+  <main>
+    <div id="tab-summary">
+      <div class="cards" id="cards"></div>
+      <div class="charts" id="summaryChart"></div>
+    </div>
+    <div id="tab-trend" style="display:none">
+      <div class="chart"><svg id="trendSvg" viewBox="0 0 700 220" style="width:100%;height:auto"></svg></div>
+    </div>
+    <div id="tab-detail" style="display:none">
+      <table><thead><tr><th>时间</th><th>模型</th><th>Token(入)</th><th>Token(出)</th><th>费用</th></tr></thead><tbody id="detailBody"></tbody></table>
+      <div class="pagination" id="pagination"></div>
+    </div>
+  </main>
+</div>
+
+<script>
+const API = "http://localhost:4097/api/v1"
+let isRegister = false, detailPage = 1, currentTab = "summary"
+
+document.getElementById("ltoggle").onclick = () => {
+  isRegister = !isRegister
+  document.getElementById("lname").style.display = isRegister ? "block" : "none"
+  document.getElementById("lbtn").textContent = isRegister ? "注 册" : "登 录"
+  document.getElementById("ltoggle").textContent = isRegister ? "已有账号?登录" : "没有账号?注册"
+}
+
+document.getElementById("lbtn").onclick = async () => {
+  const email = document.getElementById("lemail").value
+  const pass = document.getElementById("lpass").value
+  const name = document.getElementById("lname").value
+  const ep = isRegister ? "register" : "login"
+  const body = isRegister ? {email,password:pass,display_name:name} : {email,password:pass}
+  try {
+    const r = await fetch(`${API}/auth/${ep}`, {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)})
+    const d = await r.json()
+    if (d.error) { document.getElementById("lerror").textContent = d.error; return }
+    localStorage.setItem("kc_token", d.token)
+    localStorage.setItem("kc_user", JSON.stringify(d.user))
+    showApp(d.user)
+  } catch(e) { document.getElementById("lerror").textContent = "连接失败 — 请确认 Admin Server 已启动" }
+}
+
+function showApp(user) {
+  document.getElementById("login").style.display = "none"
+  document.getElementById("app").style.display = "block"
+  document.getElementById("dispName").textContent = "👤 " + (user.display_name || user.email)
+  loadTab("summary")
+}
+
+async function api(path) {
+  const t = localStorage.getItem("kc_token")
+  const r = await fetch(`${API}${path}`, {headers:{Authorization:`Bearer ${t}`}})
+  return r.json()
+}
+
+async function loadTab(tab) {
+  currentTab = tab
+  document.querySelectorAll(".tab").forEach(e => e.classList.toggle("active", e.dataset.tab === tab))
+  document.querySelectorAll("[id^='tab-']").forEach(e => e.style.display = "none")
+  document.getElementById("tab-"+tab).style.display = "block"
+
+  if (tab === "summary") {
+    const s = await api("/personal/summary")
+    document.getElementById("cards").innerHTML = `
+      <div class="card"><div class="card-label">今日 Token</div><div class="card-value">${((s.input||0)+(s.output||0)).toLocaleString()}</div></div>
+      <div class="card"><div class="card-label">请求次数</div><div class="card-value">${(s.requests||0).toLocaleString()}</div></div>
+      <div class="card"><div class="card-label">今日费用</div><div class="card-value">$${(s.cost||0).toFixed(4)}</div></div>`
+    const t = await api("/personal/trend")
+    if (t.length) drawTrend(t)
+  }
+  if (tab === "trend") {
+    const t = await api("/personal/trend")
+    if (t.length) drawTrend(t)
+    else document.getElementById("trendSvg").outerHTML = '<div style="color:#7f8c8d;text-align:center;padding:40px">暂无数据</div>'
+  }
+  if (tab === "detail") {
+    detailPage = 1; loadDetail()
+  }
+}
+
+async function loadDetail() {
+  const d = await api(`/personal/detail?page=${detailPage}`)
+  document.getElementById("detailBody").innerHTML = d.data.map(e => `
+    <tr><td style="font-size:11px;color:#7f8c8d">${new Date(e.created_at).toLocaleString()}</td>
+    <td>${e.provider}/${e.model}</td>
+    <td style="text-align:right">${e.input_tokens.toLocaleString()}</td>
+    <td style="text-align:right">${e.output_tokens.toLocaleString()}</td>
+    <td style="text-align:right;color:#f39c12">$${e.cost_usd.toFixed(5)}</td></tr>`).join("")
+  document.getElementById("pagination").innerHTML = `
+    <button ${detailPage<=1?"disabled":""} onclick="detailPage--;loadDetail()">上一页</button>
+    <span style="color:#7f8c8d;font-size:13px">${d.page}/${d.pages} (${d.total}条)</span>
+    <button ${detailPage>=d.pages?"disabled":""} onclick="detailPage++;loadDetail()">下一页</button>`
+}
+
+function drawTrend(data) {
+  const max = Math.max(...data.map(d=>d.input+d.output),1); const w=700,h=200
+  const pts = data.map((d,i) => `${(i/(data.length-1))*w},${h-((d.input+d.output)/max)*h}`).join(" ")
+  const labels = data.filter((_,i)=>i%Math.max(1,Math.floor(data.length/6))===0).map((d,i)=>{
+    const x = (i*Math.max(1,Math.floor(data.length/6))/(data.length-1))*w
+    return `<text x="${x}" y="${h+20}" fill="#7f8c8d" font-size="10" text-anchor="middle">${new Date(d.minute).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}</text>`
+  }).join("")
+  document.getElementById("trendSvg").innerHTML = `
+    <line x1="0" y1="0" x2="0" y2="${h}" stroke="#2c3e50" stroke-width="1"/>
+    <line x1="0" y1="${h}" x2="${w}" y2="${h}" stroke="#2c3e50" stroke-width="1"/>
+    <polyline points="${pts}" fill="none" stroke="#e74c3c" stroke-width="2"/>
+    <polygon points="0,${h} ${pts} ${w},${h}" fill="url(#g)" opacity="0.15"/>
+    <defs><linearGradient id="g"><stop offset="0%" stop-color="#e74c3c"/><stop offset="100%" stop-color="#e74c3c" stop-opacity="0"/></linearGradient></defs>
+    ${labels}`
+}
+
+function logout() {
+  localStorage.removeItem("kc_token"); localStorage.removeItem("kc_user")
+  document.getElementById("login").style.display = "block"
+  document.getElementById("app").style.display = "none"
+}
+
+document.querySelectorAll(".tab").forEach(e => e.onclick = () => loadTab(e.dataset.tab))
+
+// Auto-login
+if (localStorage.getItem("kc_token")) showApp(JSON.parse(localStorage.getItem("kc_user") || "{}"))
+else document.getElementById("login").style.display = "block"
+</script>
+</body>
+</html>

+ 150 - 127
packages/desktop/src/main/admin-server.ts

@@ -1,6 +1,4 @@
-// KirinCode Admin Server — JSON-store + JWT + Usage Stats + Team Management
-// Runs on port 4097, persists data to kirincode-admin.json
-
+// KirinCode Server v0.6.0 — Personal Usage + Admin API
 const PORT = 4097
 const { app } = require("electron")
 const path = require("path")
@@ -9,155 +7,191 @@ const { randomUUID, createHmac } = require("crypto")
 
 const DATA_FILE = path.join(app?.getPath("userData") || process.env.HOME || "/tmp", "kirincode-admin.json")
 
-// -- Data Store --
-let data: any = { users: {}, teams: {}, members: {}, usage: [] }
+let data: any = { users: {}, teams: {}, models: {}, usage: [] }
 try { data = JSON.parse(fs.readFileSync(DATA_FILE, "utf-8")) } catch {}
-const save = () => fs.writeFileSync(DATA_FILE, JSON.stringify(data), "utf-8")
+const save = () => fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), "utf-8")
 
-// -- JWT --
-const JWT_SECRET = String(process.env.KIRINCODE_ADMIN_SECRET || "kirincode-admin-secret" + Math.random())
-function signToken(uid: string): string {
+// JWT
+const JWT_SECRET = String(process.env.KIRINCODE_ADMIN_SECRET || "kirincode-admin-" + Math.random().toString(36))
+function signToken(uid: string, role: string): string {
   const h = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url")
-  const p = Buffer.from(JSON.stringify({ sub: uid, iat: ~~(Date.now()/1000), exp: ~~(Date.now()/1000)+86400 })).toString("base64url")
+  const p = Buffer.from(JSON.stringify({ sub: uid, role, iat: ~~(Date.now()/1000), exp: ~~(Date.now()/1000)+86400 })).toString("base64url")
   const s = createHmac("sha256", JWT_SECRET).update(`${h}.${p}`).digest("base64url")
   return `${h}.${p}.${s}`
 }
-function verifyToken(t: string): string | null {
-  try { const p = JSON.parse(Buffer.from(t.split(".")[1], "base64url").toString()); return p.exp*1000 > Date.now() ? p.sub : null }
+function verifyToken(t: string): { sub: string; role: string } | null {
+  try { const p = JSON.parse(Buffer.from(t.split(".")[1], "base64url").toString()); return p.exp*1000 > Date.now() ? { sub: p.sub, role: p.role } : null }
   catch { return null }
 }
-async function hashPw(pw: string) { const { hash } = await import("crypto"); return require("crypto").createHash("sha256").update(pw + JWT_SECRET).digest("hex") }
-function checkPw(pw: string, h: string) { return hashPw(pw) === h }
 
-// Cost estimation
-const PRICING: Record<string, Record<string, [number,number]>> = {
+const pricing: Record<string, Record<string, [number, number]>> = {
   anthropic: { "claude-sonnet-4-5": [3,15], "claude-haiku-4-5": [0.8,4] },
   openai: { "gpt-4o": [2.5,10], "gpt-4o-mini": [0.15,0.6] },
   deepseek: { "deepseek-v4-pro": [1.74,3.48], "deepseek-v4-flash": [0.14,0.28] },
   google: { "gemini-2.5-pro": [3.5,10.5], "gemini-2.5-flash": [0.4,1.5] },
 }
-function calcCost(provider: string, model: string, input: number, output: number): number {
-  const p = PRICING[provider]?.[model]
-  if (!p) return 0
+
+function cost(provider: string, model: string, input: number, output: number): number {
+  const p = pricing[provider]?.[model]; if (!p) return 0
   return Math.round(((input/1e6)*p[0] + (output/1e6)*p[1]) * 1e6) / 1e6
 }
 
-function auth(req: any): string | null {
-  const h = req.headers["authorization"] || req.headers["Authorization"]
-  if (!h?.startsWith("Bearer ")) return null
-  return verifyToken(h.slice(7))
+function auth(req: any): { sub: string; role: string } | null {
+  const h = req.headers["authorization"]?.replace("Bearer ", "")
+  if (!h) return null
+  return verifyToken(h)
 }
 
-function json(res: any, data: any, status = 200) {
-  res.writeHead(status, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type,Authorization" })
-  res.end(JSON.stringify(data))
-}
+const json = (res: any, d: any, s = 200) => { res.writeHead(s, {"Content-Type":"application/json","Access-Control-Allow-Origin":"*","Access-Control-Allow-Headers":"Content-Type,Authorization"}); res.end(JSON.stringify(d)) }
 
 async function body(req: any): Promise<any> {
-  return new Promise((resolve) => {
-    let b = ""; req.on("data", (c: string) => b += c); req.on("end", () => { try { resolve(JSON.parse(b)) } catch { resolve({}) } })
-  })
+  return new Promise(r => { let b=""; req.on("data",(c:any)=>b+=c); req.on("end",()=>{try{r(JSON.parse(b))}catch{r({})}}) })
 }
 
-// -- Start Server --
+// Cleanup old usage data (keep last 50000)
+function trim() { if (data.usage.length > 50000) data.usage = data.usage.slice(-50000) }
+
+// ===== Server =====
 require("http").createServer(async (req: any, res: any) => {
-  if (req.method === "OPTIONS") { res.writeHead(200, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE", "Access-Control-Allow-Headers": "Content-Type,Authorization" }); return res.end() }
+  if (req.method === "OPTIONS") { res.writeHead(200, {"Access-Control-Allow-Origin":"*","Access-Control-Allow-Methods":"GET,POST,PUT,DELETE","Access-Control-Allow-Headers":"Content-Type,Authorization"}); return res.end() }
 
-  const url = new URL(req.url, "http://localhost")
-  const p = url.pathname
+  const u = new URL(req.url, "http://localhost")
+  const p = u.pathname
 
-  if (p === "/api/v1/health") return json(res, { status: "ok", version: "0.5.1", users: Object.keys(data.users).length })
+  // --- Health ---
+  if (p === "/api/v1/health") return json(res, { status: "ok", version: "0.6.0", users: Object.keys(data.users).length })
 
-  // Auth
+  // --- Auth ---
   if (p === "/api/v1/auth/register" && req.method === "POST") {
     const b = await body(req)
-    if (data.users[b.email]) return json(res, { error: "Email exists" }, 409)
+    if (Object.values(data.users).some((u:any) => u.email === b.email)) return json(res, { error: "邮箱已注册" }, 409)
     const id = randomUUID()
-    data.users[id] = { id, email: b.email, password: await hashPw(b.password), display_name: b.display_name || b.email.split("@")[0], role: "member", team_id: null }
-    save()
-    return json(res, { token: signToken(id), user: data.users[id] })
+    const pw = require("crypto").createHash("sha256").update(b.password + JWT_SECRET).digest("hex")
+    data.users[id] = { id, email: b.email, password: pw, display_name: b.display_name || b.email.split("@")[0], role: "member", team_id: null, status: "pending" }
+    save(); return json(res, { token: signToken(id, "member"), user: { id, email: b.email, display_name: data.users[id].display_name, role: "member", status: "pending" } })
   }
+
   if (p === "/api/v1/auth/login" && req.method === "POST") {
     const b = await body(req)
-    const u = Object.values(data.users).find((u: any) => u.email === b.email) as any
-    if (!u || !(await checkPw(b.password, u.password))) return json(res, { error: "Invalid" }, 401)
-    return json(res, { token: signToken(u.id), user: { id: u.id, email: u.email, display_name: u.display_name, role: u.role, team_id: u.team_id } })
+    const pw = require("crypto").createHash("sha256").update(b.password + JWT_SECRET).digest("hex")
+    const user = Object.values(data.users).find((u:any) => u.email === b.email && u.password === pw) as any
+    if (!user) return json(res, { error: "邮箱或密码错误" }, 401)
+    if (user.status === "disabled") return json(res, { error: "账号已被禁用" }, 403)
+    return json(res, { token: signToken(user.id, user.role), user: { id: user.id, email: user.email, display_name: user.display_name, role: user.role, team_id: user.team_id, status: user.status } })
   }
 
-  // Prompts record usage (even without admin)
-  const uid = auth(req)
-  if (p.match(/^\/api\/v1\/sessions\/[^/]+\/prompt$/) && req.method === "POST" && uid) {
-    const sid = p.split("/")[4]
+  // --- Record usage (any authenticated user) ---
+  const userAuth = auth(req)
+  if (p.match(/^\/api\/v1\/prompt$/) && req.method === "POST" && userAuth) {
     const b = await body(req)
-    const text = (b.parts || []).filter((p:any) => p.type === "text").map((p:any) => p.text).join("\n")
-    const inputTokens = Math.ceil(text.length / 4)
-    const outputTokens = Math.ceil(text.length / 3)
-    const provider = b.model?.providerID || "anthropic"
-    const model = b.model?.modelID || "claude-sonnet-4-5"
-    const c = calcCost(provider, model, inputTokens, outputTokens)
-    data.usage.push({ id: randomUUID(), user_id: uid, team_id: (data.users[uid] as any)?.team_id, provider, model, session_id: sid, input_tokens: inputTokens, output_tokens: outputTokens, cost_usd: c, created_at: new Date().toISOString() })
-    keepOnly(10000)
-    save()
-    return json(res, { usage: { input_tokens: inputTokens, output_tokens: outputTokens, cost_usd: c } })
+    const text = b.text || ""
+    const it = Math.ceil(text.length / 4); const ot = Math.ceil(text.length / 3)
+    const c = cost(b.provider || "anthropic", b.model || "claude-sonnet-4-5", it, ot)
+    data.usage.push({ id: randomUUID(), user_id: userAuth.sub, team_id: (data.users[userAuth.sub] as any)?.team_id, provider: b.provider || "anthropic", model: b.model || "claude-sonnet-4-5", input_tokens: it, output_tokens: ot, cost_usd: c, duration_ms: b.duration || 3000, created_at: new Date().toISOString() })
+    trim(); save()
+    return json(res, { recorded: true, tokens: it + ot })
   }
 
-  // Admin required below
-  if (!uid) return json(res, { error: "Unauthorized" }, 401)
-  const user = data.users[uid] as any
-  const teamId = user?.team_id
+  // ===== Personal Usage (用户自己查看) =====
+  if (p === "/api/v1/personal/summary" && userAuth) {
+    const uid = userAuth.sub
+    const today = data.usage.filter((e:any) => e.user_id === uid && new Date(e.created_at) > new Date(Date.now()-86400000))
+    const total = { input: 0, output: 0, requests: today.length, cost: 0 }
+    for (const e of today) { total.input += e.input_tokens; total.output += e.output_tokens; total.cost += e.cost_usd }
+    return json(res, total)
+  }
 
-  // Usage Summary
-  if (p === "/api/v1/admin/usage/summary") {
-    const now = new Date().toISOString()
-    const today = data.usage.filter((e: any) => e.created_at > new Date(Date.now()-86400000).toISOString() && (!teamId || e.team_id === teamId))
-    const total = { input: 0, output: 0, requests: 0, cost: 0, users: new Set() }
-    for (const e of today) { total.input += e.input_tokens; total.output += e.output_tokens; total.requests++; total.cost += e.cost_usd; total.users.add(e.user_id) }
-    return json(res, { ...total, users: total.users.size })
+  if (p === "/api/v1/personal/trend" && userAuth) {
+    const uid = userAuth.sub
+    const m: Record<string,any> = {}
+    data.usage.filter((e:any) => e.user_id === uid && new Date(e.created_at) > new Date(Date.now()-86400000)).forEach((e:any) => {
+      const k = e.created_at.slice(0,16)
+      if (!m[k]) m[k] = { minute: k, input:0, output:0, requests:0, cost:0 }
+      m[k].input += e.input_tokens; m[k].output += e.output_tokens; m[k].requests++; m[k].cost += e.cost_usd
+    })
+    return json(res, Object.values(m).sort((a:any,b:any) => a.minute.localeCompare(b.minute)))
   }
 
-  // Trend (minute-level)
-  if (p === "/api/v1/admin/usage/trend") {
-    const trend: Record<string, any> = {}
-    const recent = data.usage.filter((e: any) => e.created_at > new Date(Date.now()-86400000).toISOString() && (!teamId || e.team_id === teamId))
-    for (const e of recent) {
-      const m = e.created_at.slice(0, 16)
-      if (!trend[m]) trend[m] = { minute: m, input: 0, output: 0, requests: 0, cost: 0 }
-      trend[m].input += e.input_tokens; trend[m].output += e.output_tokens; trend[m].requests++; trend[m].cost += e.cost_usd
-    }
-    return json(res, Object.values(trend).sort((a:any,b:any) => a.minute.localeCompare(b.minute)))
+  if (p === "/api/v1/personal/detail" && userAuth) {
+    const uid = userAuth.sub
+    const page = parseInt(u.searchParams.get("page") || "1"); const limit = 20
+    const rows = data.usage.filter((e:any) => e.user_id === uid).sort((a:any,b:any) => b.created_at.localeCompare(a.created_at))
+    const total = rows.length
+    const paged = rows.slice((page-1)*limit, page*limit)
+    return json(res, { data: paged, total, page, pages: Math.ceil(total/limit) })
   }
 
-  // Users ranking
-  if (p === "/api/v1/admin/usage/users") {
-    const uidFilter = url.searchParams.get("user_id")
-    const modelFilter = url.searchParams.get("model")
-    const from = url.searchParams.get("from") || "1970-01-01"
-    const to = url.searchParams.get("to") || "2099-12-31"
-    const users: Record<string, any> = {}
-    const filtered = data.usage.filter((e: any) => {
-      if (teamId && e.team_id !== teamId) return false
-      if (uidFilter && e.user_id !== uidFilter) return false
-      if (modelFilter && !e.model.includes(modelFilter)) return false
-      if (e.created_at < from || e.created_at > to) return false
-      return true
-    })
-    for (const e of filtered) {
-      if (!users[e.user_id]) { const u = data.users[e.user_id] || {} as any; users[e.user_id] = { user_id: e.user_id, display_name: u.display_name || e.user_id, email: u.email || "", input: 0, output: 0, requests: 0, cost: 0, duration: 0 } }
-      users[e.user_id].input += e.input_tokens; users[e.user_id].output += e.output_tokens; users[e.user_id].requests++; users[e.user_id].cost += e.cost_usd
+  // ===== Admin only below =====
+  if (!userAuth || userAuth.role !== "admin") return json(res, { error: "Not found" }, 404)
+
+  // --- Team Management ---
+  if (p === "/api/v1/admin/teams" && req.method === "GET") return json(res, Object.values(data.teams))
+  if (p === "/api/v1/admin/teams" && req.method === "POST") {
+    const b = await body(req)
+    const id = randomUUID()
+    data.teams[id] = { id, name: b.name, owner_id: userAuth.sub, created_at: new Date().toISOString() }
+    save(); return json(res, data.teams[id])
+  }
+
+  // --- User Management (CRUD) for team ---
+  if (p === "/api/v1/admin/users" && req.method === "GET") {
+    const teamId = u.searchParams.get("team_id") || (data.users[userAuth.sub] as any)?.team_id
+    const users = Object.values(data.users).filter((u:any) => u.team_id === teamId).map((u:any) => ({
+      id: u.id, email: u.email, display_name: u.display_name, role: u.role, status: u.status || "active",
+      usage: data.usage.filter((e:any) => e.user_id === u.id).reduce((s:any,e:any)=>({input:s.input+e.input_tokens,output:s.output+e.output_tokens,requests:s.requests+1,cost:s.cost+e.cost_usd}),{input:0,output:0,requests:0,cost:0})
+    }))
+    return json(res, users)
+  }
+
+  if (p.match(/^\/api\/v1\/admin\/users\/[^/]+$/) && req.method === "PUT") {
+    const uid = p.split("/")[5]
+    const b = await body(req)
+    if (data.users[uid]) {
+      if (b.display_name) data.users[uid].display_name = b.display_name
+      if (b.role) data.users[uid].role = b.role
+      if (b.status) data.users[uid].status = b.status
+      save()
     }
-    return json(res, Object.values(users).sort((a:any,b:any) => b.cost - a.cost))
+    return json(res, data.users[uid])
+  }
+
+  // --- Model Configuration (admin sets enterprise models) ---
+  if (p === "/api/v1/admin/models" && req.method === "GET") {
+    const teamId = (data.users[userAuth.sub] as any)?.team_id
+    return json(res, Object.values(data.models).filter((m:any) => !m.team_id || m.team_id === teamId))
+  }
+  if (p === "/api/v1/admin/models" && req.method === "POST") {
+    const b = await body(req)
+    const id = randomUUID()
+    const teamId = (data.users[userAuth.sub] as any)?.team_id
+    data.models[id] = { id, team_id: teamId, provider: b.provider, model: b.model, api_key: b.api_key, base_url: b.base_url || "", created_at: new Date().toISOString() }
+    save(); return json(res, data.models[id])
+  }
+  if (p.match(/^\/api\/v1\/admin\/models\/[^/]+$/) && req.method === "DELETE") {
+    const mid = p.split("/")[5]
+    delete data.models[mid]; save()
+    return json(res, { success: true })
+  }
+
+  // --- Admin Usage Stats ---
+  if (p === "/api/v1/admin/usage/summary") {
+    const teamId = (data.users[userAuth.sub] as any)?.team_id
+    const from = u.searchParams.get("from") || new Date(Date.now()-86400000).toISOString()
+    const filtered = data.usage.filter((e:any) => e.team_id === teamId && e.created_at > from)
+    const r = { input:0, output:0, requests:0, cost:0, users: new Set<string>() }
+    for (const e of filtered) { r.input += e.input_tokens; r.output += e.output_tokens; r.requests++; r.cost += e.cost_usd; r.users.add(e.user_id) }
+    return json(res, { ...r, users: r.users.size })
   }
 
-  // Detail (paginated)
   if (p === "/api/v1/admin/usage/detail") {
-    const uidFilter = url.searchParams.get("user_id")
-    const modelFilter = url.searchParams.get("model")
-    const from = url.searchParams.get("from") || "1970-01-01"
-    const to = url.searchParams.get("to") || "2099-12-31"
-    const page = parseInt(url.searchParams.get("page") || "1")
-    const limit = 30
-    const filtered = data.usage.filter((e: any) => {
-      if (teamId && e.team_id !== teamId) return false
+    const teamId = (data.users[userAuth.sub] as any)?.team_id
+    const uidFilter = u.searchParams.get("user_id")
+    const modelFilter = u.searchParams.get("model")
+    const from = u.searchParams.get("from") || "1970-01-01"
+    const to = u.searchParams.get("to") || "2099-12-31"
+    const page = parseInt(u.searchParams.get("page") || "1"); const limit = 30
+    const filtered = data.usage.filter((e:any) => {
+      if (e.team_id !== teamId) return false
       if (uidFilter && e.user_id !== uidFilter) return false
       if (modelFilter && !e.model.includes(modelFilter)) return false
       if (e.created_at < from || e.created_at > to) return false
@@ -171,27 +205,16 @@ require("http").createServer(async (req: any, res: any) => {
     return json(res, { data: paged, total, page, pages: Math.ceil(total/limit) })
   }
 
-  // Teams
-  if (p === "/api/v1/admin/teams" && req.method === "GET") return json(res, Object.values(data.teams))
-  if (p === "/api/v1/admin/teams" && req.method === "POST") {
-    const b = await body(req)
-    const id = randomUUID()
-    data.teams[id] = { id, name: b.name, owner_id: b.owner_id || uid }
-    if (b.owner_id) {
-      data.users[b.owner_id].team_id = id; data.users[b.owner_id].role = "team_admin"
-    }
-    save(); return json(res, data.teams[id])
-  }
-  if (p === "/api/v1/admin/teams/invite" && req.method === "POST") {
-    const b = await body(req)
-    const invited = Object.values(data.users).find((u:any) => u.email === b.email) as any
-    if (!invited) return json(res, { error: "Not found" }, 404)
-    if (!invited.team_id) { invited.team_id = teamId; invited.role = "member" }
-    save(); return json(res, { success: true })
+  if (p === "/api/v1/admin/usage/export") {
+    const teamId = (data.users[userAuth.sub] as any)?.team_id
+    const csv = ["时间,用户,邮箱,模型,Token(入),Token(出),费用(USD)"]
+    data.usage.filter((e:any) => e.team_id === teamId).sort((a:any,b:any) => b.created_at.localeCompare(a.created_at)).forEach((e:any) => {
+      const u = data.users[e.user_id] || {}
+      csv.push(`${e.created_at},${(u as any).display_name||e.user_id},${(u as any).email||""},${e.provider}/${e.model},${e.input_tokens},${e.output_tokens},${e.cost_usd}`)
+    })
+    res.writeHead(200, {"Content-Type":"text/csv","Content-Disposition":"attachment;filename=usage-export.csv","Access-Control-Allow-Origin":"*"})
+    return res.end(csv.join("\n"))
   }
-  if (p === "/api/v1/admin/users") return json(res, Object.values(data.users).map((u:any) => ({ id: u.id, display_name: u.display_name, email: u.email, role: u.role, team_id: u.team_id })))
-
-  return json(res, { error: "Not found" }, 404)
-}).listen(PORT, () => console.log(`Admin server: http://localhost:${PORT}`))
 
-function keepOnly(n: number) { if (data.usage.length > n) data.usage = data.usage.slice(-n) }
+  json(res, { error: "Not found" }, 404)
+}).listen(PORT, () => console.log(`Admin API: http://localhost:${PORT}`))

+ 1 - 1
packages/effect-drizzle-sqlite/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "name": "@kirincode-ai/effect-drizzle-sqlite",
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/effect-sqlite-node/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "name": "@kirincode-ai/effect-sqlite-node",
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/enterprise/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/enterprise",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/function/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/function",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "$schema": "https://json.schemastore.org/package.json",
   "private": true,
   "type": "module",

+ 1 - 1
packages/http-recorder/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "name": "@kirincode-ai/http-recorder",
   "description": "Record and replay Effect HTTP client traffic with deterministic cassettes",
   "type": "module",

+ 1 - 1
packages/kirincode-cloud/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/kirincode-cloud",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "type": "module",
   "description": "KirinCode Cloud Sync Server — multi-user backend with session sync, memory, skills, workflows",
   "license": "MIT",

+ 1 - 1
packages/kirincode-workflow/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/kirincode-workflow",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "type": "module",
   "description": "KirinCode workflow engine — scheduled tasks, event triggers, background agents",
   "license": "MIT",

+ 1 - 1
packages/kirincode/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "name": "kirincode",
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/llm/package.json

@@ -1,6 +1,6 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "name": "@kirincode-ai/llm",
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/mobile/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/mobile",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "private": true,
   "scripts": {
     "start": "expo start",

+ 1 - 1
packages/plugin/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/plugin",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/sdk/js/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/sdk",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/server/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/server",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/session-ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/session-ui",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/slack",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "type": "module",
   "license": "MIT",
   "scripts": {

+ 1 - 1
packages/stats/app/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/stats-app",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/stats/core/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/stats-core",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/stats/server/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/stats-server",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/tui/package.json

@@ -1,7 +1,7 @@
 {
   "$schema": "https://json.schemastore.org/package.json",
   "name": "@kirincode-ai/tui",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "private": true,
   "type": "module",
   "license": "MIT",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@kirincode-ai/ui",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "type": "module",
   "license": "MIT",
   "repository": {

+ 1 - 1
packages/web/package.json

@@ -2,7 +2,7 @@
   "name": "@kirincode-ai/web",
   "type": "module",
   "license": "MIT",
-  "version": "0.5.2",
+  "version": "0.6.0",
   "scripts": {
     "dev": "astro dev",
     "dev:remote": "VITE_API_URL=https://api.kirincode.ai astro dev",