任务列表分页(服务端 + 前端): - GET /tasks 新增 limit(1..100,非法回 400)、offset、 order=desc、status/executor/q 服务端过滤;回 total(过滤后 计数)与 status_counts(全项目统计,不受过滤与分页影响, 数据卡片不再把当前页当成全项目);无 limit 时旧行为不变 (全量、正序,协调子任务与旧调用不受影响)。 - 前端每页 20 条、最新在前,上一页/下一页 + 页码;筛选与 搜索改服务端过滤(搜索防抖 300ms),切筛选/切项目/派发 后回第 1 页,10 秒刷新与刷新按钮停在当前页;新测试 test_task_list_pagination.py 5 项。 分页面签: - 顶栏锚点导航改成 5 个页签:工作台(派发 + 任务列表 + 对话 + 详情)、项目(项目 + 成员)、环境与 Workers、 技能与自动化(技能 + 自动化 + 分诊)、管理与 Key; 数据卡片仍在页签上方常驻。选中任务存在 App 层 store 里,切页不丢,回来仍是同一任务、10 秒刷新续同步; 页签同步到 location.hash(#tab-workbench 等),刷新与 分享链接落在同一页签;窄屏工作台仍保持派发在前的顺序。 - 工作台页签仍是左(任务)右(派发/对话/详情)两栏, 沿用等高内部滚动规则;其它页签单列堆叠。
159 lines
8.6 KiB
TypeScript
159 lines
8.6 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import { getKey } from "../api/client";
|
|
import { useCatalog } from "./useCatalog";
|
|
import { useTaskList } from "./useTaskList";
|
|
import { useTaskDetail } from "./useTaskDetail";
|
|
import { useProjectTools } from "./useProjectTools";
|
|
import { formatRefreshTime } from "./useAutoRefresh";
|
|
import type { ConsoleCtx } from "../context";
|
|
|
|
// Glue only: composes the domain hooks into one ConsoleCtx and owns
|
|
// the auto-refresh policy. No API calls and no rendering live here.
|
|
export function useConsoleStore(): ConsoleCtx {
|
|
const catalog = useCatalog();
|
|
const taskList = useTaskList();
|
|
const projectIdRef = useRef("");
|
|
projectIdRef.current = catalog.projectId;
|
|
const getProjectId = useCallback(() => projectIdRef.current, []);
|
|
const tools = useProjectTools(getProjectId);
|
|
const getRole = useCallback((pid?: string) => {
|
|
const p = catalog.projects.find((x) => x.id === pid);
|
|
return p ? (p.my_role || null) : null;
|
|
}, [catalog.projects]);
|
|
const onTasksReload = useCallback(() => { void taskList.refreshTasks(undefined, projectIdRef.current); }, [taskList]);
|
|
const detail = useTaskDetail(getRole, onTasksReload);
|
|
const [refreshStamp, setRefreshStamp] = useState("--:--:--");
|
|
|
|
const selectProject = useCallback((id: string) => {
|
|
catalog.setProjectId(id);
|
|
projectIdRef.current = id;
|
|
void taskList.refreshTasksFirstPage(id);
|
|
void catalog.refreshEnvs(id);
|
|
void catalog.refreshMembers(id);
|
|
void catalog.refreshSkills(id);
|
|
void tools.refreshAutomations(id);
|
|
void tools.refreshTriage(id);
|
|
}, [catalog, taskList, tools]);
|
|
|
|
// Fix: the old single page's project <select> defaulted to its first
|
|
// option once loaded, so tasks loaded without a click. The React
|
|
// version is a controlled select with projectId "" on every reload,
|
|
// so without this the task list stayed on "先选项目" forever and
|
|
// auto-refresh never fixed it. Auto-select the first project only
|
|
// while nothing is selected; an explicit user choice is never
|
|
// overridden.
|
|
const firstProjectId = catalog.projects[0]?.id;
|
|
useEffect(() => {
|
|
if (!catalog.projectId && firstProjectId) selectProject(firstProjectId);
|
|
}, [catalog.projectId, firstProjectId, selectProject]);
|
|
|
|
const saveKeyInput = useCallback((v: string) => {
|
|
catalog.saveKeyInput(v);
|
|
if (!v.trim()) return;
|
|
// Same reload set as the live saveKey handler.
|
|
void catalog.refreshHealth();
|
|
void catalog.refreshProjects();
|
|
void catalog.refreshWorkers();
|
|
if (projectIdRef.current) {
|
|
void taskList.refreshTasks(projectIdRef.current);
|
|
void catalog.refreshEnvs(projectIdRef.current);
|
|
void catalog.refreshMembers(projectIdRef.current);
|
|
}
|
|
void catalog.loadRelayModels();
|
|
void catalog.loadQuota();
|
|
void catalog.loadGithubRepos();
|
|
void catalog.probeAdmin();
|
|
}, [catalog, taskList]);
|
|
|
|
const refreshAll = useCallback((selectFocused: boolean) => {
|
|
setRefreshStamp(formatRefreshTime(new Date()));
|
|
if (!getKey()) { void catalog.refreshHealth(); return; }
|
|
void taskList.refreshTasks(undefined, projectIdRef.current);
|
|
void catalog.refreshHealth();
|
|
void catalog.loadQuota();
|
|
void catalog.refreshWorkers();
|
|
void catalog.refreshMembers();
|
|
void catalog.refreshSkills();
|
|
void tools.refreshAutomations();
|
|
void tools.refreshTriage();
|
|
void detail.refreshDetailLists();
|
|
if (!selectFocused) {
|
|
void catalog.refreshProjects();
|
|
void catalog.refreshEnvs();
|
|
}
|
|
}, [catalog, taskList, tools, detail]);
|
|
|
|
const refreshSlow = useCallback(() => {
|
|
if (!getKey()) return;
|
|
void catalog.loadRelayModels();
|
|
void catalog.loadGithubRepos();
|
|
}, [catalog]);
|
|
|
|
// Initial load, mirroring the live file's boot sequence.
|
|
useEffect(() => {
|
|
void catalog.refreshHealth();
|
|
void catalog.refreshProjects();
|
|
void catalog.refreshWorkers();
|
|
void catalog.loadQuota();
|
|
void catalog.loadGithubRepos();
|
|
if (getKey()) void catalog.loadRelayModels();
|
|
void catalog.probeAdmin();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
return {
|
|
keySaved: catalog.keySaved, keyState: catalog.keyState,
|
|
saveKeyInput, clearKeyInput: catalog.clearKeyInput,
|
|
quotaText: catalog.quotaText,
|
|
healthText: catalog.healthText, refreshHealth: catalog.refreshHealth,
|
|
adminGate: catalog.adminGate, adminBody: catalog.adminBody, adminResult: catalog.adminResult,
|
|
adminUserId: catalog.adminUserId, setAdminUserId: catalog.setAdminUserId,
|
|
adminQuota: catalog.adminQuota, adminSweep: catalog.adminSweep, adminBackup: catalog.adminBackup,
|
|
projects: catalog.projects, projectId: catalog.projectId, projectMsg: catalog.projectMsg,
|
|
selectProject, refreshProjects: catalog.refreshProjects,
|
|
createProject: catalog.createProject,
|
|
githubRepos: catalog.githubRepos,
|
|
workers: catalog.workers, workersSummary: catalog.workersSummary, workersError: catalog.workersError, refreshWorkers: catalog.refreshWorkers,
|
|
members: catalog.members, memberMsg: catalog.memberMsg, memberError: catalog.memberError,
|
|
refreshMembers: () => void catalog.refreshMembers(), inviteMember: catalog.inviteMember, changeMemberRole: catalog.changeMemberRole,
|
|
skills: catalog.skills, apps: catalog.apps, mcpServers: catalog.mcpServers, skillMsg: catalog.skillMsg, skillError: catalog.skillError,
|
|
mcpMsg: catalog.mcpMsg, mcpOauthMsg: catalog.mcpOauthMsg, mcpOauthBox: catalog.mcpOauthBox,
|
|
refreshSkills: () => void catalog.refreshSkills(), reloadMcp: catalog.reloadMcp, mcpOauth: catalog.mcpOauth,
|
|
envs: catalog.envs, envCount: catalog.envCount, envMsg: catalog.envMsg, envRepoLine: catalog.envRepoLine, envRepoBar: catalog.envRepoBar, envRepoMsg: catalog.envRepoMsg, envSelect: catalog.envSelect, setEnvSelect: catalog.setEnvSelect,
|
|
refreshEnvs: () => void catalog.refreshEnvs(), createEnv: catalog.createEnv, archiveEnv: catalog.archiveEnv, saveEnvRepo: catalog.saveEnvRepo,
|
|
models: catalog.models, modelCustomVisible: catalog.modelCustomVisible,
|
|
tasks: taskList.tasks, taskCountText: taskList.taskCountText, taskError: taskList.taskError,
|
|
refreshTasks: () => void taskList.refreshTasks(undefined, projectIdRef.current),
|
|
statusFilter: taskList.statusFilter, setStatusFilter: taskList.setStatusFilter,
|
|
executorFilter: taskList.executorFilter, setExecutorFilter: taskList.setExecutorFilter,
|
|
searchText: taskList.searchText, setSearchText: taskList.setSearchText,
|
|
filteredTasks: taskList.filteredTasks,
|
|
taskPage: taskList.page, taskTotalPages: taskList.totalPages, taskTotal: taskList.taskTotal,
|
|
taskStatusCounts: taskList.taskStatusCounts, goToTaskPage: taskList.goToPage,
|
|
currentTask: detail.currentTask, detailBody: detail.detailBody, actionMsg: detail.actionMsg, actionReasons: detail.actionReasons,
|
|
showDetail: (id: string) => void detail.showDetail(id),
|
|
doAction: detail.doAction, taskCost: detail.taskCost, costBox: detail.costBox, costSummary: detail.costSummary,
|
|
forkVisible: detail.forkVisible, setForkVisible: detail.setForkVisible, forkTask: detail.forkTask, forkMsg: detail.forkMsg,
|
|
coordChildren: detail.coordChildren, coordText: "", agents: detail.agents, agentsNote: detail.agentsNote,
|
|
reviews: detail.reviews, reviewMsg: detail.reviewMsg, reviewError: detail.reviewError,
|
|
refreshReviews: detail.refreshReviews, postReview: detail.postReview, submitFindings: detail.submitFindings,
|
|
traceEntries: detail.traceEntries, traceNote: detail.traceNote, traceDone: detail.traceDone,
|
|
traceKind: detail.traceKind, setTraceKind: detail.setTraceKind, downloadTrace: detail.downloadTrace, traceMsg: detail.traceMsg,
|
|
sendChatMessage: detail.sendChatMessage, chatMsg: detail.chatMsg,
|
|
chatHistory: detail.chatHistory,
|
|
followups: detail.followups,
|
|
approvals: detail.approvals, approvalMsg: detail.approvalMsg, refreshApprovals: detail.refreshApprovals, decideApproval: detail.decideApproval,
|
|
peerMessages: detail.peerMessages, peerMsg: detail.peerMsg, postPeer: detail.postPeer,
|
|
automations: tools.automations, autoMsg: tools.autoMsg, refreshAutomations: () => void tools.refreshAutomations(), createAutomation: tools.createAutomation, cancelAutomation: tools.cancelAutomation,
|
|
triage: tools.triage, triageError: tools.triageError, refreshTriage: () => void tools.refreshTriage(),
|
|
threadText: detail.threadText, threadGateNote: detail.threadGateNote, threadMsg: detail.threadMsg, threadList: detail.threadList, threadAction: detail.threadAction,
|
|
dispatch: async (body, atts) => {
|
|
const msg = await taskList.dispatch(body, atts);
|
|
void taskList.refreshTasksFirstPage(projectIdRef.current);
|
|
return msg;
|
|
},
|
|
dispatchMsg: "",
|
|
refreshAll, refreshSlow, refreshStamp,
|
|
};
|
|
}
|