refactor(desktop,gateway): 用户端去重 —— 一个功能一个入口,一份数据一个查法

用户反馈「UI 太乱」。数出来的:report/kb/memory 各 4 个入口、studio 5 个;
运行历史两套渲染。乱的机制是同一件事写了两遍,改一处另一处必然掉队。

**一份数据一个查法**
- 工作台「最近任务」和运行页「运行历史」是同一批数据,却各查各的:
  前者走 store.RecentTasks(),后者走 RecentRuns()。于是我给运行历史加的
  topic 字段工作台完全没跟上,还在 mono 显示 report_<hex>;而且 RecentTasks
  没有租户过滤,口径也不一致。现在统一走 RecentRuns,RecentTasks 删掉
  (它只有那一个调用点)。前端 recent_runs 也改用 RunSummary 同一个类型。

**一个功能一个入口**
- 工作台删掉「入库知识/生成报告/管理记忆」按钮排 —— 跳的目标和上面四张
  能力卡片完全重合,同一页给两个入口。
- 「最近任务」现在点击直达运行页对应那条(App 加 goto(view, taskId) →
  RunsView focusTaskId),而不是只把页面切过去让用户自己再找一遍。

**报告归 RUN,只做启动器**
- 报告是「执行」不是「构建」,从 BUILD 组移到 RUN。
- 报告页砍掉「执行轨迹」「报告正文」两个面板 —— 和运行页完全重复,而且
  那套是组件本地 state,切页面就没。现在它只有主题输入框。
- 报告改走 App 的全局运行态(新增 onRunReport,与 onRun 同构、复用 attachRun):
  提交 → 跳「运行 · 观测」→ 实时看轨迹/正文/导出。App 里 onRun 早就写着
  「发起即跳运行页,观测统一收敛在此页」——这条方针一直在,只有报告没遵守。

**顺带清死代码**
- 市场(规划中)从导航下架,ViewKey 里的 "market" 和 Boxes 图标一并删。
- PLACEHOLDERS 整块死代码:它列的 home/kb/report/runs 全都早已真实实现,
  market 是唯一还可能用到的、下架后那个 Placeholder 分支永远走不到
  (tsc 报 "Spread types" 就是因为 PLACEHOLDERS[view] 已成 never)。
  连同 views/Placeholder.tsx 一起删。

