feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1

Merged
Blizzard merged 181 commits from feat/wails3 into main 2026-07-17 01:12:32 +00:00
3 changed files with 71 additions and 5 deletions
Showing only changes of commit 248267984c - Show all commits
+18 -2
View File
@@ -15,7 +15,7 @@ import { Placeholder } from "./views/Placeholder";
import { CommandPalette, type Command } from "./components/CommandPalette"; import { CommandPalette, type Command } from "./components/CommandPalette";
import { UpdateBanner } from "./components/UpdateBanner"; import { UpdateBanner } from "./components/UpdateBanner";
import { Login } from "./views/Login"; import { Login } from "./views/Login";
import { submitTask, streamTokens, streamExec, taskStatus, listRuns, authMe, logout, tenantCurrent, type Identity, type AuthUser, type TenantCtx } from "./lib/api"; import { submitTask, streamTokens, streamExec, taskStatus, listRuns, authMe, logout, tenantCurrent, myTenants, switchTenant, type Identity, type AuthUser, type TenantCtx, type MyTenant } from "./lib/api";
import type { TaskDsl } from "./lib/dsl"; import type { TaskDsl } from "./lib/dsl";
import { emptyRun, type RunState } from "./lib/run"; import { emptyRun, type RunState } from "./lib/run";
import { ToastProvider } from "./ui"; import { ToastProvider } from "./ui";
@@ -43,6 +43,7 @@ export default function App() {
const [view, setView] = useState<ViewKey>("home"); const [view, setView] = useState<ViewKey>("home");
const [user, setUser] = useState<AuthUser | null>(null); const [user, setUser] = useState<AuthUser | null>(null);
const [tenant, setTenant] = useState<TenantCtx | null>(null); const [tenant, setTenant] = useState<TenantCtx | null>(null);
const [tenants, setTenants] = useState<MyTenant[]>([]);
const [authLoading, setAuthLoading] = useState(true); const [authLoading, setAuthLoading] = useState(true);
const identity = useMemo<Identity>(() => ({ userId: user?.id ?? "", sessionId: getSessionId() }), [user]); const identity = useMemo<Identity>(() => ({ userId: user?.id ?? "", sessionId: getSessionId() }), [user]);
const [run, setRun] = useState<RunState>(emptyRun); const [run, setRun] = useState<RunState>(emptyRun);
@@ -92,10 +93,25 @@ export default function App() {
tenantCurrent() tenantCurrent()
.then(setTenant) .then(setTenant)
.catch(() => {}); .catch(() => {});
myTenants()
.then((r) => setTenants(r.tenants))
.catch(() => {});
}, []); }, []);
const onSwitchTenant = useCallback(
async (id: string) => {
try {
await switchTenant(id);
refreshTenant();
} catch {
/* ignore */
}
},
[refreshTenant],
);
useEffect(() => { useEffect(() => {
if (!user) { if (!user) {
setTenant(null); setTenant(null);
setTenants([]);
return; return;
} }
refreshTenant(); refreshTenant();
@@ -239,7 +255,7 @@ export default function App() {
style={{ background: "radial-gradient(60% 100% at 50% 0%, rgba(124,92,246,0.10), transparent 70%)" }} style={{ background: "radial-gradient(60% 100% at 50% 0%, rgba(124,92,246,0.10), transparent 70%)" }}
/> />
<UpdateBanner /> <UpdateBanner />
<TopBar user={user} tenant={tenant} onLogout={onLogout} onCommand={() => setCmdOpen(true)} onOpenUsage={() => setView("usage")} /> <TopBar user={user} tenant={tenant} tenants={tenants} onSwitchTenant={onSwitchTenant} onLogout={onLogout} onCommand={() => setCmdOpen(true)} onOpenUsage={() => setView("usage")} />
<ApprovalBar run={run} /> <ApprovalBar run={run} />
<div className="relative flex min-h-0 flex-1"> <div className="relative flex min-h-0 flex-1">
<LeftNav active={view} onSelect={setView} /> <LeftNav active={view} onSelect={setView} />
+26
View File
@@ -139,6 +139,32 @@ export async function tenantCurrent(): Promise<TenantCtx | null> {
}; };
} }
// ---- 我所属的租户 + 切换活跃租户(多租户)----
export interface MyTenant {
id: string;
name: string;
slug: string;
plan: string;
credit_balance_micro: number;
shared_billing: boolean;
members: number;
}
export async function myTenants(): Promise<{ tenants: MyTenant[]; active: string }> {
const res = guard401(await fetch(`${GATEWAY}/api/v1/me/tenants`, { headers: bearer() }));
if (!res.ok) return { tenants: [], active: "" };
const d = (await res.json()) as { tenants?: MyTenant[]; active?: string };
return { tenants: d.tenants ?? [], active: d.active ?? "" };
}
export async function switchTenant(tenantId: string): Promise<void> {
const res = guard401(await fetch(`${GATEWAY}/api/v1/me/tenant`, { method: "POST", headers: { "Content-Type": "application/json", ...bearer() }, body: JSON.stringify({ tenant_id: tenantId }) }));
if (!res.ok) {
const d = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(d.error ?? `switch failed: ${res.status}`);
}
}
// ---- 我的用量明细(当前用户自己租户:余额 + 按天趋势 + 最近消耗)---- // ---- 我的用量明细(当前用户自己租户:余额 + 按天趋势 + 最近消耗)----
export interface UsageDay { export interface UsageDay {
day: string; // YYYYMMDD day: string; // YYYYMMDD
+27 -3
View File
@@ -1,6 +1,6 @@
import type { CSSProperties } from "react"; import type { CSSProperties } from "react";
import { User, ChevronDown, Search as SearchIcon, LogOut, Sun, Moon, Coins } from "lucide-react"; import { User, ChevronDown, Search as SearchIcon, LogOut, Sun, Moon, Coins, Building2 } from "lucide-react";
import type { AuthUser, TenantCtx } from "../lib/api"; import type { AuthUser, TenantCtx, MyTenant } from "../lib/api";
import { useHealth } from "../lib/health"; import { useHealth } from "../lib/health";
import { isMacDesktop } from "../lib/desktop"; import { isMacDesktop } from "../lib/desktop";
import { useTheme } from "../lib/theme"; import { useTheme } from "../lib/theme";
@@ -42,8 +42,31 @@ function CreditChip({ tenant, onClick }: { tenant: TenantCtx; onClick?: () => vo
); );
} }
// 租户切换器:仅当用户属多个租户时显示;切换即改活跃租户(工作区 + 计费上下文)。
function TenantSwitcher({ tenants, activeId, onSwitch }: { tenants: MyTenant[]; activeId?: string; onSwitch?: (id: string) => void }) {
if (tenants.length < 2) return null;
return (
<div className="relative" style={NODRAG}>
<Building2 className="pointer-events-none absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-500" />
<select
value={activeId ?? ""}
onChange={(e) => onSwitch?.(e.target.value)}
title="切换当前租户(工作区 + 计费)"
className="appearance-none rounded-md border border-line bg-ink-800 py-1 pl-7 pr-7 text-xs text-slate-300 focus:border-brand focus:outline-none"
>
{tenants.map((t) => (
<option key={t.id} value={t.id}>
{t.name}
</option>
))}
</select>
<ChevronDown className="pointer-events-none absolute right-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-slate-500" />
</div>
);
}
// 顶栏:品牌 · 垂直切换 · 健康灯 · 积分余额 · 登录用户 + 登出(深色 + 毛玻璃)。 // 顶栏:品牌 · 垂直切换 · 健康灯 · 积分余额 · 登录用户 + 登出(深色 + 毛玻璃)。
export function TopBar({ user, tenant, onLogout, onCommand, onOpenUsage }: { user: AuthUser; tenant?: TenantCtx | null; onLogout: () => void; onCommand?: () => void; onOpenUsage?: () => void }) { export function TopBar({ user, tenant, tenants = [], onSwitchTenant, onLogout, onCommand, onOpenUsage }: { user: AuthUser; tenant?: TenantCtx | null; tenants?: MyTenant[]; onSwitchTenant?: (id: string) => void; onLogout: () => void; onCommand?: () => void; onOpenUsage?: () => void }) {
const h = useHealth(); const h = useHealth();
const { theme, toggle } = useTheme(); const { theme, toggle } = useTheme();
return ( return (
@@ -86,6 +109,7 @@ export function TopBar({ user, tenant, onLogout, onCommand, onOpenUsage }: { use
<Light on={h.neo4j} label="Neo4j" /> <Light on={h.neo4j} label="Neo4j" />
</div> </div>
<div className="ml-auto flex items-center gap-2" style={NODRAG}> <div className="ml-auto flex items-center gap-2" style={NODRAG}>
<TenantSwitcher tenants={tenants} activeId={tenant?.tenant?.id} onSwitch={onSwitchTenant} />
{tenant?.tenant && <CreditChip tenant={tenant} onClick={onOpenUsage} />} {tenant?.tenant && <CreditChip tenant={tenant} onClick={onOpenUsage} />}
<button <button
onClick={toggle} onClick={toggle}