live 验证(真账号,桌面端实机):报告页输入主题 → 提交 → 自动跳运行页 →
顶部「当前运行 · live」→ 轨迹实时跑 → 报告正文流式出 + 导出按钮在位 →
report_7a0110a10b534f90 落库带 topic「归口测试」,POST /api/v1/reports → 202。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-16 14:08:16 +08:00
parent fd99ba6226
commit 759483bf92
9 changed files with 110 additions and 212 deletions
+36 -16
View File
@@ -12,11 +12,10 @@ import { ReportView } from "./views/ReportView";
import { RunsView } from "./views/RunsView";
import { UsageView } from "./views/UsageView";
import { Home } from "./views/Home";
import { Placeholder } from "./views/Placeholder";
import { CommandPalette, type Command } from "./components/CommandPalette";
import { UpdateBanner } from "./components/UpdateBanner";
import { Login } from "./views/Login";
import { submitTask, streamTokens, streamExec, taskStatus, listRuns, authMe, logout, tenantCurrent, myTenants, switchTenant, spaceCurrent, mySpaces, switchSpace, createSpace, type Identity, type AuthUser, type TenantCtx, type MyTenant, type SpaceCtx, type MySpace } from "./lib/api";
import { submitTask, generateReport, streamTokens, streamExec, taskStatus, listRuns, authMe, logout, tenantCurrent, myTenants, switchTenant, spaceCurrent, mySpaces, switchSpace, createSpace, type Identity, type AuthUser, type TenantCtx, type MyTenant, type SpaceCtx, type MySpace } from "./lib/api";
import type { TaskDsl } from "./lib/dsl";
import { emptyRun, type RunState } from "./lib/run";
import { ToastProvider } from "./ui";
@@ -31,16 +30,11 @@ function getSessionId(): string {
return s;
}
const PLACEHOLDERS: Partial<Record<ViewKey, { title: string; desc: string }>> = {
home: { title: "工作台", desc: "概览:知识库 / 文档 / 近期运行 / 待办报告 / 配额计费 + 快捷入口。" },
kb: { title: "知识库 (RAG)", desc: "入库流水线监控 · 检索调试台(带来源徽标) · 文档/块浏览 · 知识图谱 · 检索评测。依赖 embedding + 入库 worker + 真实混合检索。" },
report: { title: "报告生成", desc: "模板库 · 大纲编辑 · 章节并行生成进度 · 实时预览(含引用) · 导出 docx/pdf。依赖 RAG 核心链 + UniOffice。" },
runs: { title: "运行 · 观测", desc: "实时执行 · 节点轨迹 · 工具调用 · 运行历史复盘。当前运行结果见底部抽屉。" },
market: { title: "市场 · Packs(规划中)", desc: "垂直包(法律/医疗/金融) · Agent 模板 · 开通向导(建租户→入库→注册模板→应用配置)。依赖多租户 + Pack 格式。" },
};
export default function App() {
const [view, setView] = useState<ViewKey>("home");
// 从工作台「最近任务」点进来时要定位到具体那条运行,而不是只把页面切过去。
const [focusRun, setFocusRun] = useState<string | null>(null);
const goto = (v: ViewKey, taskId?: string) => { setView(v); setFocusRun(taskId ?? null); };
const [user, setUser] = useState<AuthUser | null>(null);
const [tenant, setTenant] = useState<TenantCtx | null>(null);
const [tenants, setTenants] = useState<MyTenant[]>([]);
@@ -252,6 +246,34 @@ export default function App() {
[identity, attachRun],
);
// 报告生成走和编排任务同一条路:灌进全局运行态 + 跳「运行 · 观测」。
// 此前报告页自己维护一套 SSE/输出/轨迹本地 state,于是运行页看不到它、
// 切个页面这套 state 就没了 —— 报告明明在后端跑完了却永远找不回。
const onRunReport = useCallback(
async (topic: string, kb?: string) => {
closeRef.current?.();
execCloseRef.current?.();
stopPoll();
const t0 = Date.now();
setRun({ phase: "submitting", output: "", events: [{ t: 0, label: "提交报告任务" }], exec: [] });
setFocusRun(null);
setView("runs");
try {
const taskId = await generateReport(identity, topic, kb);
setRun((r) => ({
...r,
phase: "streaming",
taskId,
events: [...r.events, { t: Date.now() - t0, label: `已发布 ${taskId}` }],
}));
attachRun(taskId, t0);
} catch (e) {
setRun((r) => ({ ...r, phase: "error", error: (e as Error).message }));
}
},
[identity, attachRun],
);
// 恢复在途待审任务:登录后若存在 waiting 任务且当前无 live run,挂回它 → 全局审批条重现,
// 用户刷新页面/重开 app 也能继续批准(HITL 持久化中断后审批可跨重启、可等数小时)。
const restoredRef = useRef(false);
@@ -303,22 +325,20 @@ export default function App() {
<LeftNav active={view} onSelect={setView} />
<main className="min-w-0 flex-1 overflow-hidden">
{view === "home" ? (
<Home onSelect={setView} userName={user.name || user.email} spaceName={space?.space?.name} />
<Home onSelect={goto} userName={user.name || user.email} spaceName={space?.space?.name} />
) : view === "studio" ? (
<StudioView onRun={onRun} phase={run.phase} identity={identity} readOnly={tenant?.role === "viewer"} spaceId={space?.space?.id ?? ""} spaceReadOnly={space?.role === "viewer"} />
) : view === "kb" ? (
<KbView identity={identity} spaceId={space?.space?.id ?? ""} spaceReadOnly={space?.role === "viewer"} />
) : view === "report" ? (
<ReportView identity={identity} />
<ReportView onRunReport={onRunReport} running={run.phase === "submitting" || run.phase === "streaming"} />
) : view === "runs" ? (
<RunsView run={run} />
<RunsView run={run} focusTaskId={focusRun} />
) : view === "memory" ? (
<MemoryView identity={identity} />
) : view === "usage" ? (
<UsageView />
) : (
<Placeholder {...(PLACEHOLDERS[view] ?? { title: "模块", desc: "规划中。" })} />
)}
) : null}
</main>
</div>
<CommandPalette open={cmdOpen} onClose={() => setCmdOpen(false)} commands={commands} />